Работа с COM портом в web-проекте

в 10:55, , рубрики: chrome application, chrome extension, Google Chrome, javascript, Веб-разработка, капризы

Пролог

Один из клиентов нашего web-проекта захотел использовать для поиска заказов в системе сканер штрихкодов. Но, к сожалению, полностью отказался от идеи работы с ними в режиме имитации клавиатуры — только эмуляция COM-порта.
Вариантов решения было не особенно много:

  • отдельное нативное приложение, которое бы отправляло запрос на наш сервер, а сервер бы отдавал команду в браузер
  • работа с COM портом непосредственно из браузера

К счастью, есть способ решения проблемы вторым путём.

Chrome Application

Если кто не знает, Chrome Application — это приложения для браузера Chrome, написанные на JavaScript. В этих приложениях доступно API для работы с последовательными портами. Этот вариант практически идеальный для нас.
Основная проблема состоит в том, что хоть у Chrome Application и есть подходящие инструменты, оно не может напрямую работать с открытыми страницами. Тут нам на помощь приходят расширения, которые такую возможность имеют.

Далее я постараюсь подробнее описать как всё это связать вместе, что бы это работало.

Эмуляция COM порта

К сожалению у меня не было возможности работать с реальным сканером, поэтому мне пришлось его эмулирвать.
Для этого я использовал socat:

  1. Запускаем:
    				socat -d -d pty,raw,echo=0 pty,raw,echo=0
    			

  2. Получаем ответ вида:
    				socat[1473] N PTY is /dev/ttys001
    				socat[1473] N PTY is /dev/ttys002
    				socat[1473] N starting data transfer loop with FDs [3,3] and [5,5]
    			

  3. В другом окне терминала выполняем:
    				cat > /dev/ttys001
    			

    вместо /dev/ttys001 указываем тот путь, что вернул socat
    И пишем любые сообщения.

  4. Для проверки, в третьем окне:
    				cat < /dev/ttys002
    			

    /dev/ttys002 — второй путь из socat.
    Написав сообщение во втором окне — получим его в третьем, если пришло — можно идти дальше.

Создание приложения

В документации достаточно хорошо расписан сам процесс, стоит только обратить внимание на то, что нам необходим доступ к работе с последовательными портами. Для этого в файле manifest.json указываем:

	"permissions": [
		"serial"
	]

Файл background.js содержит код самого приложения:

Листинг

	chrome.app.runtime.onLaunched.addListener(function() {
		chrome.serial.connect("/dev/ttys004", {bitrate: 115200}, onConnect);
	});
	var stringReceived = '';

	var onConnect = function(connectionInfo) {
		var connectionId = connectionInfo.connectionId;

		var onReceiveCallback = function(info) {
			if (info.connectionId == connectionId) {
				var str = arrayBufferToString(info.data);
				if (str.charAt(str.length-1) === 'n') {
					stringReceived += str.substring(0, str.length-1);
					chrome.runtime.sendMessage('dbmjhdcnjkeeopcmhbooojabanopplnd', {
						action: 'scanner', data: {
							barcode: stringReceived
						}
					});
					stringReceived = '';
				} else {
					stringReceived += str;
				}
			}
		};

		chrome.serial.onReceive.addListener(onReceiveCallback);
	};

	function arrayBufferToString (buffer) {
		var string = '';
		var bytes = new Uint8Array( buffer );
		var len = bytes.byteLength;
		for (var i = 0; i < len; i++) {
			string += String.fromCharCode( bytes[ i ] )
		}
		return string;
	}
	

Разберём его подробнее.

chrome.app.runtime.onLaunched.addListener — добавляет функцию в список, который выполняется при старте приложения.
chrome.serial.connect("/dev/ttys001", {bitrate: 115200}, onConnect) — подключаемся к необходимому нам порту, при установке соединения выполнится функция onConnect.
chrome.serial.onReceive.addListener(onReceiveCallback) — при получении сообщения — вызовется onReceiveCallback
chrome.runtime.sendMessage — функция, которая отправляет сообщение в другое приложение/расширение. Первый аргумент — уникальный ID расширения в которое мы отправляем сообщение — можно увидеть в списке установленных расширений (chrome://extensions/ — парсер ломает ссылку), второй аргумент — сами данные.

Создание расширения

Здесь тоже всё несложно и подробно описано в документации
Ключевые настройки из файла манифеста:

	"permissions": [
		"tabs",
		"file:///*"
	],
	"content_scripts": [
		{
			"matches": ["file:///*"],
			"js": ["action.js"]
		}
	],
	"background": {
		"persistent":	false,
		"scripts": 		["js/background.js"]
	}

permissions — указывает, что нам необходим доступ к вкладкам, далее указываем к каким (для тестов — указаны все локальные файлы file)
content_scripts — описывает какие дополнительные скрипты запускать на страницах
background — описывает скрипт расширения, который работает в фоне

В background.js содержится код, который отвечает за приём сообщения и отправку его в определённый таб

background.js

	var onMessage = function(data) {
		switch (data.action) {
			case 'scanner': {
				chrome.tabs.query({url: "file:///*"}, function(tab) {
					for (var i = 0; i < tab.length; i++) {
						chrome.tabs.sendMessage(tab[i].id, data);
					}
				});
			}
		}
	};
	chrome.runtime.onMessageExternal.addListener(onMessage);
	

chrome.tabs.query — делает выборку табов по критерию, в нашем случае это url = «file:///*»
Есть 2 способа выполнить js код на странице из расширения

  • chrome.tabs.executeScript — напрямую вызвать js код на странице, на мой взгляд не самый лучший вариант с точки зрения архитектуры
  • добавить через манифест content_scripts — то есть скрит, который добавится на все вкладки удовлетворяющие условиям, описанным в matches

Я выбрал второй вариант. Стоит заметить что любой код, выполняемый во вкладке из расширения, выполняется в специальном окружении. Это значит что он будет иметь полный доступ к DOM элементам, но не будет иметь доступа к любым переменным созданным во вкладке. Подробнее.
Оптимальный способ передать данные из расширения в код вкладки — воспользваться
CustomEvent

В файле action.js мы просто получаем сообщение из backgroud.js и создаём событие для document.

action.js

	chrome.runtime.onMessage.addListener(
	function(data) {
			var event = new CustomEvent(data.action, {detail: data.data});
			document.dispatchEvent(event);
		}
	);
	

Принимаем сообщение

Осталось самое простое — принять сообщение и сделать с ним желаемые действия, например просто вставить его в input

index.html

	<html>
		<head>
			<script type="text/javascript">
				document.addEventListener("scanner", function(e) { 
				    document.getElementById('barcode').value = e.detail.barcode;
				});
			</script>
		</head>
		<body>
			<input id="barcode">
		</body>
	</html>
	

Эпилог

В целом я был приятно удивлён тем, что chrome предоставляет API для работы с железом, в том числе не только для чтения, но и для записи.
К сожалению, после того как было сделано практически всё, клиент сообщил, что всё таки переведёт сканеры в режим имитации клавиатуры. Хоть нам в конечном счёте это не пригодилось — надеюсь этот материал будет кому-нибудь полезен.

P.S.

Если кому интересно могу рассказать про то, как мы создали и поддерживаем несколько одностраничных больших проектов с использованием backbone, как кэшируем всю верстку на стороне клиента.

Автор: Forx

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js