Odczyt z portu szeregowego i zapis na nim

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

François Beaufort
François Beaufort

Co to jest Web Serial API?

Port szeregowy to dwukierunkowy interfejs komunikacyjny, który umożliwia wysyłanie bajt danych po bajcie.

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

Innymi słowy, interfejs Web Serial API łączy sieć i świat fizyczny, umożliwiające witrynom komunikowanie się z urządzeniami szeregowymi, takimi jak mikrokontrolery, i drukarki 3D.

Ten interfejs API jest też świetnym uzupełnieniem WebUSB, ponieważ systemy operacyjne wymagają aplikacje do komunikowania się z niektórymi portami szeregowymi za pomocą ich szeregowego interfejsu API, a nie niskopoziomowego interfejsu USB API.

Sugerowane zastosowania

W sektorach edukacyjnych, hobbystycznych i przemysłowych użytkownicy łączą się z urządzeniami peryferyjnymi między urządzeniami i komputerami. Te urządzenia są często sterowane przez: przez połączenie szeregowe wykorzystywane przez specjalne oprogramowanie. Niektóre niestandardowe do sterowania tymi urządzeniami korzysta z technologii internetowej:

Czasem strony komunikują się z urządzeniem przez agenta którą użytkownicy zainstalowali ręcznie. W innych aplikacja jest w postaci pakietu aplikacji za pomocą platformy takiej jak Electron. W innych przypadkach użytkownik musi wykonać dodatkową czynność, taką jak kopiowanie skompilowanej aplikacji na urządzenie za pomocą dysku flash USB.

We wszystkich tych przypadkach poprawa wrażeń użytkowników poprzez bezpośrednie między stroną internetową a urządzeniem, którym zarządza.

Obecny stan,

Krok Stan
1. Utwórz wyjaśnienie Zakończono
2. Utwórz początkową wersję roboczą specyfikacji Zakończono
3. Zbieraj opinie iterować projekt Zakończono
4. Wersja próbna origin Zakończono
5. Wprowadzenie na rynek Zakończono

Używanie interfejsu Web Serial API

Wykrywanie cech

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. Uniemożliwia to interfejsowi witryny w trakcie oczekiwania na dane wejściowe, co jest ważne, ponieważ dane seryjne mogą być w dowolnym momencie, i wymagać możliwości słuchania.

Aby otworzyć port szeregowy, najpierw uzyskaj dostęp do obiektu SerialPort. Aby to zrobić, albo poproś użytkownika o wybranie pojedynczego portu szeregowego, wywołując navigator.serial.requestPort() w odpowiedzi na gest użytkownika, taki jak dotyk lub kliknięciem przycisku myszy. Możesz też wybrać jeden z nich z listy navigator.serial.getPorts(), który zwróci lista 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óra definiuje filtry. Służą one do dopasowywania dowolnych urządzeń szeregowych podłączonych USB z wymaganym dostawcą USB (usbVendorId) i opcjonalnym urządzeniem 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 z prośbą o port szeregowy widoczny na stronie internetowej
Prośba użytkownika o wybór pola BBC micro:bit

Gdy zadzwonisz pod requestPort(), użytkownik będzie mógł wybrać urządzenie i zwróci SerialPort obiekt. Gdy masz już obiekt SerialPort, wywołanie port.open() z wybraną szybkością transmisji spowoduje otwarcie portu szeregowego. Słownik baudRate element określa szybkość przesyłania danych przez linię szeregową. Jest wyrażony w w bitach na sekundę (b/s). Zapoznaj się z dokumentacją urządzenia, aby dowiedzieć się, ma prawidłową wartość, bo wszystkie wysyłane i odbierane dane będą bezużyteczne. nieprawidłowo określono. Niektóre urządzenia USB i Bluetooth, które emulują kod szeregowy port może mieć dowolną wartość, ponieważ jest ignorowana przez za pomocą emulacji.

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

