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

  • Tutorial

Пролог


Один из клиентов нашего 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, как кэшируем всю верстку на стороне клиента.
Поделиться публикацией
Похожие публикации
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 31
  • 0
    Как обстоят дела с отзывчивостью? Заметна ли задержка между звуковым сигналом сканера и откликом приложения?
    • 0
      Как я и писал в статье — с реальным сканером так и не протестировал, но задержки между командой в консоль и появлением текста в input я глазом заметить не могу.
    • +5
      Если кому интересно могу рассказать про то, как мы создали и поддерживаем несколько одностраничных больших проектов с использованием backbone, как кэшируем всю верстку на стороне клиента.

      Если кеширование значительно интереснее, чем простое складывание в localStorage — интересно, рассказывай.

      И вопрос по статье: оффлайн работа приложения предусмотрена? Например если есть топ 100 товаров, что бы они при отсутствии подключения отображались. Ну и вообще изначальный запуск без интернета возможен, либо логика приложения такого не предусматривала вообще?

      Когда то решал вопрос работы онлайн-плеера при отсутствии интернета. На тот момент лимит в localStorage был что-то около 5гб, а не как сейчас 5мб, так что значительную часть логики и треков можно было хранить в нём, так же запускать и слушать при отсутствии интернета.
      Ещё кассу для кинотеатра делал схожим образом. Отсутствие интернета не должно было сказываться на продажу билетов.
      Может быть как-нибудь созрею написать статю про работу онлайн проектов, которые должны работать даже без подключения.
      • +1
        По поводу кэширования — средствами самого браузера, то есть так же как кэширование стилей и скриптов.
        У нас все заказы хранятся на стороне клиента, поиск тоже работает без участия сервера, так что отсутствие интернета не повлияет на работу.
        Кстати, ограничение на размер localStorage тоже можно решить с помощью расширения.
        • 0
          Я примерно понял, о чем ты говоришь. Вероятно, схожую схему использовал для кеширования получаемых данных. Там трюк в том, что ajax запрос тянет из кэша, даже не отправляя на сервер запроса были/нет изменения, верно?

          решить с помощью расширения.

          Ну с раширением там уж можно и получше хранилище использовать, но это не всегда применимо, и не всегда удобно.
          Иногда надо что бы работало без расширений и дополнений — более девайсо/браузеро независимо выходит.
          • 0
            На самом деле с кэшированием всё проще — просто отдаём всю вёрстку запакованную в js файле, распаковываем на клиенте, при обновлении страницы этот файл уже выдернется из кэша.
            • 0
              Для этого можно не паковать в JS, если что :-)
              С отдельными файлами можно через зависимости с помощью, например, stealjs. Сами сгрупируются, запакуются и т.п.
              Во всяком случае звучит проще, чем отдельно специально паковать вёрстку.
              • 0
                Конечно можно не паковать, просто не хотелось отдельно перенастраивать правила кэширования.
                Верстка переупаковывается самописным скриптом при обновлении продакшена, он же пакует стили и скрипты.
                • 0
                  Давай, как и предлагал, отдельной статьёй. Почитаю с удовольствием :-)
        • 0
          К рассказу еще б неплохо исходнички и демки.
          • +3
            А не думали об установке локального приложения, слушающего некий порт на 127.0.0.1 и работающего с браузером через веб-сокеты? Тоже интересный вариант, как по мне.
            • +5
              Думали, но решение средствами самого браузера проще, на мой взгляд.
              • +1
                Сетевой вариант хорош еще возможностью удаленного доступа.
                Я так интерфейсы управления железяками и делаю. Вот, недавно перешел на вебсокеты. Удобно. Можно даже отказаться от механизма CGI, а при желании — даже от установки веб-сервера. Просто при старте компьютера запускается демон, берущий на себя роль сервера и запускающий вебсокет.
                Проверял отзывчивость при работе на расстоянии около пяти километров (оптоволокно): паузы незаметны.
                Ну и еще один плюс такого подхода: любой браузер. Все-таки, хромой — поделка жуткая…
              • +3
                Дополнительные приложения сложнее поддерживать.
                С браузером всё значительно проще — можно попросить обновить страницу, обновить расширение.
                Можно наладить автоматическое обновление расширений и т.п.
                • +2
                  Возможно, я невнимательно читал, но ведь в данном случае будет работать только хром. А в случае внешнего приложения и через веб-сокеты будут работать вообще все современные браузеры (и IE10+, если не ошибаюсь). Один браузер — очень жесткое ограничение, мне кажется.
                  • 0
                    В данном случае, вероятно, да.
                    Но вообще сейчас есть библиотеки, позволяющие писать плагин сразу под все браузеры (включая IE10), т.е. работать будет сразу во всех.

                    Я более широкий круг задач подразумевал, а не конкретное решение. Иногда даже плагины не нужны, достаточно просто правильной настройки браузера на конечной машине через about:config или аналог.
                    • +2
                      Да, будет работать только хром, но, к счастью, хром уже был, к тому же практически все наши пользователи используют хром. Установка внешнего приложения у корпоративного клиента затруднена ограничениями внутренного IT отдела, а поставить хром — пожалуйста.
                  • 0
                    Это решение прекрасно впишется в ситуацию, когда к сканеру нужно допустить несколько приложений — доступ к COM-порту ведь монопольный.
                  • 0
                    Есть программы, которые данные с ком-порта преобразуют в имитацию нажатий клавиатуры…
                    • 0
                      Суть в том что у клиента установлен софт, в котором тоже работают сканеры, и он очень настаивал на том, что бы не перенастраивать его.
                    • 0
                      Интересно попробовать отправлять даниє з вкладки на термопринтер Citizen CLP-621 которий подключений через usb в режиме емуляции COM.
                      • 0
                        С принтерами всё просто на самом деле.
                        Браузер сам по себе может печатать на принтер, а на какой уже не важно.
                        В браузере убираются поля и всё-всё-всё лишнее, подгоняется под нужный формат.
                        .print() -> пошла печать. Что бы окна не вылазили настраивается silient_print в about:config (или как то так называется).
                        Всё работает на отлично, проверено. Отправкой данных на сам принтер занимается ось.
                        • +2
                          Можно и через инсталирований драйвер печатать, а можно отправлять коди, например печать интегрированим шрифтом, рендер штрихкода самим принтером, температура печати и многиє параметри. Вииграш скорость печати.
                          • 0
                            Идея конечно хорошая, но только предоставляет ли Citizen информацию по кодам для этого принтера? Если она есть, и есть сам принтер — грех не попробовать.
                            • 0
                              Принтер поддерживает фактически уже стандартный язык Zebra ZPL-II.
                            • 0
                              Звучит здорово, да. Может если столкнусь ещё с подобным, то попробую так.
                        • 0
                          У меня крамольный вопрос назрел — а насколько внятно браузер спрашивает у пользователя разрешение на доступ к порту? Несмотря на то что у многих интернет подключен по Ethernet или Wi-Fi, многие GPRS-модемы для системы выглядят как старый добрый модем на COM-порту, со всеми вытекающими последствиями. История идет на второй виток?
                          • +2
                            При установке расширения в списке необходимых ему разрешений будет такое


                            Так что просто нужно быть внимательным при установке расширений.
                          • 0
                            Спасибо за информацию.

                            Я когда-то решал подобного плана задачу — надо было из веб-приложения (рабочее место оператора по складскому учету/выдаче заказов), работать с принтером наклеек — тогда решил просто — поднял на компьютере подключенном к принтеру наклеек — отдельное небольшое веб-приложение — и уже с ним (на localhost) общался из браузера, с которым работал оператор.

                            То есть оператор нажимает в веб интерфейсе «напечатать наклейку» — происходит JavaScript-ajax запрос (с параметрами текста для печати) на localhost — где поднят веб-сервер общающийся с принтером наклеек (по сути он просто на LPT посылал текст) — и происходила печать наклейки на рабочем месте оператора.
                            • 0
                              >Если кому интересно могу рассказать про то, как мы создали и поддерживаем несколько одностраничных больших проектов с использованием backbone, как кэшируем всю верстку на стороне клиента.

                              Насколько я понял из комментариев выше вы используете что-то вроде basket.js?

                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.