Interfejs Web Serial API umożliwia witrynom komunikowanie się z urządzeniami szeregowymi.
Co to jest Web Serial API?
Port szeregowy to dwukierunkowy interfejs komunikacji, który umożliwia wysyłanie i odbieranie danych bajt po bajcie.
Interfejs Web Serial API umożliwia witrynom odczytywanie i zapisywanie danych na urządzeniu szeregowym za pomocą kodu JavaScript. 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 doskonale uzupełnia WebUSB, ponieważ systemy operacyjne wymagają, aby aplikacje komunikowały się z niektórymi portami szeregowymi za pomocą wyższego poziomu szeregowego interfejsu API zamiast niskopoziomowego interfejsu USB API.
Sugerowane zastosowania
W sektorze edukacyjnym, hobbystycznym i przemysłowym użytkownicy podłączają do komputerów urządzenia peryferyjne. Te urządzenia są często kontrolowane przez mikrokontrolery za pomocą połączenia szeregowego używanego przez niestandardowe oprogramowanie. Niektóre niestandardowe oprogramowanie do sterowania tymi urządzeniami jest tworzone przy użyciu technologii internetowych:
W niektórych przypadkach strony komunikują się z urządzeniem za pomocą aplikacji agenta, którą użytkownicy zainstalowali ręcznie. W innych przypadkach aplikacja jest dostarczana w pakiecie za pomocą platformy, takiej jak Electron. W innych przypadkach użytkownik musi wykonać dodatkowy krok, na przykład skopiować skompilowane aplikacje na urządzenie za pomocą pamięci USB.
We wszystkich tych przypadkach wygodę użytkownika zwiększy bezpośrednia komunikacja między witryną a urządzeniem, którym steruje.
Obecny stan,
Krok | Stan |
---|---|
1. Tworzenie wyjaśnienia | Zakończono |
2. Tworzenie wstępnej wersji specyfikacji | Zakończono |
3. Zbieranie opinii i ulepszanie projektu | Zakończono |
4. Wersja próbna origin | Zakończono |
5. Uruchom | Zakończono |
Korzystanie z 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.
}
Otwieranie portu szeregowego
Interfejs Web Serial API jest asynchroniczny z założenia. Zapobiega to blokowaniu interfejsu witryny w oczekiwaniu na dane wejściowe, co jest ważne, ponieważ dane szeregowe mogą być odbierane w dowolnym momencie, co wymaga sposobu ich odsłuchiwania.
Aby otworzyć port szeregowy, najpierw uzyskaj dostęp do obiektu SerialPort
. W tym celu możesz albo poprosić użytkownika o wybranie jednego portu szeregowego, wywołując funkcję navigator.serial.requestPort()
w odpowiedzi na gest użytkownika, taki jak dotyk lub kliknięcie myszką, albo wybrać jeden z portów z listy navigator.serial.getPorts()
, która zwraca listę portów szeregowych, do których strona 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 definiuje filtry. Są one używane do dopasowywania dowolnego urządzenia szeregowego podłączonego przez USB do obowiązkowego identyfikatora dostawcy USB (usbVendorId
) i opcjonalnych identyfikatorów produktu 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();
Wywołanie funkcji requestPort()
powoduje wyświetlenie użytkownikowi prośby o wybranie urządzenia i zwraca obiekt SerialPort
. Gdy masz obiekt SerialPort
, wywołanie funkcji port.open()
z wybraną szybkością transmisji danych spowoduje otwarcie portu szeregowego. Słownik baudRate
określa szybkość przesyłania danych przez linię szeregową. Jest ono wyrażane w jednostkach bitów na sekundę (bps). Sprawdź w dokumentacji urządzenia prawidłową wartość, ponieważ jeśli zostanie podana nieprawidłowa wartość, wszystkie wysyłane i odbierane dane będą niezrozumiałe. W przypadku niektórych urządzeń USB i Bluetooth, które emulują port szeregowy, można bezpiecznie ustawić dowolną wartość, ponieważ emulacja ją ignoruje.
// 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ż określić dowolną z podanych niżej opcji. 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 stopu na końcu ramki (1 lub 2).parity
: tryb parzy ("none"
,"even"
lub"odd"
).bufferSize
: rozmiar buforów odczytu i zapisu, które należy utworzyć (musi być mniejszy niż 16 MB).flowControl
: tryb kontroli przepływu ("none"
lub"hardware"
).
Odczyt z portu szeregowego
Strumienie wejściowe i wyjściowe w interfejsie Web Serial API są obsługiwane przez interfejs Streams API.
Po nawiązaniu połączenia z portem szeregowym właściwości readable
i writable
obiektu SerialPort
zwracają obiekty ReadableStream i WritableStream. Będą one służyć do odbierania danych z urządzenia seryjnego i do ich wysyłania na to urządzenie. Do przesyłania danych obie używają instancji Uint8Array
.
Po otrzymaniu nowych danych z urządzenia szeregowego funkcja port.readable.getReader().read()
zwraca asynchronicznie 2 właściwości: value
i done
. Jeśli done
ma wartość prawda, port szeregowy został zamknięty lub nie ma już więcej danych. Wywołanie port.readable.getReader()
tworzy pod odczytującego i blokuje readable
. Gdy readable
jest zablokowany, port szeregowy nie może zostać zamknięty.
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);
}
W niektórych warunkach mogą wystąpić niekrytyczne błędy odczytu portu szeregowego, takie jak przepełnienie bufora, błędy kadrowania lub błędy parzy. Są one zgłaszane jako wyjątki i można je złapać, dodając kolejną pętlę nad poprzednią, która sprawdza port.readable
. To działa, jeśli błędy nie są krytyczne, automatycznie tworzony jest nowy obiekt ReadableStream. Jeśli wystąpi krytyczny błąd, np. zostanie usunięte urządzenie seryjne, port.readable
stanie się 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 pokazano poniżej. TextDecoderStream
to strumień transformacji, który pobiera wszystkie fragmenty Uint8Array
i konwertuje je na ciągi znaków.
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);
}
Podczas odczytu z potoku za pomocą czytnika „Bring Your Own Buffer” możesz kontrolować sposób przydzielania pamięci. Wywołaj port.readable.getReader({ mode: "byob" })
, aby uzyskać interfejs ReadableStreamBYOBReader, i podaj własny obiekt ArrayBuffer
podczas wywoływania read()
. Web Serial API obsługuje tę funkcję w Chrome 106 lub nowszej.
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 funkcji 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 kolejny 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);
Pisanie do portu szeregowego
Aby wysłać dane do urządzenia szeregowego, prześlij je do port.writable.getWriter().write()
. Wywołanie funkcji releaseLock()
w programie port.writable.getWriter()
jest wymagane, aby później można było zamknąć port szeregowy.
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 tekst na urządzenie za pomocą polecenia TextEncoderStream
przesł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 elementy readable
i writable
są odblokowane, co oznacza, że dla odpowiednich czytników i nagrywarek została wywołana funkcja releaseLock()
.
await port.close();
Jednak podczas ciągłego odczytu danych z urządzenia szeregowego za pomocą pętli port.readable
będzie zawsze zablokowany, dopóki nie napotka błędu. W takim przypadku wywołanie metody reader.cancel()
wymusi natychmiastowe wykonanie polecenia reader.read()
przez wywołanie { value: undefined, done: true }
, co pozwoli pętli 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, gdy używasz przekształcania strumieni. Zadzwoń pod numer reader.cancel()
tak jak wcześniej.
Następnie zadzwoń do writer.close()
i port.close()
. Spowoduje to rozpowszechnianie błędów przez strumienie przekształcenia do bazowego portu szeregowego. Ponieważ propagacja błędów nie następuje natychmiast, musisz użyć wcześniej utworzonych obietnic readableStreamClosed
i writableStreamClosed
, aby wykryć, kiedy port.readable
i port.writable
zostały odblokowane. Anulowanie reader
powoduje przerwanie strumienia. Dlatego musisz przechwycić i zignorować powstały 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();
Odsłuchiwanie połączeń i rozłączeń
Jeśli port szeregowy jest udostępniany przez urządzenie USB, to urządzenie może być połączone lub rozłączone z systemem. Gdy witryna ma przyznane uprawnienia do dostępu 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 wysyłać zapytania i ustawiać sygnały udostępniane przez port szeregowy w celu wykrywania urządzenia i kontroli przepływu danych. Te sygnały są zdefiniowane jako wartości logiczne. Na przykład niektóre urządzenia, takie jak Arduino, przechodzą w tryb programowania, gdy sygnał DTR jest włączony.
Ustawianie sygnałów wyjściowych i pobieranie sygnałów wejściowych odbywa się odpowiednio przez wywołanie funkcji 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 otrzymasz dane z urządzenia szeregowego, niekoniecznie otrzymasz wszystkie dane naraz. Może być dowolnie dzielone na fragmenty. Więcej informacji znajdziesz w artykule Koncepcje dotyczące interfejsu Streams API.
Aby to zrobić, możesz użyć wbudowanych przekształceń danych, takich jak TextDecoderStream
, lub utworzyć własne przekształcenie danych, które pozwoli Ci przeanalizować przychodzący strumień danych i zwrócić przeanalizowane dane. Strumień transformacji znajduje się pomiędzy urządzeniem szeregowym a pętlą odczytu, która pobiera strumień. Może zastosować dowolne przekształcenie przed przetworzeniem danych. Wyobraź sobie to jak taśmę produkcyjną: gdy element przemieszcza się po taśmie, każdy etap modyfikuje go, tak aby do czasu dotarcia do miejsca docelowego był w pełni funkcjonalny.
Możesz na przykład utworzyć klasę transformacji strumienia, która pobiera strumień i dzieli go na części na podstawie znaków końca wiersza. Jego metoda transform()
jest wywoływana za każdym razem, gdy strumień otrzyma nowe dane. Może ona umieścić dane w kole lub zapisać je na później. Metoda flush()
jest wywoływana po zamknięciu strumienia i przetwarza wszystkie dane, które nie zostały jeszcze przetworzone.
Aby używać klasy transform stream, musisz przekierować do niej strumień wejściowy. W 3 przykładzie kodu w sekcji Odczyt z portu szeregowego pierwotny strumień danych był przekazywany tylko przez TextDecoderStream
, więc musimy wywołać pipeThrough()
, aby przekazać go 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();
Aby debugować problemy z komunikacją z urządzeniem szeregowym, użyj metody tee()
w programie port.readable
, aby podzielić strumienie na te, które idą do urządzenia szeregowego i z niego. Utworzone 2 strumienie można używać niezależnie, co pozwala na wydrukowanie jednego z nich w 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.
Odmowa dostępu do portu szeregowego
Witryna może usunąć uprawnienia do dostępu do portu szeregowego, którego nie chce już zachowywać, wywołując funkcję forget()
w instancji SerialPort
. Na przykład w przypadku edukacyjnej aplikacji internetowej używanej na współdzielonym komputerze z wieloma urządzeniami duża liczba gromadzonych uprawnień generowanych przez użytkowników może pogorszyć komfort korzystania z aplikacji.
// Voluntarily revoke access to this serial port.
await port.forget();
Usługa forget()
jest dostępna w Chrome 103 i nowszych wersjach. Sprawdź, czy ta funkcja jest obsługiwana w tych miejscach:
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 wewnętrznej stronie about://device-log
, na której możesz zobaczyć wszystkie zdarzenia związane z urządzeniem szeregowym w jednym miejscu.
Ćwiczenia z programowania
W Google Developer Codelab użyjesz interfejsu Web Serial API do interakcji z płytką BBC micro:bit, aby wyświetlać obrazy na jej 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 obsługa portów szeregowych na USB jest możliwa za pomocą interfejsu WebUSB API i polyfilla Serial API. Ta funkcja zastępcza jest ograniczona do sprzętu i platform, w których przypadku urządzenie jest dostępne za pomocą interfejsu WebUSB API, ponieważ nie zostało ono zadeklarowane przez wbudowany sterownik urządzenia.
Bezpieczeństwo i prywatność
Autorzy specyfikacji zaprojektowali i wdrożyli interfejs Web Serial API, korzystając z podstawowych zasad zdefiniowanych w dokumentacji Controlling Access to Powerful Web Platform Features (Kontrolowanie dostępu do zaawansowanych funkcji platformy internetowej), w tym kontroli użytkownika, przejrzystości i ergonomii. Możliwość korzystania z tego interfejsu API zależy głównie od modelu uprawnień, który przyznaje dostęp tylko do jednego urządzenia szeregowego naraz. W odpowiedzi na prośbę użytkownika musi on wykonać określone czynności, aby wybrać konkretne urządzenie z numerem seryjnym.
Aby lepiej zrozumieć wady bezpieczeństwa, zapoznaj się z sekcjami dotyczącymi bezpieczeństwa i prywatności artykułu Web Serial API Explainer.
Prześlij opinię
Zespół Chrome chętnie pozna Twoje opinie i wrażenia związane z interfejsem Web Serial API.
Opowiedz nam o konstrukcji interfejsu API
Czy interfejs API nie działa zgodnie z oczekiwaniami? A może brakuje metod lub właściwości, których potrzebujesz do wdrożenia swojego pomysłu?
Zgłoś problem ze specyfikacją w gabinecie GitHub interfejsu Web Serial API lub dodaj swoje uwagi do istniejącego problemu.
Zgłaszanie problemów z implementacją
Czy znalazłeś/znalazłaś błąd w implementacji 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, dołącz proste instrukcje odtwarzania błędu i ustaw Składniki na Blink>Serial
. Glitch to świetne narzędzie do szybkiego i łatwego udostępniania informacji o powtarzalności problemu.
Pokaż pomoc
Czy zamierzasz korzystać z interfejsu Web Serial API? Twoja publiczna pomoc pomaga zespołowi Chrome ustalać priorytety funkcji i pokazuje innym dostawcom przeglądarek, jak ważne jest ich wsparcie.
Wyślij tweeta do @ChromiumDev, używając hashtaga #SerialAPI
, i podaj, gdzie i jak go używasz.
Przydatne linki
- Specyfikacja
- Śledzenie błędu
- Wpis na stronie ChromeStatus.com
- Składnik Blink:
Blink>Serial
Prezentacje
Podziękowania
Dziękujemy Reilly Grant i Joe Medley za sprawdzenie tego artykułu. Zdjęcie fabryki samolotu wykonane przez Birmingham Museums Trust na kanale Unsplash.