Odczyt z portu szeregowego i zapis na nim

Interfejs Web Serial API umożliwia witrynom komunikację z urządzeniami szeregowymi.

François Beaufort
François Beaufort

Co to jest interfejs Web Serial API?

Port szeregowy to dwukierunkowy interfejs komunikacyjny, który umożliwia wysyłanie i odbieranie danych w bajtach.

Interfejs Web Serial API umożliwia witrynom odczyt z urządzenia szeregowego i zapis na nich za pomocą JavaScriptu. Urządzenia szeregowe są podłączone przez port szeregowy w systemie użytkownika lub przez wymienne urządzenia USB i Bluetooth, które emulują port szeregowy.

Inaczej mówiąc, interfejs Web Serial API łączy internet z światem fizycznym, umożliwiając witrynom komunikację z urządzeniami szeregowymi, takimi jak mikrokontrolery i drukarki 3D.

Ten interfejs API jest świetnym uzupełnieniem WebUSB, ponieważ systemy operacyjne wymagają, aby aplikacje komunikowały się z niektórymi portami szeregowymi przez interfejs API wyższego poziomu, a nie niskopoziomowy interfejs USB API.

Sugerowane przypadki użycia

W branżach edukacyjnych, hobbystycznych i przemysłowych użytkownicy podłączają urządzenia peryferyjne do komputerów. Urządzenia te są często sterowane przez mikrokontrolery przez połączenie szeregowe z wykorzystaniem własnego oprogramowania. Niektóre niestandardowe programy do sterowania tymi urządzeniami są oparte na technologii internetowej:

W niektórych przypadkach strony internetowe komunikują się z urządzeniem za pomocą aplikacji agenta, którą użytkownicy zainstalowali ręcznie. W innych aplikacja jest dostarczana w postaci pakietu aplikacji za pomocą platformy Electron. W innych sytuacjach użytkownik musi wykonać dodatkową czynność, na przykład skopiować skompilowaną aplikację na urządzenie za pomocą dysku flash USB.

We wszystkich tych przypadkach zwiększy się komfort użytkowników, ponieważ zapewnimy bezpośrednią komunikację między witryną a urządzeniem, którym zarządza.

Obecny stan,

Step Stan
1. Utwórz wyjaśnienie Zakończono
2. Utwórz wstępną wersję roboczą specyfikacji Zakończono
3. Zbieranie opinii i ulepszanie projektu Zakończono
4. Testowanie origin Zakończono
5. Uruchom Zakończono

Korzystanie z interfejsu Web Serial API

Wykrywanie funkcji

Aby sprawdzić, czy interfejs Web Serial API jest obsługiwany, użyj polecenia:

if ("serial" in navigator) {
  // The Web Serial API is supported.
}

Otwórz port szeregowy

Interfejs Web Serial API jest z założenia asynchroniczny. Dzięki temu interfejs witryny nie będzie blokowany w czasie oczekiwania na dane wejściowe, co jest ważne, ponieważ dane seryjne mogą być odbierane w dowolnym momencie, wymagając ich nasłuchiwania.

Aby otworzyć port szeregowy, najpierw uzyskaj dostęp do obiektu SerialPort. W tym celu możesz poprosić użytkownika o wybranie pojedynczego portu szeregowego, wywołując navigator.serial.requestPort() w odpowiedzi na gest użytkownika, np. dotknięcie lub kliknięcie myszą, albo wybrać z protokołu navigator.serial.getPorts() listę portów szeregowych, do których witryna ma dostęp.

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();

Funkcja navigator.serial.requestPort() przyjmuje opcjonalny literał obiektu, który określa filtry. Służą one do dopasowywania wszystkich urządzeń szeregowych połączonych przez USB z obowiązkowym dostawcą USB (usbVendorId) i opcjonalnymi identyfikatorami produktów 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();
Zrzut ekranu przedstawiający komunikat o przejściu na port szeregowy na stronie internetowej
Prośba użytkownika o wybranie opcji BBC micro:bit

