Работа с COM портом в web-проекте
Пролог Один из клиентов нашего web-проекта захотел использовать для поиска заказов в системе сканер штрихкодов. Но, к сожалению, полностью отказался от идеи работы с ними в режиме имитации клавиатуры — только эмуляция COM-порта.Вариантов решения было не особенно много: отдельное нативное приложение, которое бы отправляло запрос на наш сервер, а сервер бы отдавал команду в браузер работа с COM портом непосредственно из браузера К счастью, есть способ решения проблемы вторым путём.Chrome Application Если кто не знает, Chrome Application — это приложения для браузера Chrome, написанные на JavaScript. В этих приложениях доступно API для работы с последовательными портами. Этот вариант практически идеальный для нас.Основная проблема состоит в том, что хоть у Chrome Application и есть подходящие инструменты, оно не может напрямую работать с открытыми страницами. Тут нам на помощь приходят расширения, которые такую возможность имеют.Далее я постараюсь подробнее описать как всё это связать вместе, что бы это работало.
Эмуляция COM порта К сожалению у меня не было возможности работать с реальным сканером, поэтому мне пришлось его эмулирвать.Для этого я использовал socat: Запускаем: socat -d -d pty, raw, echo=0 pty, raw, echo=0 Получаем ответ вида: 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] В другом окне терминала выполняем: cat > /dev/ttys001 вместо /dev/ttys001 указываем тот путь, что вернул socatИ пишем любые сообщения. Для проверки, в третьем окне: 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) — при получении сообщения — вызовется onReceiveCallbackchrome.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); } ); Принимаем сообщение Осталось самое простое — принять сообщение и сделать с ним желаемые действия, например просто вставить его в inputindex.html
Эпилог В целом я был приятно удивлён тем, что chrome предоставляет API для работы с железом, в том числе не только для чтения, но и для записи.К сожалению, после того как было сделано практически всё, клиент сообщил, что всё таки переведёт сканеры в режим имитации клавиатуры. Хоть нам в конечном счёте это не пригодилось — надеюсь этот материал будет кому-нибудь полезен.P.S. Если кому интересно могу рассказать про то, как мы создали и поддерживаем несколько одностраничных больших проектов с использованием backbone, как кэшируем всю верстку на стороне клиента.