Как стать автором
Обновить
278.9
Конференции Олега Бунина (Онтико)
Профессиональные конференции для IT-разработчиков

Write Once Run Anywhere

Время на прочтение14 мин
Количество просмотров3.1K

Write Once Run Anywhere

Вспоминается мем, где человек говорит: «JavaScript — это круто, на нем можно делать роботов и мобильные приложения», а потом его душит собака. Я себя представляю таким человеком, но надеюсь, меня никто не задушит, потому что я делаю на JavaScript вещи, которые в принципе не положено на нем делать. Например, пульт управления машинкой с телефона или любого другого устройства. Прошивка, и вообще всё на JS. Мы разберем подробнее такую машинку, джойстик, часы и другие устройства, и посмотрим как их самостоятельно запрограммировать.

Меня зовут Илья Черторыльский, я Senior Community Lead в Райффайзен банке. Эта статья про WebBluetooth, WebUSB, WebSerial и WebHID. Полную версию выступления можно посмотреть на YouTube.

Чтобы было проще понимать некоторые вещи, давайте начнем по порядку.

Браузерная часть

В браузерной части для всех API потребуется:

  • HTTPS

  • Пользовательское действие (click или touch), потому что мы защищаем пользователя. Просто запускать Bluetooth или USB — опасно! Мало ли, вдруг у него подключена камера или еще что-то.

  • Расположение — в объекте navigator (navigator.bluetooth; navigator.usb; navigator.serial), там же, где находится работа с вибромоторчиками и навигацией, которые есть, например, в нашем телефоне.

WebBluetooth

WebBluetooth — это сам bluetooth, но в нашем контексте это Bluetooth Low Energy (BLE) — его более современная модель. Она потребляет меньше энергии и используется повсеместно. Например, в наушниках и часах от Apple. У таких устройств нас интересуют три протокола.

GAP (Generic Access Profile)

Первый протокол определяет то, как компьютер или браузер соединяются с bluetooth-устройством:

  • в каком режиме работают устройства;

  • как они соединяются между собой;

  • роль устройства;

  • параметры соединения.

На этом нет смысла заостряться, за нас все это делается.

GATT (Generic Attribute Profile) и ATT (Attribute Protocol)

Эти протоколы условно описывают следующую модель:

У нас есть профиль устройства (Profile/Device), у которого внутри есть сервисы. Их может быть много. В них в свою очередь вложены характеристики, их тоже может быть много. Каждая характеристика имеет значение. Его можно читать, писать или подписываться на нотификации из него. Например, в режиме нотификации работает многим известный гаджет — с браслета постоянно передаются данные на телефон, чтобы можно было посмотреть свой пульс.

У характеристик и сервисов есть уникальный идентификатор, который бывает 2 типов.

Стандартный 16-битный — справочный. Сделан для того, чтобы не было разброса по устройствам, чтобы не появлялись каждый день новые стандарты. Аккумулятор, или тот же датчик пульса, или количества кислорода в крови — у всего этого есть стандартные идентификаторы. К ним можно подключиться.

Если же вы сами разрабатываете какое-то устройство, и хотите, чтобы к нему никто кроме вас не мог подключиться, то можете использовать 128- битный собственный идентификатор.

Если упрощенно посмотреть на модель, которую описывают алгоритмы, она похожа на серверную:

У нас есть сервер, на сервере есть сервисы, у них есть API, которая возвращает значение. Это не что иное, как Client-Server. Все API, которые мы рассмотрим, адаптированы под эту модель. У нас все равно есть заголовки, которые мы отправляем, тело запроса, который мы отправляем и обрабатываем. В плане стандарта все учтено и удобно.

Как мы принципиально подключаемся к устройству?

В первой строке requestDevice — это стандартный запрос устройства. Обратите внимание, что нужно передать фильтры. Без фильтров выскочит ошибка, что нужно хоть что-то указать. Иначе мы заброадкастим сеть и выскочит вереница всего. Как только мы получили девайс, запрашиваем сервис. Как только запросили сервис, можно запрашивать из него характеристику.

Обратите внимание на то, что выделено красным. Это тот самый уникальный идентификатор сервиса, характеристики или девайса, который мы передаем, чтобы найти устройство.

