Версия 89 браузеров Chrome и Edge выпустила неотмеченный Web Serial API, что означает, что теперь он доступен для общего использования, а не заблокирован экспериментальными флагами (если вы используете более раннюю версию, вы можете включить Experimental API). Функции веб-платформы в chrome://flags)

API обеспечивает связь между браузером и поддерживаемым последовательным оборудованием, таким как Arduino или RaspberryPi, через USB Serial.

Если у вас нет оборудования для подключения, вы можете использовать Bluetooth Serial — при условии, что на вашем компьютере есть модуль Bluetooth. Подключите к нему свое мобильное устройство и используйте соответствующее программное обеспечение. Для Android есть Serial Bluetooth Terminal и для iOS BLE to Serial Terminal.

Подключение к последовательному устройству

Чтобы запросить доступ к устройству, необходимо выполнить вызов navigator.serial.requestPort. Этот вызов должен быть выполнен после жеста пользователя, такого как нажатие кнопки. Вы не можете просто вызвать requestPort из своего кода без какой-либо взаимодействие с пользователем, так как это приведет к нарушению безопасности. Вы также должны вызывать его из местоположения, в котором не настроена политика s, чтобы отключить это (вы можете увидеть это в демо выше — если вы попытаетесь запустить его в редакторе, это не сработает из-за того, что <iframe> не имеет правильного политика).

Вам также может понадобиться установить в вашем проекте типы w3c-web-serial, чтобы убедиться, что у вас есть доступные типы для объекта navigator и глобальные типы, такие как SerialPort.

Чтобы получить порт, вызовите navigator.serial.requestPort внутри обработчика — он вернет обещание, содержащее объект порта — вы также можете обернуть его в try/catch для обработки, когда пользователь отменяет выбор устройства.

const startButton = document.getElementById("start");
startButton.addEventListener("click", async event => {
  try {
    const port = await navigator.serial.requestPort();
    // We can now access the serial device by opening it
    // e.g. await port.open({baudRate: 9600})
  } catch (e) {
    // The prompt has been dismissed without selecting a device.
  }
});

Однажды созданный объект порта должен быть вызван с помощью метода open — единственным обязательным свойством параметров является baudRate, которое представляет собой максимальную скорость передачи бит в секунду, но есть и другие параметры, основанные на требованиях устройства.

После открытия порт может возвращать ReadableStream и WritableStream, что позволяет передавать данные на устройство и с него.

Наш оператор RxJS

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

Чтение из последовательной шины

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

Здесь мы настроим итерируемый ридер для нашего потока — пока результат не done и порт все еще доступен для чтения, мы продолжим читать исходник и выдавать его подписчику Observable. Если считыватель завершил работу или порт был закрыт, мы закончим эту итерацию.

await port.open({baudRate: 9600});
const process = async (
  result: ReadableStreamReadResult<Uint8Array>
): Promise<ReadableStreamReadResult<Uint8Array>> => {
  subscriber.next(result.value);
  return !result.done || !port.readable
    ? reader.read().then(process)
    : Promise.resolve(result);
};
if (port.readable) {
  reader = port.readable.getReader();
  reader.read().then(process);
}

Поскольку вывод нашего Observable — это Uint8Array. В зависимости от ваших потребностей вы можете декодировать это в нужный вам формат, но в большинстве случаев это будет текстовое содержимое — здесь мы можем использовать TextDecoder, чтобы получить значение:

const decoder = new TextDecoder("utf-8");
fromWebSerial(port).pipe(
  tap(value => {
    // Value is a UInt8Array, we can append to a element by decoding it
    outputEl.innerHTML = decoder.decode(value)
  })
).subscribe();

Запись в последовательную шину

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

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

Читатель из нашего энкодера будет перенаправлен на порты WritableStream, а писатель передан в toWritableStream, который соединяет Observable с писателем:

if (writerSource && port.writable) {
  const encoder = new TextEncoderStream();
  writerEnd = encoder.readable.pipeTo(port.writable);
  const outputStream = encoder.writable;
  writer = outputStream.getWriter();
  writerSource.pipe(toWritableStream(writer, signal)).subscribe();
}

Теперь мы можем передать Observable и использовать его для генерации наших значений:

const emitter$ = new Subject<string>();
fromWebSerial(port, emitter$.asObservable()).subscribe();
emitter$.next('Hello There!');

Создание приложения для последовательного чата

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

Для этого урока я создаю очень простое приложение для чата — с помощью упомянутых выше приложений Bluetooth Serial вы можете использовать его для отправки и получения текстовых данных между устройствами.

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

Надеюсь, вы нашли этот урок полезным, и если вы что-то создадите с его помощью, я буду рад услышать об этом!

Коллекция готовых операторов и Observables для ваших проектов

RxJS Ninja — это набор из более чем 130 операторов для работы с различными типами данных (такими как массивы, числа) и потоки, позволяющий модифицировать, фильтровать и запрашивать данные.

Все еще находящиеся в активной разработке, вы можете найти полезные операторы, которые обеспечивают более четкое намерение для вашего кода RxJS.

Вы можете проверить исходный код на GitHub.