Web Serial API позволяет веб-сайтам взаимодействовать с последовательными устройствами.
Что такое API веб-сериалов?
Последовательный порт — это двунаправленный интерфейс связи, который позволяет отправлять и получать данные побайтно.
Web Serial API предоставляет веб-сайтам возможность чтения и записи на последовательное устройство с помощью JavaScript. Последовательные устройства подключаются либо через последовательный порт в системе пользователя, либо через съемные устройства USB и Bluetooth, которые имитируют последовательный порт.
Другими словами, Web Serial API соединяет Интернет и физический мир, позволяя веб-сайтам взаимодействовать с последовательными устройствами, такими как микроконтроллеры и 3D-принтеры.
Этот API также является отличным дополнением к WebUSB, поскольку операционные системы требуют, чтобы приложения взаимодействовали с некоторыми последовательными портами, используя последовательный API более высокого уровня, а не низкоуровневый USB API.
Рекомендуемые варианты использования
В образовательном, любительском и промышленном секторах пользователи подключают периферийные устройства к своим компьютерам. Эти устройства часто управляются микроконтроллерами через последовательное соединение, используемое специальным программным обеспечением. Некоторое специальное программное обеспечение для управления этими устройствами создано с использованием веб-технологий:
В некоторых случаях веб-сайты взаимодействуют с устройством через приложение-агент, которое пользователи устанавливают вручную. В других случаях приложение поставляется в виде упакованного приложения через такую структуру, как Electron. А в других от пользователя требуется выполнить дополнительный шаг, например, копирование скомпилированного приложения на устройство через флешку.
Во всех этих случаях взаимодействие с пользователем будет улучшено за счет обеспечения прямой связи между веб-сайтом и устройством, которым он управляет.
Текущий статус
Шаг | Статус |
---|---|
1. Создайте объяснитель | Полный |
2. Создайте первоначальный проект спецификации. | Полный |
3. Соберите отзывы и доработайте дизайн | Полный |
4. Пробная версия происхождения | Полный |
5. Запуск | Полный |
Использование API веб-сериала
Обнаружение функций
Чтобы проверить, поддерживается ли Web Serial API, используйте:
if ("serial" in navigator) {
// The Web Serial API is supported.
}
Откройте последовательный порт
Web Serial API по своей конструкции является асинхронным. Это предотвращает блокировку пользовательского интерфейса веб-сайта при ожидании ввода, что важно, поскольку последовательные данные могут быть получены в любое время, что требует способа их прослушивания.
Чтобы открыть последовательный порт, сначала обратитесь к объекту SerialPort
. Для этого вы можете либо предложить пользователю выбрать один последовательный порт, вызвав navigator.serial.requestPort()
в ответ на жест пользователя, например прикосновение или щелчок мыши, либо выбрать один из navigator.serial.getPorts()
, который возвращает список последовательных портов, к которым веб-сайту предоставлен доступ.
document.querySelector('button').addEventListener('click', async () => {
// Prompt user to select any serial port.
const port = await navigator.serial.requestPort();
});
// Get all serial ports the user has previously granted the website access to.
const ports = await navigator.serial.getPorts();
Функция navigator.serial.requestPort()
принимает необязательный литерал объекта, определяющий фильтры. Они используются для сопоставления любого последовательного устройства, подключенного через USB, с обязательным идентификатором поставщика USB ( usbVendorId
) и дополнительными идентификаторами продукта USB ( usbProductId
).
// Filter on devices with the Arduino Uno USB Vendor/Product IDs.
const filters = [
{ usbVendorId: 0x2341, usbProductId: 0x0043 },
{ usbVendorId: 0x2341, usbProductId: 0x0001 }
];
// Prompt user to select an Arduino Uno device.
const port = await navigator.serial.requestPort({ filters });
const { usbProductId, usbVendorId } = port.getInfo();
Вызов requestPort()
предлагает пользователю выбрать устройство и возвращает объект SerialPort
. Если у вас есть объект SerialPort
, вызов port.open()
с желаемой скоростью передачи данных откроет последовательный порт. Член словаря baudRate
определяет, насколько быстро данные передаются по последовательной линии. Выражается в битах в секунду (бит/с). Проверьте документацию вашего устройства на предмет правильного значения, так как все данные, которые вы отправляете и получаете, будут бессмысленными, если оно указано неправильно. Для некоторых устройств USB и Bluetooth, которые эмулируют последовательный порт, этому значению можно безопасно установить любое значение, поскольку оно игнорируется эмуляцией.
// Prompt user to select any serial port.
const port = await navigator.serial.requestPort();
// Wait for the serial port to open.
await port.open({ baudRate: 9600 });
Вы также можете указать любой из приведенных ниже параметров при открытии последовательного порта. Эти параметры являются необязательными и имеют удобные значения по умолчанию .
-
dataBits
: количество бит данных в кадре (7 или 8). -
stopBits
: количество стоповых битов в конце кадра (1 или 2). -
parity
: режим четности (либо"none"
,"even"
или"odd"
). -
bufferSize
: размер буферов чтения и записи, которые должны быть созданы (должен быть меньше 16 МБ). -
flowControl
: режим управления потоком (либо"none"
либо"hardware"
).
Чтение из последовательного порта
Потоки ввода и вывода в Web Serial API обрабатываются API Streams.
После установки соединения через последовательный порт свойства, readable
и writable
из объекта SerialPort
возвращают ReadableStream и WritableStream . Они будут использоваться для получения данных и отправки данных на последовательное устройство. Оба используют экземпляры Uint8Array
для передачи данных.
Когда новые данные поступают от последовательного устройства, port.readable.getReader().read()
асинхронно возвращает два свойства: value
и логическое значение done
. Если done
истинно, последовательный порт закрыт или данные больше не поступают. Вызов port.readable.getReader()
создает средство чтения и блокирует readable
для него. Пока readable
заблокировано , последовательный порт не может быть закрыт.
const reader = port.readable.getReader();
// Listen to data coming from the serial device.
while (true) {
const { value, done } = await reader.read();
if (done) {
// Allow the serial port to be closed later.
reader.releaseLock();
break;
}
// value is a Uint8Array.
console.log(value);
}
Некоторые нефатальные ошибки чтения последовательного порта могут возникнуть при некоторых условиях, таких как переполнение буфера, ошибки кадрирования или ошибки четности. Они выбрасываются как исключения и могут быть перехвачены, добавив еще один цикл поверх предыдущего, который проверяет port.readable
. Это работает, поскольку, пока ошибки не являются фатальными, новый ReadableStream создается автоматически. Если возникает фатальная ошибка, например удаление последовательного устройства, значение port.readable
становится нулевым.
while (port.readable) {
const reader = port.readable.getReader();
try {
while (true) {
const { value, done } = await reader.read();
if (done) {
// Allow the serial port to be closed later.
reader.releaseLock();
break;
}
if (value) {
console.log(value);
}
}
} catch (error) {
// TODO: Handle non-fatal read error.
}
}
Если последовательное устройство отправляет текст обратно, вы можете передать port.readable
через TextDecoderStream
, как показано ниже. TextDecoderStream
— это поток преобразования , который захватывает все фрагменты Uint8Array
и преобразует их в строки.
const textDecoder = new TextDecoderStream();
const readableStreamClosed = port.readable.pipeTo(textDecoder.writable);
const reader = textDecoder.readable.getReader();
// Listen to data coming from the serial device.
while (true) {
const { value, done } = await reader.read();
if (done) {
// Allow the serial port to be closed later.
reader.releaseLock();
break;
}
// value is a string.
console.log(value);
}
Вы можете контролировать распределение памяти при чтении из потока, используя программу чтения «Принесите свой собственный буфер». Вызовите port.readable.getReader({ mode: "byob" })
чтобы получить интерфейс ReadableStreamBYOBReader и предоставить свой собственный ArrayBuffer
при вызове read()
. Обратите внимание, что API Web Serial поддерживает эту функцию в Chrome 106 или более поздней версии.
try {
const reader = port.readable.getReader({ mode: "byob" });
// Call reader.read() to read data into a buffer...
} catch (error) {
if (error instanceof TypeError) {
// BYOB readers are not supported.
// Fallback to port.readable.getReader()...
}
}
Вот пример повторного использования буфера из value.buffer
:
const bufferSize = 1024; // 1kB
let buffer = new ArrayBuffer(bufferSize);
// Set `bufferSize` on open() to at least the size of the buffer.
await port.open({ baudRate: 9600, bufferSize });
const reader = port.readable.getReader({ mode: "byob" });
while (true) {
const { value, done } = await reader.read(new Uint8Array(buffer));
if (done) {
break;
}
buffer = value.buffer;
// Handle `value`.
}
Вот еще один пример того, как прочитать определенный объем данных из последовательного порта:
async function readInto(reader, buffer) {
let offset = 0;
while (offset < buffer.byteLength) {
const { value, done } = await reader.read(
new Uint8Array(buffer, offset)
);
if (done) {
break;
}
buffer = value.buffer;
offset += value.byteLength;
}
return buffer;
}
const reader = port.readable.getReader({ mode: "byob" });
let buffer = new ArrayBuffer(512);
// Read the first 512 bytes.
buffer = await readInto(reader, buffer);
// Then read the next 512 bytes.
buffer = await readInto(reader, buffer);
Запись в последовательный порт
Чтобы отправить данные на последовательное устройство, передайте данные в port.writable.getWriter().write()
. Вызов releaseLock()
в port.writable.getWriter()
необходим для последующего закрытия последовательного порта.
const writer = port.writable.getWriter();
const data = new Uint8Array([104, 101, 108, 108, 111]); // hello
await writer.write(data);
// Allow the serial port to be closed later.
writer.releaseLock();
Отправьте текст на устройство через TextEncoderStream
, переданный в port.writable
, как показано ниже.
const textEncoder = new TextEncoderStream();
const writableStreamClosed = textEncoder.readable.pipeTo(port.writable);
const writer = textEncoder.writable.getWriter();
await writer.write("hello");
Закрыть последовательный порт
port.close()
закрывает последовательный порт, если его readable
и writable
члены разблокированы , что означает, что для соответствующих устройств чтения и записи была вызвана releaseLock()
.
await port.close();
Однако при непрерывном чтении данных с последовательного устройства с использованием цикла port.readable
всегда будет заблокирован до тех пор, пока не возникнет ошибка. В этом случае вызов reader.cancel()
приведет к немедленному разрешению reader.read()
с помощью { value: undefined, done: true }
и, следовательно, позволит циклу вызвать reader.releaseLock()
.
// Without transform streams.
let keepReading = true;
let reader;
async function readUntilClosed() {
while (port.readable && keepReading) {
reader = port.readable.getReader();
try {
while (true) {
const { value, done } = await reader.read();
if (done) {
// reader.cancel() has been called.
break;
}
// value is a Uint8Array.
console.log(value);
}
} catch (error) {
// Handle error...
} finally {
// Allow the serial port to be closed later.
reader.releaseLock();
}
}
await port.close();
}
const closedPromise = readUntilClosed();
document.querySelector('button').addEventListener('click', async () => {
// User clicked a button to close the serial port.
keepReading = false;
// Force reader.read() to resolve immediately and subsequently
// call reader.releaseLock() in the loop example above.
reader.cancel();
await closedPromise;
});
Закрытие последовательного порта сложнее при использовании потоков преобразования . Вызовите reader.cancel()
как и раньше. Затем вызовите writer.close()
и port.close()
. Это распространяет ошибки через потоки преобразования на базовый последовательный порт. Поскольку распространение ошибок не происходит сразу, вам необходимо использовать обещания readableStreamClosed
и writableStreamClosed
, созданные ранее, чтобы определить, когда port.readable
и port.writable
были разблокированы. Отмена reader
приводит к прерыванию потока; вот почему вы должны поймать и проигнорировать возникающую ошибку.
// With transform streams.
const textDecoder = new TextDecoderStream();
const readableStreamClosed = port.readable.pipeTo(textDecoder.writable);
const reader = textDecoder.readable.getReader();
// Listen to data coming from the serial device.
while (true) {
const { value, done } = await reader.read();
if (done) {
reader.releaseLock();
break;
}
// value is a string.
console.log(value);
}
const textEncoder = new TextEncoderStream();
const writableStreamClosed = textEncoder.readable.pipeTo(port.writable);
reader.cancel();
await readableStreamClosed.catch(() => { /* Ignore the error */ });
writer.close();
await writableStreamClosed;
await port.close();
Слушайте соединение и отключение
Если последовательный порт предоставляется устройством USB, то это устройство можно подключить или отключить от системы. Когда веб-сайту предоставлено разрешение на доступ к последовательному порту, он должен отслеживать события connect
и disconnect
.
navigator.serial.addEventListener("connect", (event) => {
// TODO: Automatically open event.target or warn user a port is available.
});
navigator.serial.addEventListener("disconnect", (event) => {
// TODO: Remove |event.target| from the UI.
// If the serial port was opened, a stream error would be observed as well.
});
Обработка сигналов
После установления соединения через последовательный порт вы можете явно запрашивать и устанавливать сигналы, предоставляемые последовательным портом, для обнаружения устройств и управления потоком данных. Эти сигналы определяются как логические значения. Например, некоторые устройства, такие как Arduino, перейдут в режим программирования, если переключится сигнал готовности терминала данных (DTR).
Установка выходных сигналов и получение входных сигналов соответственно выполняются путем вызова port.setSignals()
и port.getSignals()
. См. примеры использования ниже.
// Turn off Serial Break signal.
await port.setSignals({ break: false });
// Turn on Data Terminal Ready (DTR) signal.
await port.setSignals({ dataTerminalReady: true });
// Turn off Request To Send (RTS) signal.
await port.setSignals({ requestToSend: false });
const signals = await port.getSignals();
console.log(`Clear To Send: ${signals.clearToSend}`);
console.log(`Data Carrier Detect: ${signals.dataCarrierDetect}`);
console.log(`Data Set Ready: ${signals.dataSetReady}`);
console.log(`Ring Indicator: ${signals.ringIndicator}`);
Преобразование потоков
Когда вы получаете данные от последовательного устройства, вы не обязательно получите все данные сразу. Он может быть произвольно разбит на части. Дополнительные сведения см. в разделе Концепции Streams API .
Чтобы справиться с этим, вы можете использовать некоторые встроенные потоки преобразования, такие как TextDecoderStream
, или создать свой собственный поток преобразования, который позволит вам анализировать входящий поток и возвращать проанализированные данные. Поток преобразования находится между последовательным устройством и циклом чтения, который потребляет поток. Он может применить произвольное преобразование перед использованием данных. Думайте об этом как о сборочном конвейере: по мере того, как виджет спускается по конвейеру, каждый шаг в этой линии изменяет виджет, так что к тому времени, когда он доберется до конечного пункта назначения, он становится полностью функционирующим виджетом.
Например, рассмотрим, как создать класс потока преобразования, который принимает поток и разбивает его на фрагменты на основе разрывов строк. Его метод transform()
вызывается каждый раз, когда поток получает новые данные. Он может либо поставить данные в очередь, либо сохранить их на будущее. Методlush flush()
вызывается, когда поток закрывается, и обрабатывает любые данные, которые еще не были обработаны.
Чтобы использовать класс потока преобразования, вам необходимо передать через него входящий поток. В третьем примере кода в разделе «Чтение из последовательного порта» исходный входной поток передавался только через TextDecoderStream
, поэтому нам нужно вызвать pipeThrough()
, чтобы передать его через наш новый LineBreakTransformer
.
class LineBreakTransformer {
constructor() {
// A container for holding stream data until a new line.
this.chunks = "";
}
transform(chunk, controller) {
// Append new chunks to existing chunks.
this.chunks += chunk;
// For each line breaks in chunks, send the parsed lines out.
const lines = this.chunks.split("\r\n");
this.chunks = lines.pop();
lines.forEach((line) => controller.enqueue(line));
}
flush(controller) {
// When the stream is closed, flush any remaining chunks out.
controller.enqueue(this.chunks);
}
}
const textDecoder = new TextDecoderStream();
const readableStreamClosed = port.readable.pipeTo(textDecoder.writable);
const reader = textDecoder.readable
.pipeThrough(new TransformStream(new LineBreakTransformer()))
.getReader();
Для отладки проблем связи с последовательным устройством используйте метод tee()
в port.readable
, чтобы разделить потоки, идущие к последовательному устройству или от него. Два созданных потока могут использоваться независимо, и это позволяет вам распечатать один из них на консоль для проверки.
const [appReadable, devReadable] = port.readable.tee();
// You may want to update UI with incoming data from appReadable
// and log incoming data in JS console for inspection from devReadable.
Отменить доступ к последовательному порту
Веб-сайт может очистить разрешения на доступ к последовательному порту, в сохранении которого он больше не заинтересован, вызвав функцию forget()
в экземпляре SerialPort
. Например, для образовательного веб-приложения, используемого на общем компьютере со многими устройствами, большое количество накопленных разрешений, созданных пользователями, ухудшает взаимодействие с пользователем.
// Voluntarily revoke access to this serial port.
await port.forget();
Поскольку forget()
доступна в Chrome 103 и более поздних версиях, проверьте, поддерживается ли эта функция, с помощью следующего:
if ("serial" in navigator && "forget" in SerialPort.prototype) {
// forget() is supported.
}
Советы разработчикам
Отладку Web Serial API в Chrome легко выполнить с помощью внутренней страницы about://device-log
, где вы можете увидеть все события, связанные с последовательными устройствами, в одном месте.
Кодлаб
В лаборатории кода Google Developer вы будете использовать Web Serial API для взаимодействия с платой BBC micro:bit для отображения изображений на ее светодиодной матрице 5x5.
Поддержка браузера
API Web Serial доступен на всех настольных платформах (ChromeOS, Linux, macOS и Windows) в Chrome 89.
Полифилл
В Android поддержка последовательных портов на базе USB возможна с помощью WebUSB API и полифила Serial API . Этот полифилл ограничен аппаратным обеспечением и платформами, на которых устройство доступно через API WebUSB, поскольку оно не заявлено встроенным драйвером устройства.
Безопасность и конфиденциальность
Авторы спецификации разработали и реализовали API Web Serial, используя основные принципы, определенные в разделе «Управление доступом к мощным функциям веб-платформы» , включая пользовательский контроль, прозрачность и эргономику. Возможность использования этого API в первую очередь ограничивается моделью разрешений, которая предоставляет доступ только к одному последовательному устройству одновременно. В ответ на запрос пользователя пользователь должен предпринять активные действия для выбора конкретного последовательного устройства.
Чтобы понять компромиссы в области безопасности, ознакомьтесь с разделами безопасности и конфиденциальности в объяснении веб-последовательного API.
Обратная связь
Команда Chrome будет рада услышать ваши мысли и опыт использования Web Serial API.
Расскажите нам о дизайне API
Что-то в API работает не так, как ожидалось? Или вам не хватает методов или свойств, необходимых для реализации вашей идеи?
Сообщите о проблеме спецификации в репозитории Web Serial API GitHub или добавьте свои мысли к существующей проблеме.
Сообщить о проблеме с реализацией
Вы нашли ошибку в реализации Chrome? Или реализация отличается от спецификации?
Сообщите об ошибке на https://new.crbug.com . Обязательно укажите как можно больше подробностей, предоставьте простые инструкции по воспроизведению ошибки и установите для параметра «Компоненты» значение Blink>Serial
. Glitch отлично подходит для быстрого и простого обмена репродукциями.
Показать поддержку
Планируете ли вы использовать Web Serial API? Ваша публичная поддержка помогает команде Chrome расставлять приоритеты в функциях и показывает другим поставщикам браузеров, насколько важно их поддерживать.
Отправьте твит @ChromiumDev, используя хэштег #SerialAPI
, и сообщите нам, где и как вы его используете.
Полезные ссылки
- Спецификация
- Ошибка отслеживания
- Запись ChromeStatus.com
- Компонент Blink:
Blink>Serial
Демо
Благодарности
Спасибо Рейли Гранту и Джо Медли за рецензии на эту статью. Фотография завода по производству самолетов, сделанная Birmingham Museums Trust на Unsplash .