Старые модемы, роутеры, сетевое оборудование. Последовательные и телефонные разъемы, аудиоразъемы, Ethernet-разъемы.

Действия по чтению и записи последовательного порта

Web Serial API позволяет веб-сайтам обмениваться данными с устройствами с последовательным интерфейсом.

Published on Updated on

Translated to: English, 日本語

Success

Web Serial API — это часть проекта возможностей, запущенного в Chrome 89.

Что такое Web Serial API?

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

Web Serial API предоставляет веб-сайтам способ осуществлять действия по чтению/записи последовательного устройства с помощью JavaScript. Последовательные устройства подключаются либо через последовательный порт в системе пользователя, либо через съемные устройства USB и Bluetooth, которые имитируют последовательный порт.

Другими словами, Web Serial API соединяет сеть и физический мир, позволяя веб-сайтам обмениваться данными с устройствами с последовательным интерфейсом, такими как микроконтроллеры и 3D-принтеры.

Этот API также является отличным дополнением к WebUSB, поскольку операционные системы требуют, чтобы приложения взаимодействовали с некоторыми последовательными портами, используя свой высокоуровневый последовательный API, а не низкоуровневый USB API.

Предлагаемые варианты использования

В образовательном, любительском и промышленном секторах пользователи подключают периферийные устройства к компьютерам. Эти устройства часто управляются микроконтроллерами через последовательное соединение, используемое специальным программным обеспечением. Некоторое программное обеспечение для управления этими устройствами создано с применением веб-технологий:

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

Во всех этих случаях пользовательский опыт будет улучшен за счет обеспечения прямой связи между веб-сайтом и устройством, которым он управляет.

Текущий статус

ЭтапСтатус
1. Составление объяснительного материала.Завершен
2. Создание черновика спецификации.Завершен
3. Сбор отзывов и доработка проекта.Завершен
4. Испытания по схеме Origin Trial.Завершен
5. Запуск.Завершен

Использование Web Serial 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, с обязательным идентификатором поставщика (usbVendorId) и необязательным идентификатором продукта (usbProductId) для USB-устройства.

// 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();
Скриншот веб-формы с выбором последовательного порта
Указание пользователю выбрать BBC micro:bit

Вызов requestPort() предлагает пользователю выбрать устройство и возвращает объект SerialPort Если у вас есть SerialPort , вызов port.open() с желаемой скоростью передачи откроет последовательный порт. baudRate словаря 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 обрабатываются Streams API.

Если потоки для вас впервые, ознакомьтесь с концепциями Streams API . В этой статье даже поверхностно рассматриваются потоки и их обработка.

После установления соединения через последовательный порт свойства writable readable и записи из объекта SerialPort возвращают ReadableStream и WritableStream . Они будут использоваться для приема и отправки данных на последовательное устройство. Оба используют Uint8Array для передачи данных.

Когда новые данные поступают от последовательного устройства, port.readable.getReader().read() асинхронно возвращает два свойства: value и логическое done Если done имеет значение true, последовательный порт был закрыт или данные больше не поступают. Вызов 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 добавлением другого цикла поверх предыдущего, который проверяет 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.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;
});

При использовании потоков преобразования (таких как TextDecoderStream и TextEncoderStream ) закрыть последовательный порт сложнее. Вызовите 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.
});

До Chrome 89 события connect и disconnect SerialConnectionEvent настраиваемый объект SerialConnectionEvent с затронутым SerialPort доступным в качестве атрибута port Вы можете использовать event.port || event.target для обработки перехода.

Обработка сигналов

После установления соединения через последовательный порт вы можете явно запросить и установить сигналы, предоставляемые последовательным портом, для обнаружения устройства и управления потоком. Эти сигналы определены как логические значения. Например, некоторые устройства, такие как 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() вызывается каждый раз, когда поток получает новые данные. Он может либо поставить данные в очередь, либо сохранить их на потом. Метод 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.

Советы разработчиков

Отладка Web Serial API в Chrome проста с помощью внутренней страницы about://device-log где вы можете увидеть все события, связанные с последовательным устройством, в одном месте.

Снимок экрана внутренней страницы для отладки Web Serial API.
Внутренняя страница в Chrome для отладки Web Serial API.

Codelab

В кодовой лаборатории Google Developer вы будете использовать Web Serial API для взаимодействия с доской BBC micro: bit для отображения изображений на ее светодиодной матрице 5x5.

Поддержка браузера

Web Serial API доступен на всех настольных платформах (ChromeOS, Linux, macOS и Windows) в Chrome 89.

Полифилл

В Android поддержка последовательных портов на базе USB возможна с помощью WebUSB API и полифилла Serial API . Этот полифил ограничен оборудованием и платформами, на которых устройство доступно через API WebUSB, поскольку оно не заявлено встроенным драйвером устройства.

Безопасность и конфиденциальность

Авторы спецификации разработали и реализовали Web Serial API, используя основные принципы, определенные в разделе « Управление доступом к мощным функциям веб-платформы» , включая пользовательский контроль, прозрачность и эргономику. Возможность использования этого API в первую очередь обеспечивается моделью разрешений, которая предоставляет доступ только одному последовательному устройству за раз. В ответ на запрос пользователя пользователь должен предпринять активные действия для выбора конкретного последовательного устройства.

Чтобы понять компромиссы безопасности, ознакомьтесь с разделами о безопасности и конфиденциальности в объяснении Web Serial API Explainer.

Отзыв

Команда Chrome хотела бы услышать о ваших мыслях и опыте работы с Web Serial API.

Расскажите о дизайне API

Есть ли в API что-то, что не работает должным образом? Или отсутствуют методы или свойства, необходимые для реализации вашей идеи?

Отправьте вопрос спецификации в репозиторий GitHub Web Serial API или добавьте свои мысли к существующей проблеме.

Сообщить о проблеме с реализацией

Вы нашли ошибку в реализации Chrome? Или реализация отличается от спецификации?

Сообщите об ошибке на https://new.crbug.com . Обязательно укажите как можно больше подробностей, предоставьте простые инструкции по воспроизведению ошибки и установите для Компонентов Blink>Serial . Glitch отлично подходит для быстрого и легкого обмена репродукциями.

Показать поддержку

Планируете ли вы использовать Web Serial API? Ваша общедоступная поддержка помогает команде Chrome определять приоритеты функций и показывает другим поставщикам браузеров, насколько важна их поддержка.

Отправьте твит на @ChromiumDev, используя хэштег #WebShare, и сообщите нам, где и как вы его используете.

Полезные ссылки

Демо

Благодарности

Спасибо Рейли Гранту и Джо Медли за рецензии на эту статью. Фотография завода самолетов Бирмингемского музейного фонда на Unsplash.

Updated on Improve article

This site uses cookies to deliver and enhance the quality of its services and to analyze traffic. If you agree, cookies are also used to serve advertising and to personalize the content and advertisements that you see. Learn more about our use of cookies.