Wywołanie requestPort() prosi użytkownika o wybranie urządzenia i zwraca obiekt SerialPort. Gdy masz obiekt SerialPort, wywołanie metody port.open() z odpowiednią szybkością transmisji spowoduje otwarcie portu szeregowego. Element słownika baudRate określa szybkość wysyłania danych przez linię szeregową. Jest wyrażona w jednostkach bitów na sekundę (b/s). Sprawdź w dokumentacji urządzenia, czy wartość jest prawidłowa, ponieważ wszystkie wysyłane i odbierane dane będą bezużyteczne, jeśli zostaną nieprawidłowo określone. W przypadku niektórych urządzeń USB i Bluetooth, które emulują port szeregowy, ta wartość może być bezpiecznie ustawiona na dowolną wartość, ponieważ jest ignorowana przez emulację.

// 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 });

Podczas otwierania portu szeregowego możesz też wybrać dowolną z poniższych opcji. Opcje te są opcjonalne i mają wygodne wartości domyślne.

  • dataBits: liczba bitów danych na klatkę (7 lub 8).
  • stopBits: liczba bitów zatrzymania na końcu klatki (1 lub 2).
  • parity: tryb parzystości ("none", "even" lub "odd").
  • bufferSize: rozmiar buforów do odczytu i zapisu, które należy utworzyć (musi być mniejszy niż 16 MB).
  • flowControl: tryb sterowania przepływem ("none" lub "hardware").

Odczyt z portu szeregowego

Strumienie wejściowe i wyjściowe w interfejsie Web Serial API są obsługiwane przez Streams API.

Po nawiązaniu połączenia z portem szeregowym właściwości readable i writable obiektu SerialPort zwracają wartości ReadableStream i WritableStream. Będą one służyć do odbierania danych z urządzenia szeregowego i do niego. Do przenoszenia danych obie używają instancji Uint8Array.

Gdy nowe dane odbierają z urządzenia szeregowego, funkcja port.readable.getReader().read() zwraca asynchronicznie 2 właściwości: value i wartość logiczną done. Jeśli done ma wartość prawda, port szeregowy został zamknięty lub nie docierają już żadne dane. Wywołanie port.readable.getReader() tworzy czytnik i blokuje go readable. Gdy port readable jest zablokowany, nie można go zamknąć.

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);
}

Niektóre niekrytyczne błędy odczytu portów szeregowych mogą wystąpić w niektórych sytuacjach, np. przepełnienie bufora, błędy w ramkach lub błędy parzystości. Są one zgłaszane jako wyjątki i można je przechwycić, dodając kolejną pętlę nad poprzednią, która sprawdza właściwość port.readable. Jest to możliwe, ponieważ o ile błędy nie są krytyczne, automatycznie tworzony jest nowy element ReadableStream. Jeśli wystąpi błąd krytyczny, np. zostanie usunięte urządzenie szeregowe, port.readable będzie mieć wartość null.

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.
  }
}

Jeśli urządzenie szeregowe odeśle tekst, możesz użyć kreski port.readable przez kreskę TextDecoderStream, jak pokazano poniżej. TextDecoderStream to strumień przekształcenia, który pobiera wszystkie fragmenty (Uint8Array) i przekształca je w ciągi tekstowe.

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);
}

Przy użyciu czytnika „Przynieś własny bufor” możesz kontrolować sposób przydzielania pamięci podczas czytania ze strumienia. Wywołaj port.readable.getReader({ mode: "byob" }), aby uzyskać interfejs ReadableStreamBYOBReader i udostępnić własny ArrayBuffer podczas wywoływania metody read(). Pamiętaj, że interfejs Web Serial API obsługuje tę funkcję w Chrome 106 i nowszych wersjach.

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()...
  }
}