Możesz też określić dowolną z poniższych opcji podczas otwierania portu szeregowego. Te opcje 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 odczytu i zapisu, które powinny zostać utworzone (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 ustanowieniu połączenia przez port szeregowy readable i writable właściwości z obiektu SerialPort zwracają obiekty ReadableStream i WritableStream. Te dane będą używane do odbierania danych z usług i wysyłania ich do urządzenia szeregowego. Do przesyłania danych obie używają instancji Uint8Array.

Gdy z urządzenia szeregowego przychodzą nowe dane, port.readable.getReader().read() zwraca asynchronicznie dwie właściwości: value i done. Jeśli done ma wartość prawda, port szeregowy został zamknięty lub nie ma już przesyłanych danych cal Wywołanie port.readable.getReader() powoduje utworzenie czytnika i blokowanie użytkownika readable . Gdy urządzenie readable jest zablokowane, nie można zamknąć portu szeregowego.

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 pewnych sytuacjach, na przykład przepełnienie bufora, błędy kadrowania i błędy parzystości. Rzucane jako wyjątków i można je złapać, dodając kolejną pętlę na poprzednią sprawdza port.readable. To działa, jeśli błędy są niekrytyczny, automatycznie tworzony jest nowy obiekt ReadableStream. W przypadku błędu krytycznego na przykład urządzenie szeregowe zostanie usunięte, wówczas port.readable stanie się 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ć potoku port.readable przez TextDecoderStream, jak widać poniżej. TextDecoderStream to strumień przekształcenia który przechwytuje wszystkie Uint8Array fragmenty i konwertuje je w ciągi.

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

Możesz kontrolować sposób przydzielania pamięci podczas odczytywania danych ze strumienia za pomocą opcji „przynieś własny bufor” . Wywołaj port.readable.getReader({ mode: "byob" }), aby uzyskać interfejs ReadableStreamBYOBReader i podaj własny ArrayBuffer przy wywoływaniu funkcji 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 odczytywania 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 w porcie szeregowym

Aby wysłać dane na urządzenie szeregowe, przekaż je do port.writable.getWriter().write() Dzwonię do: releaseLock() wł. Aby port szeregowy można było później zamknąć, wymagany jest element 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();

Wyślij SMS-a na urządzenie za pomocą potoku TextEncoderStream przesyłanego do: 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 wezwano użytkownika releaseLock() do odpowiednich czytelnika i pisarza.

await port.close();

Jednak podczas ciągłego odczytu danych z urządzenia szeregowego za pomocą pętli, Urządzenie port.readable będzie zawsze zablokowane, dopóki nie wystąpi błąd. W tym wywołanie reader.cancel() wymusi rozwiązanie reader.read() natychmiast za pomocą { value: undefined, done: true }, dzięki czemu funkcja zapętlanie funkcji 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;
});

Zamykanie portu szeregowego jest bardziej skomplikowane w przypadku korzystania ze strumieni przekształcania. Zadzwoń pod numer reader.cancel() tak jak poprzednio. Następnie zadzwoń pod numer writer.close() i port.close(). Spowoduje to rozpowszechnianie błędów przez strumienie przekształcenia do odpowiedniego portu szeregowego. Ponieważ propagacja błędów nie dzieje się od razu, musisz użyć funkcji readableStreamClosed oraz Funkcja writableStreamClosed obiecuje utworzone wcześniej, aby wykryć, kiedy port.readable i port.writable zostały odblokowane. Anulowanie reader powoduje, że strumień do przerwania; dlatego musisz wychwycić i zignorować 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łuchuj połączenia i rozłączania

Jeśli port szeregowy jest zapewniony przez urządzenie USB, można je podłączyć do lub odłączony od systemu. W przypadku gdy zezwolono witrynie na dostępu do portu szeregowego, powinien 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.
});

Sygnały

Po nawiązaniu połączenia przez port szeregowy możesz bezpośrednio sygnałów ujawnianych przez port szeregowy do wykrywania urządzeń i kontrolowania przepływu. Te sygnały są zdefiniowane jako wartości logiczne. Na przykład niektóre urządzenia, takie jak Arduino, przejdzie w tryb programowania, jeśli sygnał Data Terminal Ready (DTR) będzie przełączono.

Aby skonfigurować sygnały wyjściowe i uzyskać sygnały wejściowe, należy odpowiednio: Dzwonię do: port.setSignals() i port.getSignals(). Zobacz przykłady ich 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

Gdy odbierasz dane z urządzenia szeregowego, niekoniecznie są to wszystkie wszystkie dane naraz. Może być dowolnie podzielony na fragmenty. Więcej informacji: Pojęcia związane z interfejsem Streams API.

Aby sobie z tym poradzić, możesz użyć wbudowanych strumieni przekształcenia, takich jak: TextDecoderStream lub utwórz własny strumień przekształcenia, który umożliwia przeanalizować strumień przychodzący i zwrócić przeanalizowane dane. Strumień przekształcenia leży między urządzeniem szeregowym a pętlą odczytu, która pobiera strumień. Może zastosowanie dowolnego przekształcenia przed przetworzeniem danych. Pomyśl o tym jak o tym, wiersz montażowy: w miarę jak widżet przesuwa się po linii, każdy krok w wierszu zmienia w widżecie, dzięki czemu, zanim dotrze do miejsca docelowego, będzie całkowicie działającego widżetu.

Zdjęcie fabryki samolotów
Fabryka samolotów w zamku Bromwich II wojny światowej

