Что вы узнаете
Что вы узнаетеПо итогу вы узнаете, как отправлять и принимать данные от устройства с последовательным портом в браузере, разработаете демонстрационное веб-приложение, которое отправляет команды в микроконтроллер с последовательным портом и напишите прошивку для микроконтроллера AVR128DA48 для обработки этих команд. Готовый вариант проекта можно посмотреть здесь, исходники загружены в репозиторий на GitHub.
Что это и где применяется
Что это и где применяетсяWebSerial API - это браузерный API, который предоставляет возможность к чтению и записи данных в последовательное устройство. Использование этого API довольно простое, нужно лишь написать несколько строк кода, чтобы получить или отправить ваши данные. К тому же имеется подробная документация, где имеются примеры использования этого API.
Благодаря этому API, открываются новые возможности. Например, вы можете перепрошивать ваш микроконтроллер прямо из браузера, что звучит весьма интересно (но перед этим вам необходимо написать bootloader), или же просто выводить информацию с датчиков на веб страницу. Вариантов применения неограниченно, все зависит только от вашей фантазии.
Еще существуют и высокоуровневые API для доступа к таким устройствам как геймпад или камера. Я оставлю эту ссылку на эту статью, где рассказывается про список этих API.
Поддержка браузеров
Поддержка браузеровНа момент написания статьи (2022 год) полноценная поддержка этого API доступна лишь в браузерах на базе Chromium. Полную таблицу можно посмотреть на сайте Can I Use.
На Android поддержка может быть осуществлена на базе WebUSB API и полифилла Serial API.
Краткий экскурс про UART
Краткий экскурс про UARTПеред работой с последовательным портом, неплохо было бы знать основы протокола передачи данных UART, если вы с ним уже знакомы, можете пропустить эту часть.
UART - расшифровывается как (Universal Asynchronous Receiver Transmitter) - универсальный асинхронный приемопередатчик, который определяет протокол для приема и передачи данных между двумя устройствами. Универсальность означает, что мы можем настраивать параметры, включая скорость передачи данных. Асинхронность же означает, что у нас нет синхронизирующего сигнала между передатчиком и приёмником. Существует также и синхронная версия передачи данных с общим тактовым сигналом, которая может передавать данные намного быстрее, но она используется редко. Для связи двух устройств используются лишь две линии, которые обозначаются как RX (receiver) и TX (transmitter).
UART передает данные последовательно по одному биту в одном из трех режимов:
- односторонний режим - данные отправляются только в одном направлении, от передатчика к приемнику
- полудуплексный режим - устройства могут передавать и принимать данные по очереди
- полнодуплексный режим - устройства могут передавать и принимать данные одновременно
Так как в этом проекте предполагается использование асинхронной версии UART, то соответственно необходимо устанавливать одинаковую скорость передачи битов в секунду (baud rate
, bps
) на двух устройствах. Наиболее распространенные скорости являются: 4800, 9600, 19.2 К, 57.6К и 115.2К.
Данные передаются в виде так называемых пакетов
, где каждый пакет содержит в себе:
- стартовый и стоповый биты - стартовый бит сигнализирует о поступлении битов данных, стоповый соответственно о конце данных.
- биты данных - пользовательские данные, которые поступают сразу после стартового бита, может содержаться от 5 до 8 битов.
- бит четности - необязательный бит, который идет после битов данных и перед стоповым битом и используется для обнаружения ошибок.
Я думаю этой информации вполне достаточно для того, чтобы понимать, что из себя представляет UART. Если вы хотите более подробно изучить этот интерфейс, то в интернете существует множество статей на эту тему.
Преобразователь интерфейсов
Преобразователь интерфейсовПоскольку скорее всего вы будете подключать микроконтроллер к USB порту компьютера, вам необходим некий переводчик между двумя интерфейсами для отправки и получения данных, так как USB и UART совершенно разные протоколы передачи данных. Таких преобразователей существует множество, вот одни из них: CH340, CP2102 или FT232. Если вы используете отладочную плату Arduino UNO, то у вас уже используется один из этих преобразователей на вашей плате.
Если же на вашей отладочной плате не оказалось этого преобразователя интерфейсов, то вы можете приобрести модуль в любом магазине, цена на него обычно не превышает $5.
Разработка прошивки для микроконтроллера
Разработка прошивки для микроконтроллераКак я упомянул ранее, прошивка будет написана для микроконтроллера AVR128DA48. На самом деле неважно какой микроконтроллер вы используете, вам достаточно нужно уметь отправлять и принимать данные используя UART периферию вашего микроконтроллера.
Суть программы будет очень проста: необходимо написать некий обработчик команд который будет принимать на вход строку (например led_toggle
) и вызывать необходимую функцию, которая будет выполнять какое-то действие, в данном случае переключать светодиод.
Постановка задачи ясна, теперь рассмотрим реализацию данной программы. Я буду использовать среду разработки MPLAB X IDE и язык C для написания прошивки.
Необходимые константы
Необходимые константыДля начала для всего проекта определим список констант, создав файл constants.h
:
#ifndef CONSTANTS_H
#define CONSTANTS_H
#define F_CPU 4000000UL
#define BAUD_RATE 9600
#define BUFFER_SIZE 20
#define EOT 0x04
#endif /* CONSTANTS_H */
F_CPU
- тактовая частота микроконтроллераBAUD_RATE
- скорость передачи данных UARTBUFFER_SIZE
- размер буфера, куда будем складывать поступающие данныеEOT
- End-of-Transmission, ASCII символ конца передачи данных
Инициализация USART
Инициализация USARTСоздадим заголовочный файл usart.h
и определим несколько функций для инициализации USART, отправки символа и отправки строки.
#include "constants.h"
#ifndef USART_H
#define USART_H
#define USART1_BAUD_RATE(BAUD_RATE) ((float)(64 * F_CPU / (16 * (float)BAUD_RATE)) + 0.5)
#include <avr/io.h>
#include <stdio.h>
#include <string.h>
void USART1_Initialize(void);
void USART1_SendChar(char c);
void USART1_SendString(char *str);
#endif /* USART_H */
Макрос USART1_BAUD_RATE
вычисляет значение, которое необходимо записать в регистр USARTn.BAUD
для установки скорости передачи данных. Формула взята из документации к микроконтроллеру (глава 25.3.2.2.1, таблица 25-1).
Реализуем функцию инициализации USART в файле usart.c
:
void USART1_Initialize(void) {
/* set baud rate */
USART1.BAUD = (uint16_t) (USART1_BAUD_RATE(BAUD_RATE));
/* set char size in a frame to 8 bit */
USART1.CTRLC = USART_CHSIZE0_bm | USART_CHSIZE1_bm;
/* config pins for TX and RX */
PORTC.DIRSET = PIN0_bm;
PORTC.DIRCLR = PIN1_bm;
/* enable reveice complete interrupt */
USART1.CTRLA = USART_RXCIE_bm;
/* enable transmitter and receiver */
USART1.CTRLB = USART_TXEN_bm | USART_RXEN_bm;
}
Для полнодуплексного режима инициализация асинхронной версии USART происходит следующим образом:
- Конфигурация скорости передачи данных путем записывания значения в регистр
USARTn.BAUD
- Конфигурация размера
фрейма
, в нашем случае это 8 бит. - Конфигурация пина
TX
на выход и пинаRX
на вход. - Включение прерывания на то, когда закончился прием данных (когда пришел один пакет).
- Включение приемника и передатчика.
Теперь, рассмотрим функции отправки данных:
void USART1_SendChar(char c) {
while (!(USART1.STATUS & USART_DREIF_bm));
USART1.TXDATAL = c;
}
void USART1_SendString(char *str) {
for (size_t i = 0; i < strlen(str); i++) {
USART1_SendChar(str[i]);
}
}
Для того чтобы отправить данные, необходимо записать в регистр USARTn.TXDATAL
один байт, но перед этим нужно убедиться, что предыдущая передача была завершена путем проверки регистра USARTn.STATUS
. Как указано в документации, бит DREIF
(Data Register Empty Flag) является установленным, если данные в TX буфере отсутствуют. Соответственно можно сделать пустой цикл который будет выполняться пока у нас есть данные в TX буфере.
Функция отправки строки является некой оберткой, которая будет вызывать функцию отправки одного символа.
Обработчик команд
Обработчик командКак видно из постановки задачи, на вход нам поступает некая строка, которая говорит о том, какое действие должно выполниться.
Определим заголовочный файл command.h
и напишем туда следующее:
#ifndef COMMAND_H
#define COMMAND_H
#include <avr/io.h>
#include <string.h>
#define COMMAND_SIZE 5
#define COMMAND_MAX_NAME_LENGTH 20
struct Command {
void *addr;
char name[COMMAND_MAX_NAME_LENGTH];
};
uint8_t command_define(void *fp, char name[COMMAND_MAX_NAME_LENGTH]);
uint8_t command_process(char *cmd_name);
void command_list(void);
#endif /* COMMAND_H */
Для решения этой задачи я предлагаю создавать массив структур, где в каждой структуре будет содержаться указатель на функцию которую нам нужно выполнить, и название самой команды.
command_define
будет создавать новую структуру в массиве.command_process
будет вызывать соответствующую функцию по имени команды.command_list
будет отправлять по UART информацию о существующих командах.
Реализовать задуманное можно следующим образом создав файл command.c
:
#include "command.h"
#include "usart.h"
struct Command commands[COMMAND_SIZE];
uint8_t cmd_idx = 0;
uint8_t command_define(void *fp, char name[COMMAND_MAX_NAME_LENGTH]) {
size_t name_length = strlen(name);
if (name_length > COMMAND_MAX_NAME_LENGTH) {
return 0;
}
if (cmd_idx > COMMAND_SIZE) {
cmd_idx = 0;
}
struct Command command;
command.addr = fp;
strcpy(command.name, name);
commands[cmd_idx++] = command;
return 1;
}
uint8_t command_process(char *cmd_name) {
uint8_t i = 0;
do {
if (strcmp(cmd_name, commands[i].name) == 0) {
int (*execute)();
execute = commands[i].addr;
execute();
USART1_SendString("[LOG]: Command completed successfully with code \"0\".\n");
USART1_SendChar(EOT);
return 0;
}
} while (i++ < COMMAND_SIZE);
USART1_SendString("[LOG]: Invalid command name!\n");
USART1_SendChar(EOT);
return 1;
}
void command_list(void) {
USART1_SendString("[LOG]: List of available commands:\n");
for (uint8_t i = 0; i < COMMAND_SIZE; i++) {
if (commands[i].addr) {
USART1_SendChar((i + 1) + '0');
USART1_SendString(". ");
USART1_SendString(commands[i].name);
USART1_SendChar('\n');
}
}
}
Важно отметить, что после отправки данных, необходимо отослать символ EOT
, который будет говорить о том, что передача закончена, это поможет в будущем в разработке веб-приложения.
Прием данных
Прием данныхПоследнее, что необходимо сделать, это принимать команды по USART и отправлять их обработчику. Для этого можно использовать прерывание:
char buffer[BUFFER_SIZE];
volatile uint8_t buff_idx = 0;
ISR(USART1_RXC_vect) {
if (buff_idx > BUFFER_SIZE) buff_idx = 0;
buffer[buff_idx] = USART1.RXDATAL;
buff_idx++;
}
После срабатывания прерывания USART1_RXC_vect
, в регистре USART1.RXDATAL
появляются данные (один байт). Этот байт мы должны записать в свой буфер с фиксированным размером. Еще есть другой вариант принятия данных не используя прерывание. Для этого достаточно проверять бит RXCIF
(USART Receive Complete Interrupt Flag) в регистре USARTn.STATUS
. Этот бит является установленным, когда в приемном буфере присутствуют данные.
Для того того чтобы обработать входящие команды, в главном цикле можно проверять буфер на наличие символа \n
, этот символ будет означать конец названия команды:
while (1) {
if (buff_idx >= 1) {
if (buffer[buff_idx - 1] == '\n') {
buffer[buff_idx - 1] = '\0';
command_process(buffer);
buff_idx = 0;
}
}
}
После нахождения этого символа, можно передать буфер обработчику, затем сбросить индекс буфера в ноль. В итоге файл main.c
будет выглядеть следующим образом:
#include <avr/io.h>
#include <avr/interrupt.h>
#include <string.h>
#include "constants.h"
#include "command.h"
#include "usart.h"
char buffer[BUFFER_SIZE];
volatile uint8_t buff_idx = 0;
void led_toggle(void);
void hello_world(void);
ISR(USART1_RXC_vect) {
if (buff_idx > BUFFER_SIZE) buff_idx = 0;
buffer[buff_idx] = USART1.RXDATAL;
buff_idx++;
}
void MCU_Init_Ports(void) {
/* led pin PC6 to output */
PORTC.DIRSET = PIN6_bm;
}
int main(void) {
MCU_Init_Ports();
USART1_Initialize();
sei();
command_define(led_toggle, "led_toggle");
command_define(hello_world, "hello");
command_define(command_list, "list");
while (1) {
if (buff_idx >= 1) {
if (buffer[buff_idx - 1] == '\n') {
buffer[buff_idx - 1] = '\0';
command_process(buffer);
buff_idx = 0;
}
}
}
}
void led_toggle(void) {
PORTC.OUTTGL = PIN6_bm;
}
void hello_world(void) {
USART1_SendString("Hello world!\n");
}
Обязательно после инициализации портов микроконтроллера и USART нужно включить глобальные прерывания, вызвав функцию sei
С помощью функции command_define
мы можем определить, по какой переданной команде по USART запускать функцию:
led_toggle
будет переключать светодиод.hello_world
выводить сообщение “Hello world”.command_list
будет показывать список имеющихся команд.
Демонстрация работы
Демонстрация работыТеперь собрав и загрузив прошивку в ваш микроконтроллер, можно проверить работоспособность. Я буду использовать программу CuteCom
для открытия последовательного порта, вы можете использовать любую другую подобную.
Выберете ваше устройство из списка (в моем случае это /dev/ttyACM1
) и установите следующие настройки:
- Скорость: 9600 бод
- Количество битов: 8
- Четность: None
- Стоповые биты: 1
Теперь после открытия порта можно отправлять команды, не забудьте установить возврат каретки как LF
, т.к наша прошивка распознает окончание команды только по этому символу.
Разработка веб-приложения
Разработка веб-приложенияВеб-приложение будет состоять всего из нескольких элементов: поле для ввода, куда мы сможем писать наши команды, кнопки для открытия и закрытия последовательного порта, статус соединения. Готовый вариант можно посмотреть здесь.
Создание разметки HTML и стилей
Создание разметки HTML и стилейРазметка HTML будет выглядеть следующим образом:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Web Serial API Example</title>
<link href="./style.css" rel="stylesheet" />
<script defer src="./app.js"></script>
</head>
<body>
<div id="app">
<div>
<button id="connect">Connect</button>
<button id="disconnect">Disconnect</button>
</div>
<form id="terminal_form" action="#">
<label for="input">Enter command name:</label><br />
<div>
<input id="input" name="input" placeholder="e.g led_toggle" type="text" disabled />
<button name="send" type="submit" disabled>Send</button>
</div>
</form>
<textarea id="serial_log" placeholder=">" readonly></textarea>
<div id="port_info">
<div>
<p id="status">not connected</p>
</div>
<div>
<span>vendorId: <span id="vendor_id">-</span> |</span>
<span>deviceId: <span id="product_id">-</span></span>
</div>
</div>
</div>
</body>
</html>
Добавим немного стилей создав файл style.css
:
:root {
--border-color: #c9d1d9;
--border-color-hover: #6e7681;
--font-family: monospace;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
#app {
display: flex;
flex-direction: column;
width: 100%;
max-width: 600px;
row-gap: 5px;
}
html,
body {
box-sizing: border-box;
font-size: 16px;
}
body {
display: flex;
justify-content: center;
margin-top: 2rem;
font-family: var(--font-family);
}
p {
padding: 0;
margin: 5px;
}
input,
textarea {
font-family: var(--font-family);
padding: 5px 15px;
border-radius: 4px;
width: 100%;
border: 1px solid var(--border-color);
}
textarea {
resize: vertical;
min-height: 200px;
line-height: 1.4;
font-size: 16px;
outline: none;
font-size: 12px;
}
label {
font-size: 14px;
font-style: italic;
}
button {
cursor: pointer;
background: none;
font-family: var(--font-family);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 5px 15px;
}
button:hover:not(:disabled) {
border: 1px solid var(--border-color-hover);
}
#terminal_form div {
display: flex;
justify-content: space-between;
gap: 5px;
margin-bottom: 5px;
margin-top: 5px;
}
#port_info {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
font-size: 14px;
}
#port_info #status {
text-transform: uppercase;
}
Создание класса SerialPortHandler
Создание класса SerialPortHandlerЛогика работы с последовательным портом будет реализована в этом классе, который будет небольшой оберткой для удобного использования WebSerial API, он будет иметь всего 5 методов и несколько свойств.
Определим этот класс в файле app.js
:
class SerialPortHandler {
constructor(options, onConnect, onDisconnect) {
this.encoder = new TextEncoder();
this.decoder = new TextDecoder();
this.onConnect = onConnect;
this.onDisconnect = onDisconnect;
this.options = options;
this.port = null;
this.isOpened = false;
this.#setupListeners();
}
async open() {}
async close() {}
async write(data) {}
async read() {}
#setupListeners() {}
}
TextEncoder
является классом, который кодирует строку в массив без знаковых 8-ми битных целых чиселUint8Array
.TextDecoder
наоборот, декодирует в строку. Экземпляры этих классов нам понадобятся, т.к мы будет оперировать данными с типомUint8Array
.- Функции
onConnect
иonDisconnect
будут вызываться при подключении или отключении устройства. - Свойство
options
содержит необходимые параметры, которые будут использоваться для передачи и приема данных. - Свойство
port
- объект, который будет содержать методы для работы с портом и информацию об устройстве после установки соединения. - Функция
setupListeners
просто будет добавлять обработчики событий.
Далее рассматривается реализация этих методов.
Открытие последовательного порта
Открытие последовательного портаДля открытия последовательного порта для начала необходимо вызвать метод requestPort
, который откроет специальное окно со списком устройств. Также необходимо, чтобы пользователь сам активировал вызов этого окна, иначе будет ошибка. Это как раз можно сделать по событию нажатия кнопки.
async open() {
try {
const port = await navigator.serial.requestPort();
await port.open(this.options);
this.port = port;
this.isOpened = true;
return this.port.getInfo();
} catch (error) {
console.error(error);
throw error;
}
}
В функцию requestPort
в качестве параметра можно передать необязательный параметр фильтр, который ограничит список выбираемых портов в соответствии с USB идентификатором:
const port = await navigator.serial.requestPort({
filters: [{ usbVendorId: 0x7522 }],
});
Список USB идентификаторов можно посмотреть на этом сайте.
После того как пользователь выбрал устройство, метод requestPort
возвращает port
, который можно открыть передав туда параметры:
baudRate
: скорость передачи данных.dataBits
: количество бит данных во фрейме (7 или 8).stopBits
: количество стоповых битов в конце пакета (1 или 2).parity
: режим четности (none
,even
илиodd
).bufferSize
: размер буферов чтения и записи, которые должны быть созданы.flowControl
: режим управления потоком (none
илиhardware
).
Свойство baudRate
является единственным обязательным параметром, остальные же являются необязательными и имеют значения по умолчанию.
В конце наш метод возвращает информацию о подключенном устройстве используя метод getInfo
.
Чтение данных
Чтение данныхОтмечу, что WebSerial API является асинхронным, что позволяет предотвращать блокировку пользовательского интерфейса во время принятия данных.
Еще важно знать, что WebSerial API использует потоки Stream API. При потоковой передаче данные разбиваются на фрагменты
(chunks)
, это позволяет обрабатывать данные без полного ожидания их поступления.
Получение данных можно реализовать следующим образом:
async read() {
while (this.port.readable) {
const reader = this.port.readable.getReader();
let chunks = '';
try {
while (true) {
const { value, done } = await reader.read();
const decoded = this.decoder.decode(value);
chunks += decoded;
if (done || decoded.includes(EOT)) {
console.log('Reading done.');
reader.releaseLock();
break;
}
}
return chunks;
} catch (error) {
console.error(error);
throw error;
} finally {
reader.releaseLock();
}
}
}
Когда пользователь подключился к устройству, this.port
будет иметь такие свойства как writable
и readable
, которые являются экземплярами классов WritableStream и ReadableStream. Они нам понадобятся для чтения и записи данных.
Внешний цикл while
предназначен для проверки ошибок, такие как проверка четности. При возникновении фатальной ошибки, свойство readable
станет null
.
Метод getReader
создает читатель, который даст возможность получать данные фрагментами. Доступный для чтения поток одновременно имеет не более одного читателя, соответственно после этого поток является заблокированным, но читатель остается активным.
Внутренний цикл while
читает данные используя метод read
у читателя. Этот метод возвращает свойство value
, который имеет данные c типом UInt8Array
, и свойство done
, которое станет true
, если последовательное устройство больше не передает никаких данных. Далее мы декодируем данные в строку и записываем в переменную chunks
. Так же идет проверка на наличие символа EOT
(End-of-Transmission), который означает конец передачи данных. Определите этот символ в начале файла app.js
как глобальную переменную:
var EOT = "\u0004";
После конца приема данных необходимо разблокировать поток, вызвав метод releaseLock
для того чтобы сделать читателя неактивным.
Запись данных
Запись данныхДля того чтобы отправить данные в последовательное устройство, необходимо создать писателя, который предоставит возможность отправлять фрагменты в записывающий поток:
async write(data) {
const writer = this.port.writable.getWriter();
const encoded = this.encoder.encode(data);
await writer.write(encoded);
writer.releaseLock();
}
Перед отправкой нужно закодировать вашу строку в данные с типом UInt8Array
. После создания писателя записывающий поток так же является заблокированным, соответственно после записи необходимо этот поток разблокировать используя тот же метод releaseLock
.
Закрытие порта
Закрытие портаДля закрытия порта, когда коммуникация с устройством больше не требуется можно использовать метод close
:
async close() {
await this.port.close();
this.isOpened = false;
}
Порт невозможно закрыть когда записывающий или читающий поток является заблокированным.
Обработка событий
Обработка событийAPI дает возможность подписываться на события, когда подключается доверенное устройство или когда оно отключается:
#setupListeners() {
navigator.serial.addEventListener('connect', this.onConnect);
navigator.serial.addEventListener('disconnect', this.onDisconnect);
}
Использование созданного класса
Использование созданного классаТеперь можно написать front-end часть веб-приложения используя наш созданный класс SerialPortHandler
:
class Application {
constructor(root) {
if (!("serial" in navigator)) {
console.error("Web Serial API is not supported in your browser.");
return;
}
this.serialPortHandler = new SerialPortHandler(
{ baudRate: 9600 },
() => console.log("Device connected."),
() => {
console.log("Device disconnected.");
this.#disconnectHandler();
}
);
/**
* DOM Elements
*/
this.$root = root;
this.$connectButton = this.$root.querySelector("#connect");
this.$disconnectButton = this.$root.querySelector("#disconnect");
this.$terminalForm = this.$root.querySelector("#terminal_form");
this.$serialLog = this.$root.querySelector("#serial_log");
this.$status = this.$root.querySelector("#status");
this.$vendorId = this.$root.querySelector("#vendor_id");
this.$productId = this.$root.querySelector("#product_id");
this.#setupEvents();
}
/**
* Handlers for connecting, disconnecting and sending a command
*/
#setupEvents() {
this.$connectButton.addEventListener("click", this.#connectHandler.bind(this));
this.$disconnectButton.addEventListener("click", this.#disconnectHandler.bind(this));
this.$terminalForm.addEventListener("submit", this.#submitHandler.bind(this));
}
/**
* Open serial port and notify user of connection status
* @returns {Promise<void>}
*/
async #connectHandler() {
try {
if (this.serialPortHandler.isOpened) return;
const info = await this.serialPortHandler.open();
console.log("Port opened: ", info);
this.$terminalForm.elements.input.removeAttribute("disabled");
this.$terminalForm.elements.send.removeAttribute("disabled");
this.$vendorId.textContent = "0x" + info.usbVendorId.toString(16);
this.$productId.textContent = "0x" + info.usbProductId.toString(16);
this.$status.textContent = "CONNECTED";
} catch (error) {
this.$status.textContent = "ERROR";
}
}
/**
* Closes the serial port and updates the connection status.
* @returns {Promise<void>}
*/
async #disconnectHandler() {
if (!this.serialPortHandler.isOpened) return;
await this.serialPortHandler.close();
this.$terminalForm.elements.input.setAttribute("disabled", "true");
this.$terminalForm.elements.send.setAttribute("disabled", "true");
this.$vendorId.textContent = "-";
this.$productId.textContent = "-";
this.$status.textContent = "NOT CONNECTED";
}
/**
* Writes data to the serial port and reads the response
* @param {SubmitEvent} e - Form submit event
*/
async #submitHandler(e) {
e.preventDefault();
const $form = e.target;
const data = $form.elements.input.value;
$form.reset();
if (this.serialPortHandler.isOpened && data) {
this.$serialLog.innerHTML += ">" + data + "\n";
await this.serialPortHandler.write(data + "\n");
const message = await this.serialPortHandler.read();
this.$serialLog.textContent += message.replaceAll(EOT, "");
console.log("Message received: \n" + message);
}
this.$serialLog.scrollTo(0, this.$serialLog.scrollHeight);
}
}
В основном здесь происходит работа с обновлением интерфейса и я думаю не стоит подробно разъяснять логику, отмечу только ключевые моменты:
В конструкторе этого класса можно определить, поддерживает ли браузер WebSerial API, проверив ключ serial
в объекте navigator
и если нет, то пишем ошибку в консоль. Объект this.serialPortHandler
будет являться экземпляром нашего созданного класса SerialPortHandler
, в качестве аргументов передаем скорость 9600 и функции, которые будут являться обработчиками событий подключения и отключения устройства. Далее ищем необходимые DOM элементы и добавляем на некоторые из них прослушиватель событий.
Метод #submitHandler
предназначен для отправки команд и принятия результата выполнения команды. При использовании метода write
обязательно нужно в конце добавить символ \n
, так программа в микроконтроллере узнает, где заканчивается название команды.
Инициализируйте приложение в конце файла app.js
:
new Application(document.getElementById("app"));
Проверка работы веб-приложения
Проверка работы веб-приложенияТеперь можно отсылать команды микроконтроллеру:
Ради забавы можно помигать светодиодом:
const app = new Application(document.getElementById("app"));
setInterval(async () => {
if (app.serialPortHandler.isOpened) {
await app.serialPortHandler.write("led_toggle" + "\n");
const message = await app.serialPortHandler.read();
console.log(message);
}
}, 500);
Заключение
ЗаключениеРазработав веб-приложение мы убедились, что использование WebSerial API довольно простое. Надеюсь, что эта статья пригодится вам и поможет в разработке вашего проекта.