Oto przykład ponownego użycia bufora z 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`.
}

Oto inny przykład sposobu odczytu określonej ilości danych z portu szeregowego:

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);

Zapisz na porcie szeregowym

Aby wysłać dane na urządzenie szeregowe, przekaż je do port.writable.getWriter().write(). Wywołanie releaseLock() na port.writable.getWriter() jest wymagane, aby port szeregowy został później zamknięty.

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();

Wyślij SMS-a na urządzenie za pomocą TextEncoderStream potoku port.writable, jak pokazano poniżej.

const textEncoder = new TextEncoderStream();
const writableStreamClosed = textEncoder.readable.pipeTo(port.writable);

const writer = textEncoder.writable.getWriter();

await writer.write("hello");

Zamknij port szeregowy

port.close() zamyka port szeregowy, jeśli jego elementy readable i writableodblokowane, co oznacza, że usługa releaseLock() została wywołana dla odpowiednich odczytujących i zapisujących.

await port.close();

Jednak podczas ciągłego odczytu danych z urządzenia szeregowego za pomocą pętli port.readable jest zawsze zablokowany, dopóki nie napotka błędu. W tym przypadku wywołanie metody reader.cancel() wymusi natychmiastowe rozwiązanie problemu z protokołem reader.read() za pomocą metody { value: undefined, done: true }, dzięki czemu pętla będzie mogła wywołać 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;
});

Zamknięcie portu szeregowego jest bardziej skomplikowane w przypadku korzystania ze strumieni przekształcania. Zadzwoń pod numer reader.cancel() tak jak poprzednio. Następnie zadzwoń do: writer.close() i port.close(). Spowoduje to propagację błędów przez strumienie przekształcenia do bazowego portu szeregowego. Błędy nie występują natychmiast, dlatego musisz użyć utworzonych wcześniej readableStreamClosed i writableStreamClosed, aby wykryć, kiedy urządzenia port.readable i port.writable zostały odblokowane. Anulowanie strumienia reader spowoduje przerwanie strumienia, dlatego musisz wychwycić i zignorować wynikowy błąd.

// 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();

Nasłuchiwanie połączenia i rozłączenia

Jeśli port szeregowy jest udostępniany przez urządzenie USB, może ono być podłączone lub odłączone od systemu. Gdy witryna otrzyma dostęp do portu szeregowego, powinna monitorować zdarzenia connect i 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.
});

Obsługa sygnałów

Po nawiązaniu połączenia z portem szeregowym możesz bezpośrednio wysyłać zapytania i ustawiać sygnały udostępniane przez port szeregowy na potrzeby wykrywania urządzeń i sterowania przepływem. Sygnały te są definiowane jako wartości logiczne. Na przykład niektóre urządzenia, takie jak Arduino, przejdą w tryb programowania po włączeniu sygnału DTR.

Aby skonfigurować sygnały wyjściowe i otrzymywać sygnały wejściowe, należy wywołać metodę port.setSignals() i port.getSignals(). Zobacz przykłady użycia poniżej.

// 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}`);

Przekształcanie strumieni

W przypadku odbierania danych z urządzenia szeregowego nie zawsze możesz pobrać wszystkie dane od razu. Może być dowolnie podzielony na fragmenty. Więcej informacji znajdziesz w artykule o pojęciach związanych z interfejsem Streams API.

Aby sobie z tym radzić, możesz użyć wbudowanych strumieni przekształcenia, np. TextDecoderStream, lub utworzyć własny, który umożliwia analizowanie przychodzącego strumienia i zwracanie przeanalizowanych danych. Strumień przekształcania znajduje się między urządzeniem szeregowym a pętlą odczytu, która zużywa strumień. Może zastosować dowolne przekształcenie przed wykorzystaniem danych. Pomyśl o nim jak o linii montażowej: widżet działa jak po linii, a każdy kolejny jego element zmienia widżet tak, że zanim dotrze do miejsca docelowego, staje się on w pełni działający.

Zdjęcie fabryki samolotów
Fabryka samolotów w bromwichu w zamku z czasów II wojny światowej

Zobacz na przykład, jak utworzyć klasę strumienia przekształcania, która pozyskuje strumień i dzieli go na fragmenty według podziałów wierszy. Jej metoda transform() jest wywoływana za każdym razem, gdy strumień otrzyma nowe dane. Dane mogą zostać umieszczone w kolejce lub zapisane na później. Metoda flush() jest wywoływana po zamknięciu strumienia i obsługuje wszelkie dane, które nie zostały jeszcze przetworzone.

Aby użyć klasy strumienia przekształcenia, musisz przenieść przez nią strumień przychodzący za pomocą potoku. W trzecim przykładowym kodzie w sekcji Odczyt z portu szeregowego pierwotny strumień wejściowy był przekazywany tylko przez interfejs TextDecoderStream, więc musimy wywołać pipeThrough(), aby przekazać go potokiem przez nowy 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();

Do debugowania problemów z komunikacją z urządzeniem szeregowym użyj metody tee() port.readable, aby podzielić strumienie trafiające do tego urządzenia lub z niego. Dwa utworzone strumienie można używać niezależnie, co pozwala wydrukować jeden z nich w konsoli w celu inspekcji.

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.

Unieważnij dostęp do portu szeregowego

