API Web Serial позволяет веб-сайтам взаимодействовать с последовательными устройствами.
Что такое API веб-последовательностей?
Последовательный порт — это двунаправленный интерфейс связи, позволяющий отправлять и получать данные побайтно.
API Web Serial позволяет веб-сайтам считывать и записывать данные с последовательных устройств с помощью JavaScript. Последовательные устройства подключаются либо через последовательный порт в системе пользователя, либо через съёмные USB- и Bluetooth-устройства, эмулирующие последовательный порт.
Другими словами, Web Serial API соединяет Интернет и физический мир, позволяя веб-сайтам взаимодействовать с последовательными устройствами, такими как микроконтроллеры и 3D-принтеры.
Этот API также является отличным дополнением к WebUSB , поскольку операционным системам требуется, чтобы приложения взаимодействовали с некоторыми последовательными портами, используя их последовательный API более высокого уровня, а не низкоуровневый USB API.
Предлагаемые варианты использования
В образовательном, любительском и промышленном секторах пользователи подключают периферийные устройства к своим компьютерам. Эти устройства часто управляются микроконтроллерами через последовательное соединение, используемое специализированным программным обеспечением. Некоторые специализированные программы для управления этими устройствами разработаны с использованием веб-технологий:
В некоторых случаях веб-сайты взаимодействуют с устройством через приложение-агент, которое пользователи устанавливают вручную. В других случаях приложение поставляется в виде упакованного приложения, реализованного с помощью фреймворка, например Electron. В третьих же от пользователя требуется выполнить дополнительный шаг, например, скопировать скомпилированное приложение на устройство с помощью USB-накопителя.
Во всех этих случаях пользовательский опыт будет улучшен за счет обеспечения прямой связи между веб-сайтом и устройством, которым он управляет.
Текущий статус
Шаг | Статус |
---|---|
1. Создайте пояснитель | Полный |
2. Создайте первоначальный проект спецификации | Полный |
3. Собирайте отзывы и дорабатывайте дизайн | Полный |
4. Испытание происхождения | Полный |
5. Запуск | Полный |
Использование API веб-последовательностей
Обнаружение особенностей
Чтобы проверить, поддерживается ли API Web Serial, используйте:
if ("serial" in navigator) {
// The Web Serial API is supported.
}
Откройте последовательный порт
API Web Serial изначально асинхронный. Это предотвращает блокировку пользовательского интерфейса веб-сайта в ожидании ввода, что важно, поскольку последовательные данные могут быть получены в любой момент, требуя способа их прослушивания.
Чтобы открыть последовательный порт, сначала обратитесь к объекту 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"
).
Чтение из последовательного порта
Входные и выходные потоки в API Web Serial обрабатываются API потоков.
После установки соединения через последовательный порт свойства readable
и writable
объекта SerialPort
возвращают ReadableStream и WritableStream . Они будут использоваться для получения данных с последовательного устройства и отправки данных на него. Оба используют экземпляры Uint8Array
для передачи данных.
При поступлении новых данных с последовательного устройства метод port.readable.getReader().read()
асинхронно возвращает два свойства: value
и логическое значение done
. Если done
равно true, последовательный порт закрыт или данные больше не поступают. Вызов port.readable.getReader()
создаёт объект readable и блокирует для него 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);
}
Вы можете управлять распределением памяти при чтении из потока, используя считыватель «Bring Your Own Buffer». Вызовите 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}`);
Трансформирующие потоки
При получении данных с последовательного устройства вы не обязательно получите все данные сразу. Они могут быть произвольно разбиты на фрагменты. Подробнее см. в разделе «Концепции API потоков» .
Чтобы решить эту проблему, можно использовать встроенные потоки преобразования, такие как TextDecoderStream
, или создать собственный поток преобразования, который позволяет анализировать входящий поток и возвращать проанализированные данные. Поток преобразования находится между последовательным устройством и циклом чтения, который потребляет поток. Он может применить произвольное преобразование до того, как данные будут потреблены. Представьте это как сборочную линию: по мере того, как виджет спускается по линии, каждый этап линии изменяет его, так что к моменту попадания в пункт назначения это будет полностью функционирующий виджет.

Например, рассмотрим создание класса потока преобразования, который принимает поток и разбивает его на фрагменты с учётом переносов строк. Его метод transform()
вызывается каждый раз при получении новых данных потоком. Он может либо поставить данные в очередь, либо сохранить их для последующего использования. Метод 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.
}
Советы разработчикам
Отладка API Web Serial в Chrome проста благодаря внутренней странице about://device-log
, где можно увидеть все события, связанные с последовательными устройствами, в одном месте.

Codelab
В лабораторной работе Google Developer вы будете использовать API Web Serial для взаимодействия с платой BBC micro:bit и вывода изображений на ее светодиодную матрицу 5x5.
Поддержка браузеров
API Web Serial доступен на всех настольных платформах (ChromeOS, Linux, macOS и Windows) в Chrome 89.
Полифилл
В Android поддержка последовательных портов USB возможна с помощью API WebUSB и полифилла Serial API . Этот полифил ограничен оборудованием и платформами, на которых устройство доступно через API WebUSB, поскольку оно не заявлено встроенным драйвером устройства.
Безопасность и конфиденциальность
Авторы спецификации разработали и реализовали API Web Serial, используя основные принципы, изложенные в документе «Управление доступом к мощным функциям веб-платформы» , включая пользовательский контроль, прозрачность и эргономичность. Возможность использования этого API в первую очередь ограничена моделью разрешений, которая предоставляет доступ только к одному последовательному устройству одновременно. В ответ на запрос пользователя пользователь должен выполнить активные действия для выбора конкретного последовательного устройства.
Чтобы понять компромиссы в области безопасности, ознакомьтесь с разделами, посвященными безопасности и конфиденциальности , в документе Web Serial API Explainer.
Обратная связь
Команда Chrome будет рада услышать ваши мысли и опыт использования API Web Serial.
Расскажите нам о дизайне API
Есть ли что-то в API, что работает не так, как ожидалось? Или отсутствуют методы или свойства, необходимые для реализации вашей идеи?
Отправьте сообщение о проблеме со спецификацией в репозиторий Web Serial API GitHub или добавьте свои мысли к существующей проблеме.
Сообщить о проблеме с реализацией
Вы обнаружили ошибку в реализации Chrome? Или реализация отличается от спецификации?
Сообщите об ошибке по адресу https://new.crbug.com . Опишите её как можно подробнее, предоставьте простые инструкции по воспроизведению ошибки и установите для компонентов значение Blink>Serial
.
Показать поддержку
Планируете ли вы использовать Web Serial API? Ваша публичная поддержка помогает команде Chrome расставлять приоритеты в отношении функций и показывает другим разработчикам браузеров, насколько важна их поддержка.
Отправьте твит @ChromiumDev , используя хэштег #SerialAPI
, и расскажите, где и как вы его используете.
Полезные ссылки
- Спецификация
- Ошибка отслеживания
- Запись ChromeStatus.com
- Компонент Blink:
Blink>Serial
Демо-версии
Благодарности
Благодарим Рейли Гранта и Джо Медли за рецензии на эту статью. Фотография с завода по производству самолётов предоставлена Бирмингемским музейным фондом на Unsplash .