Zastanów się na przykład, jak utworzyć klasę strumienia przekształcania, która pobiera i dzielić go na fragmenty na podstawie podziałów wierszy. Jego metoda transform() nazywa się za każdym razem, gdy strumień odbierze nowe dane. Może dodać dane do kolejki lub zapisz je na później. Metoda flush() jest wywoływana po zamknięciu strumienia. obsługuje on wszystkie dane, które nie zostały jeszcze przetworzone.

Aby użyć klasy strumienia przekształcenia, musisz wprowadzić potok przychodzący . W trzecim przykładzie w sekcji Odczyt z portu szeregowego oryginalny strumień danych wejściowych był przesyłany tylko przez TextDecoderStream, więc trzeba zadzwonić do pipeThrough() i użyć potoku przez nasz 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ądzeniami szeregowymi użyj metody tee() port.readable, aby podzielić strumienie trafiające do lub z urządzenia szeregowego. Obie strumienie mogą być wykorzystywane niezależnie, a dzięki temu można wydrukować jedną do konsoli w celu sprawdzenia.

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

Strona może wyczyścić uprawnienia dostępu do portu szeregowego, do którego już nie ma dostępu chcesz zachować zainteresowanie, wywołując funkcję forget() w wystąpieniu SerialPort. Dla: przykładowa edukacyjna aplikacja internetowa używana na współdzielonym komputerze tych urządzeń, duża liczba zebranych uprawnień użytkowników powoduje, użytkowników.

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

Funkcja forget() jest dostępna w Chrome 103 i nowszych wersjach, więc sprawdź, czy jest ona obsługiwane przez:

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

Wskazówki dla programistów

Debugowanie interfejsu Web Serial API w Chrome jest łatwe dzięki stronie wewnętrznej, about://device-log, gdzie możesz zobaczyć wszystkie zdarzenia związane z urządzeniami szeregowymi w jednym w jednym miejscu.

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

Ćwiczenia z programowania

W ćwiczeniach z programowania w Google Developers będziesz używać interfejsu Web Serial API do interakcji i platformy BBC micro:bit, aby wyświetlać obrazy na matrycy LED 5 x 5.

Obsługa przeglądarek

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

Watolina

Na Androidzie można korzystać z portów szeregowych opartych na USB przy użyciu interfejsu WebUSB API. oraz kod polyfill Serial API. Ten kod polyfill jest ograniczony do sprzętu, platformy, na których urządzenie jest dostępne za pośrednictwem interfejsu WebUSB API, ponieważ nie jest zostało zgłoszone przez wbudowany sterownik urządzenia.

Bezpieczeństwo i prywatność

Autorzy specyfikacji zaprojektowali i wdrożyli interfejs Web Serial API za pomocą podstawowego zasad zdefiniowanych w artykule Kontrolowanie dostępu do zaawansowanych funkcji platform internetowych, takich jak kontrola użytkownika, przejrzystość i ergonomia. Możliwość korzystania z tej karty Interfejs API jest chroniony głównie przez model uprawnień, który przyznaje dostęp tylko urządzenia szeregowego jednocześnie. W odpowiedzi na prośbę użytkownika musi on podjąć decyzję kroków, aby wybrać konkretne urządzenie szeregowe.

Aby poznać zalety związane z bezpieczeństwem, zapoznaj się z informacjami o bezpieczeństwie i prywatności Web Serial API Explainer.

Prześlij opinię

Zespół Chrome chętnie pozna Twoje przemyślenia i doświadczenia dotyczące Web Serial API.

Opowiedz nam o konstrukcji interfejsu API

Czy jest coś, co w interfejsie API nie działa zgodnie z oczekiwaniami? Czy są jeśli brakuje metod lub właściwości niezbędnych do realizacji pomysłu?

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

Zgłoś problem z implementacją

Czy wystąpił błąd z implementacją Chrome? Czy wdrożenie różni się od specyfikacji?

Zgłoś błąd na https://new.crbug.com. Podaj jak najwięcej jak najwięcej szczegółów, podać proste instrukcje dotyczące odtworzenia błędu oraz Ustawiono Komponenty na Blink>Serial. Glitch działa świetnie: szybkie i łatwe udostępnianie.

Pokaż wsparcie

Czy zamierzasz korzystać z interfejsu Web Serial API? Wasze publiczne wsparcie pomaga nam ulepszyć Chrome nadaje priorytet funkcjom 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 jak go używasz.

Przydatne linki

Prezentacje

Podziękowania

Dziękujemy Reilly Grant i Joe Medley za opinię o tym artykule. Zdjęcie fabryki samolotu wykonane przez Birmingham Museums Trust na kanale Unsplash.