Witryna może wyczyścić uprawnienia dostępu do portu szeregowego, którego już nie chce zachować, wywołując forget() w instancji SerialPort. Na przykład w przypadku edukacyjnej aplikacji internetowej używanej na wspólnym komputerze z wieloma urządzeniami duża liczba uprawnień generowanych przez użytkowników pogarsza komfort korzystania z usługi.

// Voluntarily revoke access to this serial port.
await port.forget();

Usługa forget() jest dostępna w Chrome 103 i nowszych wersjach, więc sprawdź, czy obsługuje ją:

if ("serial" in navigator && "forget" in SerialPort.prototype) {
  // forget() is supported.
}

Wskazówki dla programistów

Debugowanie interfejsu Web Serial API w Chrome jest proste dzięki wewnętrznej stronie about://device-log, na której znajdziesz wszystkie zdarzenia związane z urządzeniami szeregowymi w jednym miejscu.

Zrzut ekranu przedstawiający wewnętrzną stronę do debugowania interfejsu Web Serial API.
Wewnętrzna strona w Chrome służąca do debugowania interfejsu Web Serial API.

Ćwiczenia z programowania

W ramach ćwiczenia z programowania w Google Developers użyjesz interfejsu Web Serial API do interakcji z płytką BBC micro:bit i wyświetlaniem obrazów na matrycy LED 5 x 5.

Obsługiwane przeglądarki

Interfejs Web Serial API jest dostępny na wszystkich platformach stacjonarnych (ChromeOS, Linux, macOS i Windows) w Chrome 89.

Włókno poliestrowe

Na urządzeniu z Androidem obsługa portów szeregowych USB jest możliwa za pomocą interfejsów WebUSB API i Serial API polyfill. Działanie kodu polyfill jest ograniczone do sprzętu i platform, na których urządzenie jest dostępne przez interfejs API WebUSB, ponieważ nie zostało zgłoszone przez wbudowany sterownik urządzenia.

Prywatność i bezpieczeństwo

Autorzy specyfikacji zaprojektowali i wdrożyli interfejs Web Serial API zgodnie z podstawowymi zasadami określonymi w artykule Kontrolowanie dostępu do zaawansowanych funkcji platformy internetowej, w tym dotyczących kontroli użytkownika, przejrzystości i ergonomii. Dostęp do tego interfejsu API zależy głównie od modelu uprawnień, który w danym momencie przyznaje dostęp tylko do 1 urządzenia szeregowego. W odpowiedzi na prośbę użytkownika użytkownik musi podjąć aktywne działania, aby wybrać konkretne urządzenie szeregowe.

Aby dowiedzieć się, jak to wpływa na bezpieczeństwo, zapoznaj się z sekcjami na temat bezpieczeństwa i prywatności w opisie interfejsu Web Serial API.

Prześlij opinię

Zespół Chrome chętnie pozna Twoją opinię i doświadczenia związane z interfejsem Web Serial API.

Opowiedz nam o projekcie interfejsu API

Czy jest coś, co nie działa w interfejsie API zgodnie z oczekiwaniami? A może brakuje metod lub właściwości, które są niezbędne do realizacji pomysłu?

Zgłoś problem ze specyfikacją w repozytorium Web Serial API na GitHubie lub dodaj swoje przemyślenia do istniejącego problemu.

Zgłoś problem z implementacją

Czy wystąpił błąd związany z implementacją przeglądarki Chrome? A może implementacja różni się od specyfikacji?

Zgłoś błąd na stronie https://new.crbug.com. Podaj jak najwięcej szczegółów i proste instrukcje odtworzenia błędu oraz ustaw Komponenty na Blink>Serial. Glitch to świetny sposób na udostępnianie szybkich i łatwych replik.

Okaż wsparcie

Czy zamierzasz używać interfejsu Web Serial API? Twoja publiczna pomoc pomaga zespołowi Chrome priorytetowo traktować funkcje i pokazuje innym dostawcom przeglądarek, jak ważne jest ich wsparcie.

Wyślij tweeta na adres @ChromiumDev, używając hashtagu #SerialAPI, i daj nam znać, gdzie i w jaki sposób go używasz.

Przydatne linki

Przykłady

Podziękowania

Dziękujemy Reilly Grant i Joe Medley za opinię o tym artykule. Zdjęcie fabryki samolotów udostępnione przez Birmingham Museums TrustUnsplash.