После того, как мы получили характеристику, можно прочитать из нее или записать в нее значение, используя 8-битные массивы. С помощью стандартного addEventListener можно подписаться на то, что характеристика меняется. Обязательно надо вручную запустить startNotifications, чтобы она начала с нами обмениваться данными. Если устройство будет активно всегда, то быстро разрядится.

В Chrome и других браузерах на его основе есть стандартная системная страница chrome://bluetooth-internals. На ней можно посмотреть все про bluetooth устройства: какие сейчас подключены, как они работают, и как настроены.

Посмотрим фрагмент кода, который управляет машинкой:

 navigator.bluetooth

.requestDevice({ /*... */ })

.then((device) => {

return device.gatt.connect();

})

.then((server) => server.getPrimaryService(this.#deviceId))

.then((service) => service.getCharacteristic(this.#characteristicId))

.then((characteristic) => {

this.#send = (buf) => {

if (this.#isSend) {

this.#isSend = false;

characteristic

.writeValue(buf)

.then(() => (this.#isSend = true));

}

};

});

listen = (callback) => {

this.#characteristic.startNotifications();

this.#characteristic.addEventListener(

‘characteristicvaluechanged',

() => {

this.read().then(callback);

}

);

}

Мы делаем requestDevice с необходимыми нам фильтрами, чтобы найти машинку. Потом подключаемся к устройству, возвращаем подключенный экземпляр устройства. Я даже специально назвал его server, чтобы было понятней. Потом из этого устройства запрашиваем getPrimaryService по deviceId или serviceId. После того, когда получаем service, берем из него характеристику по characteristicId. Как только получили её, можно определить метод send. Он будет проверять одновременные отправки, чтобы не засорять эфир и устройство лишними запросами. Соответственно, мы ставим, что у нас еще ничего не отправлено и делаем запись значения в характеристику. Как только в характеристику что-то успешно записалось, мы это снимаем, ставим sended = true и разрешаем следующий запрос.

Также здесь присутствует фрагмент блока, как мы подписываемся на нотификацию. Запускаем и ловим данные с парктроника машинки. То есть мы подписались, поймали событие, соответственно, записали что-то в устройство.

Ещё есть фрагмент кода джойстика:

 stick ?.addEventListener( ‘change’, ({ detail }) => {

if (this.#BLEinstanse.device) {

this.#BLEinstanse.send(new Uint8Array([detail.relativeX, detail.relativeY]));

}

});

Когда мы двигаем джойстиком, в инстанс нашего устройства записывается 8-битный массив, где первый байт — положение джойстика по оси X, а второй байт — по оси Y. Мы едем вперед, стоим на месте или едем назад с разной скоростью.

WebUSB

WebUSB тоже похож на клиент-серверную модель. У нас есть хост и клиент. В роли хоста выступает наш компьютер, а в роли клиента USB-устройство. У него есть CDC (Communication Device Class), то есть класс устройств, с которыми мы будем общаться. Под это устройство наш хост (компьютер) выбирает класс драйвер, то есть драйвер устройства, как он будет с ним общаться. Также устройство описывается дескрипторами. Клиент с хостом обмениваются протоколами (это устоявшиеся термины, они будут присутствовать даже в API):

  • OUT transfers — то, что идет от компьютера к устройству;

  • IN transfers — то, что идет от устройства к компьютеру.

Рассмотрим подробней, как устройство выглядит и как оно работает внутри.

У нас есть основной дескриптор — это Device descriptor, который говорит о том, что это за устройство, какой драйвер к нему применить. Также есть Configuration descriptor — это аналог service в Bluetooth.

У Configuration descriptor может быть вложено много разных интерфейсов. Внутри себя они имеют конечные точки, с которыми можно связаться. Это аналог value у Bluetooth.

Еще нам важен URL descriptor, который есть в USB устройстве, когда оно поддерживает WebUSB. URL descriptor пока еще описывается стандартом draft.

Он принимает адрес и протоколы HTTP или HTTPS, аналог ORS для USB. У Bluetooth такого нет, потому что его подключение контролируется. USB-девайс может торчать где-нибудь сзади в компьютере, и вы даже не знать кто его воткнул. Поэтому если любой сайт по вашему клику получит доступ к USB-устройству, это будет очень плохо. Для защиты существует KORS.

Как это выглядит в коде на C:

Это наш дескриптор, когда мы создаем экземпляр класса WebUSB. Мы задаем цифру 1, согласно блоку из описания стандарта.

С WebUSB в браузере мы работаем так же, как с Bluetooth:

let device = await navigator.usb.requestDevice({ filters: [ { vendorld: 0x2341 }] });

let device = await navigator.usb.getDevices();

device.open();

device.selectConfiguration(1);

device.claimInterface(2);

device.controlTransferOut({ ... });

device.transferin(5, 64);

Есть requestDevice, то есть мы можем запросить какое-то устройство. Метод getDevices — мы получаем все устройства, которые ранее были подключены к этому компьютеру в виде массива устройств. После получения, открываем устройство и выбираем конфигурацию. Само устройство содержит массив конфигураций — от 0 до максимально возможного. Мы говорим, что хотим работать с первой конфигурацией. Дальше, что хотим выбрать интерфейс №2. После чего задаем правило, как будем отсылать данные с компьютера на наше устройство. Дальше можно, например, сказать, что мы хотим из устройства прочитать по 5 endpoint 64 байта— transferIn (5, 64). Или же, наоборот, хотим в устройство записать по 5 endpoint какой-то типизированный массив — device.transferOut(5, [TypedArray]);

В браузерах Chrome есть полезная страница: chrome://usb-internals, где можно прочитать много интересного про USB.

В центре устройство Arduino-micro с микроконтроллером, который позволяет записать на него прошивку на С. Она содержит библиотеку WebUSB, которая регистрирует его, как устройство USB. Если к нему подключиться, можно из браузера переключать светодиод и задавать ему какое-то значение по цвету. При этом все работает внутри, даже при отключении устройства, светодиод останется в цвете. 

Сам интерфейс выглядит так:

В коде можно увидеть, как происходит подключение. Мы запрашиваем по фильтрам все Arduino-подобные устройства. У них есть usbVendorId и usbProductId. Если открыть описание устройств и нажать «Сведения», увидим ID-оборудование (VendorId 2.3.41 и ProductId 80.37.):

Само устройство может быть USB-составное. На скриншоте есть подключение к устройству Arduino micro. По интерфейсу 0 устройство связывается, например, с ArduinoIDE, прошивается, то есть на нем расположен серийный порт, а взаимодействие происходит со вторым интерфейсом.

WebSerial

Серийные порты стары как мир, сейчас их уже не встретишь.

У нас опять написано usbVendorId и usbProductId — это дань тому, что сейчас нет COM-портов в компьютерах. Все они эмулируются через USB, поэтому также называются параметры. А с устройством мы связываемся так же просто, как и с другими:

let port = await navigator.serial.requestPorts({ filters });

let port = await navigator. serial.getPorts();

await port.open({ baudRate: 9600 });

const reader = port.readable.getReader();

const { value, done } = await reader.read();

const writer = portwriteble.getWriter();

await writer.write([Uint8Array]);

Только оно называется не device, а port — requestPorts или getPorts, если уже сработало. После этого мы открываем порт, устанавливая скорость работы с ним. Дальше можно создать reader, который прослушивает данные с порта, и writer, который записывает данные в порт. В принципе, больше ничего интересного здесь нет.

Полезные данные в Chrome: chrome://device-log

Серийный порт мы рассматривать не будем, потому что мне не пришло в голову, что можно интересного про него написать.

WebHID

Джойстик общается с компьютером с помощью reports (отчетов):

Это бинарник. Если его перевернуть, увидим, что каждый байт — это 8 бит. Которые, например, хранят информацию о смещении джойстика по какой-то оси, о том, какие клавиши нажаты. Чтобы не создавать еще один лишний байт, нажатие стрелок влево-вниз закодировано в 4 оставшиеся.

Посмотрим, как работает фрагмент кода более подробно.

Первым делом в отчете запрашиваем байт со смещением 4. Это означает, что мы отступаем 4 байта и берем пятый. Каждый байт — это 8 бит, допустим 8 нулей. Представим, что от джойстика пришло значение 0100100. Это 68 в десятичном представлении. В четвертой строке мы проверяем, зажат ли кружочек. Делаем логическое сложение с 0х40, получается 64. 64 — это не 0. А все, что не 0 — это true. То есть кружочек зажат — всё просто.

А если проверять не с 0х40, а с другим значением, будет лишнее смещение, получим 0 — false (кружочек не зажат).

Далее применяем фильтр, отрезаем первые 4 байта, то есть умножаем их на 0. Оставшиеся берем с единицами, перемножаем и получаем 100. Это 4 в десятичном представлении. Соответственно, нажата стрелка вниз.

С такими устройствами сложнее работать, но на них есть спецификации. В своем GitHub-репозитории я все это разобрал.

Аппаратная часть

Если вы работали с Arduino, то наверняка задумывались, что там мало места. Получается исполнять код только на прямую из памяти контроллера, поэтому надо писать интерпретатор. А если писать интерпретатор, то почему бы не на JS. Он может быть запущен почти на любом устройстве, и интерпретатор может быть запущен на чем угодно. Многие люди задавались этим вопросом и написали кучу библиотек.

Проще всего запустить JavaScript на устройствах типа Raspberry. Достаточно скачать Node.js, набрать node example.js и всё заработает. Но мы их не будем рассматривать, потому что Raspberry много потребляют и от аккумулятора долго не проживут.

Наши устройства низко потребляемые и маломощные рассчитанные для интернета вещей: процессор 160 МГц, 4 Мб памяти и 512 Кб оперативки.

Для того, чтобы с ними работать нужно:

1. Выбрать устройство;

2. Выбрать «встраиваемый» движок;

3. Изучить его API;

4. Скачать toolchain для прошивки;

5. Прошить микроконтроллер интерпретатором;

6. Прошить МК полезным кодом (записать в оставшееся место полезный код на JS);

7. Повторить шаги с 1 по 6 столько раз, сколько устройств хотим наклепать.

Что же это за магические штуки микроконтроллеры, микропроцессоры, System-on-a-Chip?

Микроконтроллеры и микропроцессоры

Наверное, все знают, что микропроцессоры — это центральная часть компьютера, которая исполняет команды, отвечает за сложение, вычитание бит и прочее в бинарном мире. Микроконтроллер Arduino работает на архитектуре AVR. В него встроена плата atmega 328.

Её и программатор можно купить в магазине, приделать кремний, чтобы задавал частоту — и вы получите свой собственный Arduino. Все остальное на плате — это просто обвязка.

Есть вещи поменьше, на которых можно делать маленькие штуки, а есть вещи посложнее — это System on Chip. В чем между ними разница? Микропроцессор — это только процессор, а микроконтроллеры помимо устройства, которое задает частоту и определяет, как обрабатываются команды, имеют какую-то периферию. Например, аналогово-цифровые преобразователи, цифро-аналоговые преобразователи, таймеры, счетчики и прочее. Поэтому в данном контексте нас интересуют именно микроконтроллеры.

Микроконтроллеры — это Arduino и Arduino-подобные устройства. Устройства System on Chip — это Johny-five, Elk.js, Espruino, Moddable, Kaluma. Давайте разберем их подробнее.

Johny-five

Johny-five или его аналог cylonjs работает на Firmata.

У нас есть стандартное устройство Arduino Uno. Мы в него записываем прошивку скетч с Firmata. Он есть в стандартном наборе Arduino IDE. А дальше скачиваем npm install johnny-five, npm install Firmata, и выполняем простенький код на ноде. Сначала запрашиваем, что нам нужна плата, на плате нас интересует лампочка, создаем новый инстанс платы по 6 порту (это просто терминальный сервер). После того как плата определилась, создаем новый экземпляр лампочки по 13 контакту на плате, и раз в 500 мс ей мерцаем. У меня есть такая прошивка. Мы подключаемся к устройству, создаем два экземпляра лампочек. Одной мерцаем, вторую включаем в режиме стробоскопа.

Так это выглядит в коде:

var five = require("johnny-five"),

board = new five.Board({ port: ‘COM6‘ });

board.on("ready", function () {

var led = new five.Led(13);

var led2 = new five.Led(12);

setInterval(() => {

led2.toggle();

}, 1000);

led. Strobe();

});

Теперь давайте посмотрим другие интерпретаторы.

Elk.js

Допустим, у нас есть Arduino micro. 

Мы подключаем библиотеку elk.h. Если хотим дергать то, что было реализовано раньше (обратиться к процессору, видеокарте, еще чему-то), в мире браузера мы пишем файл JavaScript’овой склейки. В первых трех строках оговариваем методы, которые у нас есть на C для работы с микроконтроллером, и создаем для них функции обвязки. Дальше говорим, что хотим выделить буфер в 300 бит для прошивки. После чего в настройках создаем новый экземпляр пространства для JS, раздел global и gpio. Внутрь global записываем gpio, а туда функцию delay. У gpio внутри будет встроен mode и write. Последним блоком мы пишем нашу прошивку.

Сама библиотека Elk.js устроена просто. Она инициирует работу только со стандартным синтаксисом — присвоение переменных, циклы, блоки кода и больше ничего. Все остальное мы записываем сами: регистрируем gpio и delay, и они появляются в глобальном пространстве имен, как будто бы это window или реальный global для ноды.

У gpio есть write. Мы инициализируем первый pin и начинаем им мигать, переворачивая 0, 1, 0, 1. Такой код у нас на Elk.js.

Это лучше Firmata. Этот код можно вынести на карту памяти и повысить количество полезного места на микроконтроллере. Если вы отключите компьютер после того, как прошьёте, у вас всё равно всё будет работать.

Давайте дальше.

Espruino

Это довольно известный бренд. Возможно, вы про него слышали, Амперка использует их в своих товарах. Давайте посмотрим что еще есть на примере машинки.

То, что мигает красной лампочкой — это микроконтроллер, сбоку с зеленой лампой — это Bluetooth модуль, сзади с красной лампой плата для работы с двигателем, а спереди ультразвуковой датчик встроенного парктроника.

Модуль подключается к плате по протоколу serial. Когда вы его поставите, будете работать с ним как с обычным устройством на серийном порту. Посмотрим фрагмент кода (прошивка), который работает на машинке.

Мы вызываем стандартный метод API для этого интерпретатора init, когда у нас инициализируется прошивка. Дальше первой строкой говорим, что у нас на 33 ножке (все ножки пронумерованы) висит сервопривод. Дальше мы говорим, что устанавливаем его в позицию 90о, после чего говорим, что еще 2 ножках двигатель. Что на серийном порту tx: D4, rx: D15 установлена плата bluetooth модуля. Потом подключаем ультразвуковой датчик, set-интервалом устанавливаем, что мы будем его опрашивать. А когда опросили, записываем в наш серийный порт данные с этого датчика и ловим его уже в браузере.

В последнем блоке берем то, что получили из компьютера: смотрим, если значение по скорости больше 0, то разгоняем моторчик вперед, если равно 0, то машинка стоит на месте, если меньше 0, то едет назад. Соответственно, в сервопривод записываем угол смещения.

В принципе, всё, ничего другого там нет.

Теперь поговорим про штуку на моей руке, которой я управлял слайдами на конференции.

Казалось бы, на ней уже целый сервер поднят. На самом деле все тоже очень просто:

Мы запрашиваем Wi-Fi из require, модуль lcd (драйвер работы с модулем экрана) и http. Это прямо аналог ноды. Дальше настраиваем экран, говорим, что у нас висит на конкретных ножках. Подключаемся к экрану, lcd.connect на ножки, и на ту, которая сбрасывает экран, чтобы он перерисовывался. После этого создаем метод pageHandler, который отвечает за передачу данных, когда показывается слайд, формируем заголовок ответа и ставим Content-Type.

Я изначально все перекодировал в gzip, потому что тяжело отдавать большие файлы.

Потом устанавливаем еще время жизни, чтобы не загружать их заново. Делаем: file.pipe(res, { chunkSize: 1024 }). Именно из-за этого мы открывали E.openFile pageHandler методом. У него будет метод pipe, потому что мы ограничены оперативкой нашей платы, там 512 Кб, а файл может быть и 900 Кб. Например, PDF у меня даже 1200 Мб. Поэтому мы режем его по частям и отдаем постепенно. Скорость передачи данных получается 1000 бит (1 байт) в секунду. Это довольно медленно, но для логов и прочего сгодится.

Мы это прописали в pageHandler и дальше прописываем, что хотим соединиться с картой памяти, подключиться к Wi-Fi, получить IP, читать события, и все это повесить на 80 порт. Остальное произойдет без нас — само получение IP адреса, подключение к Wi-Fi и на нем поднимется сервер.

После чего мы выписываем на экран IP адрес, чтобы знать его и пробить на странице. Дальше выключаем режим работы точки, как access point, переводим ее в режим клиента, и в принципе все.

Ещё у меня есть часы. Они интересны тем, что как раз продает сообщество Espruino — это Bangle JS. У них и прошивка, и приложение на JavaScript. Вы даже прошиваете их через JS из браузера.

На них выведен логотип Райффайзен банка. Есть панель с настройками, встроенный датчик GPS, гироскоп, компас. Давайте подключимся к ним и прошьем. 

Если мы хотим написать свою прошивку, то берем клон репозитория Bangle.js App и клонируем к себе. Создаем папку, например, raffclock со своим приложением для часов. Тут есть много разных приложений. Создаем файл иконки, в нашем случае это логотип банка. Эту иконку перекодируем в 2 строки. После чего идут просто стандартные методы рисования — и все, часы готовы.

Как мы прошиваем? Просто публикуем всё это у себя на GitHub и там появляется страница приложений с кнопкой «Connect» справа вверху.

Дальше можно нажать на эту кнопку и будет использован webBluetooth. Мы увидим наши часы, выберем «Подключиться». После этого можно посмотреть, какие приложения на них уже установлены . Можно удалить их или записать новые из библиотеки.

Kaluma и Moddable

У Kaluma для Raspberry Pico есть такая же web IDE, то есть можно ничего лишнего себе не ставить, а прямо из браузера все прошивать, как у Espruino.

Moddable более сложен. Он позиционируется на бизнес-сегмент для того, чтобы делать IoT устройства, которые работают на JS.

Экосистема

В заключение хочу поделиться несколькими интересными сайтами.

Симуляторы микроконтроллеров:

https://wokwi.com/

https://www.tinkercad.com/dashboard

https://create.arduino.cc/editor/

Это для того, чтобы прямо в браузере писать код на C, натыкивать туда лампочки, макетную плату и все остальное. Код будет компилироваться прямо в браузере, анимация перетекать в USB порт и плата начнет работать. Это сделано благодаря библиотечке avr8js — полностью портированной на JS 8 битной архитектуры avr.

Симуляторы схем:

https://www.circuito.io/

http://falstad.com/circuit/

http://falstad.com/circuit/avr8js/

https://everycircuit.com/app

http://opencircuits.net/register

https://www.partsim.com/simulator

Это написанные на JS эмуляторы полностью электрических цепей. Там можно самим натыкивать транзисторы, резисторы, все, что угодно. И смотреть как все работает — без спайки и вообще без ничего.

Разное и интересное:

https://copy.sh/v86 — это специальная опенсорсная система для прошивки микроконтроллеров на JS. 

https://github.com/noopkat/avrgirl-arduino — здесь вы найдете много интересных библиотек по работе с маленькими платами.

https://github.com/thingsSDK/flasher.js — здесь можно прямо в браузере запустить полноценную виртуалку на JS.

Вместо заключения хочу сказать: «JavaScript — Write once run everywhere».

До 12 августа открыт прием заявок на доклады на Highload++ 2022 (24 и 25 ноября 2022 в Москве). Ждем ваши заявки. Все подробности на сайте https://cfp.highload.ru/moscow

Теги:
Хабы:
+5
Комментарии1

Публикации

Информация

Сайт
www.ontico.ru
Дата регистрации
Дата основания
Численность
31–50 человек
Местоположение
Россия