Pierwsze kroki z interfejsem Web Serial API

1. Wprowadzenie

Ostatnia aktualizacja: 21.07.2020

Co utworzysz

W ramach tego ćwiczenia w programowaniu utworzysz stronę internetową, która używa interfejsu Web Serial API do interakcji z płytką BBC micro:bit, aby pokazywać obrazy na matrycy LED 5 x 5. Dowiesz się więcej o interfejsie Web Serial API oraz o tym, jak używać strumieni możliwych do odczytania i zapisu oraz przekształcenia do komunikacji z urządzeniami szeregowymi w przeglądarce.

81167ab7c01d353d.png

Czego się nauczysz

  • Jak otworzyć i zamknąć internetowy port szeregowy
  • Jak używać pętli odczytu do obsługi danych ze strumienia wejściowego
  • Jak wysyłać dane za pomocą strumienia zapisu

Czego potrzebujesz

Wybraliśmy kartę micro:bit, bo jest przystępna cenowo, ma kilka wejść (przycisków) i wyjść (ekran LED 5 x 5) oraz umożliwia dodatkowe wejście i wyjście. Szczegółowe informacje o możliwościach mikro:bita znajdziesz na stronie BBC micro:bit w witrynie Espruino.

2. Informacje o interfejsie Web Serial API

Interfejs Web Serial API umożliwia stronom internetowym odczyt i zapis na urządzeniu szeregowym za pomocą skryptów. Ten interfejs API łączy internet ze światem fizycznym, umożliwiając witrynom komunikowanie się z urządzeniami szeregowymi, takimi jak mikrokontrolery i drukarki 3D.

Istnieje wiele przykładów oprogramowania sterującego tworzonego z wykorzystaniem technologii internetowej. Na przykład:

Czasem takie strony komunikują się z urządzeniem za pomocą natywnej aplikacji agenta, którą użytkownik instaluje ręcznie. W innych przypadkach aplikacja jest udostępniana w spakowanej aplikacji natywnej za pomocą platformy takiej jak Electron. W innych przypadkach użytkownik musi wykonać dodatkową czynność, na przykład skopiować skompilowaną aplikację na urządzenie za pomocą dysku flash USB.

Wygodę użytkowników można zwiększyć, zapewniając bezpośrednią komunikację między witryną a urządzeniem, nad którym ma kontrolę.

3. Przygotowanie

Pobierz kod

Wszystko, czego potrzebujesz do tego ćwiczenia z programowania, umieszczamy w projekcie Glitch.

  1. Otwórz nową kartę przeglądarki i wejdź na stronę https://web-serial-codelab-start.glitch.me/.
  2. Kliknij link Remiksuj zakłócenie, aby utworzyć własną wersję projektu startowego.
  3. Kliknij przycisk Pokaż, a następnie wybierz W nowym oknie, aby zobaczyć, jak działa Twój kod.

4. Otwórz połączenie szeregowe

Sprawdzanie, czy interfejs Web Serial API jest obsługiwany

Najpierw sprawdź, czy obecna przeglądarka obsługuje interfejs Web Serial API. Aby to zrobić, sprawdź, czy serial znajduje się w elemencie navigator.

W zdarzeniu DOMContentLoaded dodaj do projektu ten kod:

script.js - DOMContentLoaded

// CODELAB: Add feature detection here.
const notSupported = document.getElementById('notSupported');
notSupported.classList.toggle('hidden', 'serial' in navigator);

Sprawdza, czy numer seryjny jest obsługiwany. Jeśli tak, ten kod ukrywa baner z informacją, że numer seryjny nie jest obsługiwany.

Wypróbuj

  1. Wczytaj stronę.
  2. Sprawdź, czy na stronie nie wyświetla się czerwony baner z informacją, że numer seryjny nie jest obsługiwany.

Otwórz port szeregowy

Teraz musimy otworzyć port szeregowy. Podobnie jak większość nowoczesnych interfejsów API, interfejs Web Serial API jest asynchroniczny. Zapobiega to blokowaniu interfejsu użytkownika podczas oczekiwania na dane wejściowe, ale jest również ważne, ponieważ strona internetowa może w każdej chwili odbierać dane seryjne, a my musimy mieć możliwość ich wykrywania.

Komputer może mieć wiele urządzeń szeregowych, dlatego gdy przeglądarka próbuje poprosić o port, wyświetla użytkownikowi prośbę o wybranie urządzenia, z którym ma się połączyć.

Dodaj do projektu ten kod:

script.js - connect()

// CODELAB: Add code to request & open port here.
// - Request a port and open a connection.
port = await navigator.serial.requestPort();
// - Wait for the port to open.
await port.open({ baudrate: 9600 });

Wywołanie requestPort informuje użytkownika, z którym urządzeniem chce się połączyć. Połączenie z numerem port.open otworzy port. Musimy też określić szybkość, z jaką chcemy komunikować się z urządzeniem szeregowym. BBC micro:bit korzysta z połączenia o szybkości 9600 budów między układem USB-serowym a głównym procesorem.

Podłączmy też przycisk łączenia, tak aby wywoływał on connect(), gdy użytkownik go kliknie.

Dodaj do projektu ten kod:

script.js - clickConnect()

// CODELAB: Add connect code here.
await connect();

Wypróbuj

Nasz projekt ma teraz absolutne minimum potrzebne do rozpoczęcia pracy. Po kliknięciu przycisku Połącz użytkownik musi wybrać urządzenie szeregowe, z którym ma się połączyć, a następnie łączy się z urządzeniem micro:bit.

  1. Odśwież stronę.
  2. Kliknij przycisk Połącz.
  3. W oknie wyboru portu szeregowego wybierz urządzenie BBC micro:bit i kliknij Połącz.
  4. Na karcie powinna być widoczna ikona wskazująca, że nawiązano połączenie z urządzeniem szeregowym:

d9d0d3966960aeab.png

Konfigurowanie strumienia wejściowego, aby nasłuchiwać danych z portu szeregowego

Po nawiązaniu połączenia trzeba skonfigurować strumień wejściowy i czytnik do odczytywania danych z urządzenia. Najpierw pobieramy możliwy do odczytania strumień z portu, wywołując funkcję port.readable. Wiemy, że otrzymasz SMS-a z urządzenia, dlatego przekażemy go za pomocą dekodera tekstu. Teraz weźmiemy czytnik i rozpoczniemy pętlę odczytu.

Dodaj do projektu ten kod:

script.js - connect()

// CODELAB: Add code to read the stream here.
let decoder = new TextDecoderStream();
inputDone = port.readable.pipeTo(decoder.writable);
inputStream = decoder.readable;

reader = inputStream.getReader();
readLoop();

Pętla odczytu to funkcja asynchroniczna działająca w pętli i oczekująca na treść bez blokowania wątku głównego. Po otrzymaniu nowych danych czytnik zwraca 2 właściwości: value i done. Jeśli done ma wartość true (prawda), port został zamknięty lub nie napływają już żadne dane.

Dodaj do projektu ten kod:

script.js - readLoop()

// CODELAB: Add read loop here.
while (true) {
  const { value, done } = await reader.read();
  if (value) {
    log.textContent += value + '\n';
  }
  if (done) {
    console.log('[readLoop] DONE', done);
    reader.releaseLock();
    break;
  }
}

Wypróbuj

Nasz projekt może teraz połączyć się z urządzeniem i dołączyć do elementu dziennika wszystkie dane otrzymane z urządzenia.

  1. Odśwież stronę.
  2. Kliknij przycisk Połącz.
  3. W oknie wyboru portu szeregowego wybierz urządzenie BBC micro:bit i kliknij Połącz.
  4. Powinno pojawić się logo Espruino:

93494fd58ea835eb.png

Konfigurowanie strumienia wyjściowego do wysyłania danych na port szeregowy

Komunikacja szeregowa zazwyczaj odbywa się dwukierunkowa. Oprócz odbierania danych z portu szeregowego chcemy też wysyłać do niego dane. Tak jak w przypadku strumienia wejściowego, tekst w strumieniu wyjściowym będziemy wysyłać tylko do mikro:bita.

Najpierw utwórz strumień kodera tekstu i prześlij go za pomocą potoku do port.writeable.

script.js - connect()

// CODELAB: Add code setup the output stream here.
const encoder = new TextEncoderStream();
outputDone = encoder.readable.pipeTo(port.writable);
outputStream = encoder.writable;

Po połączeniu szeregowym z oprogramowaniem Espruino tablica mikro:bitowa BBC działa jak pętla do odczytu i ewaluacji druku (REPL) JavaScriptu, podobnie jak w powłoce Node.js. Następnie musimy udostępnić metodę wysyłania danych do strumienia. Poniższy kod pobiera ze strumienia wyjściowego zapisujący, a następnie za pomocą write wysyła poszczególne wiersze. Każdy wysyłany wiersz zawiera znak nowego wiersza (\n), który informuje funkcję micro:bit o ocenę wysłanego polecenia.

script.js - writeToStream()

// CODELAB: Write to output stream
const writer = outputStream.getWriter();
lines.forEach((line) => {
  console.log('[SEND]', line);
  writer.write(line + '\n');
});
writer.releaseLock();

Aby przywrócić system do znanego stanu i uniknąć powtarzania wysyłanych przez nas znaków, musimy wysłać kombinację klawiszy CTRL+C i wyłączyć echo.

script.js - connect()

// CODELAB: Send CTRL-C and turn off echo on REPL
writeToStream('\x03', 'echo(false);');

Wypróbuj

Nasz projekt może teraz wysyłać i odbierać dane z mikro:bitu. Sprawdźmy, czy możemy prawidłowo wysłać polecenie:

  1. Odśwież stronę.
  2. Kliknij przycisk Połącz.
  3. W oknie wyboru portu szeregowego wybierz urządzenie BBC micro:bit i kliknij Połącz.
  4. Otwórz kartę Konsola w Narzędziach deweloperskich w Chrome i wpisz writeToStream('console.log("yes")');.

Strona powinna wyglądać tak:

a13187e7e6260f7f.png

5. Steruj matrycą diod LED

Tworzenie ciągu siatki macierzy

Aby sterować matrycą diod LED na mikro:bitzie, trzeba wywołać metodę show(). Ta metoda wyświetla grafikę na wbudowanym ekranie LED 5 x 5. Musi to być liczba binarna lub ciąg znaków.

Przeanalizujemy pola wyboru i wygenerujemy tablicę 1 i 0 wskazującą, które z nich jest zaznaczone, a które nie. Następnie musimy odwrócić tablicę, bo pola wyboru są odwrotne niż kolejność diod LED w macierzy. Następnie konwertujemy tablicę na ciąg znaków i tworzymy polecenie, które zostanie wysłane do mikrodanych.

script.js - sendGrid()

// CODELAB: Generate the grid
const arr = [];
ledCBs.forEach((cb) => {
  arr.push(cb.checked === true ? 1 : 0);
});
writeToStream(`show(0b${arr.reverse().join('')})`);

Wyróżnij pola wyboru, aby zaktualizować macierz.

Następnie musimy wsłuchać się w zmiany w polach wyboru i, jeśli ulegną one zmianie, przesłać informacje do pola micro:bit. W kodzie wykrywania cech (// CODELAB: Add feature detection here.) dodaj następujący wiersz:

script.js - DOMContentLoaded

initCheckboxes();

Zresetujmy też siatkę po pierwszym połączeniu bitu micro:bit, aby wyświetlała wesołą buźkę. Funkcja drawGrid() jest już dostępna. Ta funkcja działa podobnie do funkcji sendGrid(); wybiera tablicę 1 i 0 oraz zaznacza odpowiednie pola wyboru.

script.js - clickConnect()

// CODELAB: Reset the grid on connect here.
drawGrid(GRID_HAPPY);
sendGrid();

Wypróbuj

Teraz gdy strona otworzy połączenie z mikro:bitem, zostanie wysłana radosna buźka. Kliknięcie pól wyboru spowoduje zaktualizowanie stanu na matrycy LED.

  1. Odśwież stronę.
  2. Kliknij przycisk Połącz.
  3. W oknie wyboru portu szeregowego wybierz urządzenie BBC micro:bit i kliknij Połącz.
  4. Na matrycy LED micro:bit powinien pojawić się uśmiech.
  5. Zmieniając pola wyboru, narysuj inny wzór na macierze LED.

6. Podłączanie przycisków micro:bit

Dodawanie wydarzenia związanego z zegarkiem na przyciskach micro:bit

Na płytce micro:bit znajdują się 2 przyciski, po jednym po obu stronach matrycy LED. Espruino udostępnia funkcję setWatch, która wysyła zdarzenie/wywołanie zwrotne po naciśnięciu przycisku. Chcemy wsłuchać się w oba przyciski, więc potraktujemy funkcję jako ogólną i poprosimy o wydrukowanie szczegółów zdarzenia.

script.js - watchButton()

// CODELAB: Hook up the micro:bit buttons to print a string.
const cmd = `
  setWatch(function(e) {
    print('{"button": "${btnId}", "pressed": ' + e.state + '}');
  }, ${btnId}, {repeat:true, debounce:20, edge:"both"});
`;
writeToStream(cmd);

Następnie musimy podłączyć oba przyciski (o nazwach BTN1 i BTN2 na płytce micro:bit) za każdym razem, gdy port szeregowy jest podłączony do urządzenia.

script.js - clickConnect()

// CODELAB: Initialize micro:bit buttons.
watchButton('BTN1');
watchButton('BTN2');

Wypróbuj

Naciśnięcie dowolnego z przycisków na mikro:bitzie nie tylko wyświetla wesołą buźkę po połączeniu, ale powoduje dodanie na stronie tekstu informującego o tym, jaki przycisk został naciśnięty. Najprawdopodobniej każdy znak będzie w osobnym wierszu.

  1. Odśwież stronę.
  2. Kliknij przycisk Połącz.
  3. W oknie wyboru portu szeregowego wybierz urządzenie BBC micro:bit i kliknij Połącz.
  4. Na macierze LED micro:bits powinien pojawić się uśmiech.
  5. Naciśnij przyciski na kluczu micro:bit i sprawdź, czy został dodany do strony nowy tekst ze szczegółowymi informacjami o naciśniętym przycisku.

7. Używanie strumienia przekształcenia do analizowania przychodzących danych

Podstawowa obsługa strumieni

Po naciśnięciu jednego z przycisków micro:bit przycisk micro:bit wysyła dane do portu szeregowego przez strumień. Strumienie są bardzo przydatne, ale mogą też stanowić wyzwanie, ponieważ nie zawsze można pobrać wszystkie dane naraz, a dane mogą być losowo podzielone na fragmenty.

Aplikacja drukuje przychodzący strumień, gdy tylko przyjdzie (readLoop). W większości przypadków każdy znak jest w osobnym wierszu, ale nie jest to zbyt pomocne. Najlepiej, gdyby strumień był rozpatrywany w osobnych wierszach, a każdy z nich był wyświetlany w osobnym wierszu.

Przekształcanie strumieni za pomocą TransformStream

Możemy do tego użyć strumienia przekształcenia ( TransformStream), który umożliwia analizowanie przychodzącego strumienia i zwracanie przeanalizowanych danych. Strumień przekształcenia może znajdować się między źródłem (w tym przypadku mikro:bitem), a wszystkim, co pobiera strumień (w tym przypadku readLoop), i może zastosować dowolne przekształcenie, zanim zostanie ono przetworzone. Pomyśl o nim jak o linii montażowej: każdy widżet w tym wierszu modyfikuje widżet w taki sposób, że zanim dotrze on do miejsca docelowego, stanie się on w pełni działającym widżetem.

Więcej informacji znajdziesz w artykule Pojęcia związane z interfejsem Streams API w MDN.

Przekształcanie strumienia za pomocą narzędzia LineBreakTransformer

Utwórzmy klasę LineBreakTransformer, która rozpocznie strumień i podzieli go na segmenty według podziałów wiersza (\r\n). Klasa potrzebuje 2 metod: transform i flush. Metoda transform jest wywoływana za każdym razem, gdy strumień odbiera nowe dane. Może dodać dane do kolejki lub zapisać je na później. Metoda flush jest wywoływana po zamknięciu strumienia i obsługuje wszystkie dane, które nie zostały jeszcze przetworzone.

Za pomocą metody transform dodamy nowe dane do tabeli container, a następnie sprawdzimy, czy w tabeli container występują jakieś podziały wierszy. Jeśli tak, podziel go na tablicę, a następnie iteruj kolejne wiersze, wywołując funkcję controller.enqueue() w celu wysłania przeanalizowanych wierszy.

script.js - LineBreakTransformer.transform()

// CODELAB: Handle incoming chunk
this.container += chunk;
const lines = this.container.split('\r\n');
this.container = lines.pop();
lines.forEach(line => controller.enqueue(line));

Po zamknięciu strumienia usuniemy pozostałe dane z kontenera za pomocą funkcji enqueue.

script.js - LineBreakTransformer.flush()

// CODELAB: Flush the stream.
controller.enqueue(this.container);

Na koniec trzeba przekierować strumień przychodzący przez nowe urządzenie LineBreakTransformer. Pierwotny strumień danych wejściowych był przesyłany tylko przez TextDecoderStream, dlatego musimy dodać dodatkowy strumień danych pipeThrough, aby przekazać go za pomocą nowego potoku LineBreakTransformer.

script.js - connect()

// CODELAB: Add code to read the stream here.
let decoder = new TextDecoderStream();
inputDone = port.readable.pipeTo(decoder.writable);
inputStream = decoder.readable
  .pipeThrough(new TransformStream(new LineBreakTransformer()));

Wypróbuj

Teraz po naciśnięciu jednego z przycisków micro:bit wydrukowane dane powinny się wyświetlić w jednej linii.

  1. Odśwież stronę.
  2. Kliknij przycisk Połącz.
  3. W oknie wyboru portu szeregowego wybierz urządzenie BBC micro:bit i kliknij Połącz.
  4. Na matrycy LED micro:bit powinien pojawić się uśmiech.
  5. Naciśnij przyciski na mikro:bit i sprawdź, czy wyświetla się coś takiego:

6c2193880c748412.png

Przekształcanie strumienia za pomocą narzędzia JSONTransformer

Mogliśmy spróbować przeanalizować ciąg znaków w formacie JSON w readLoop, ale zamiast tego utwórzmy bardzo prosty transformer JSON, który przekształci dane w obiekt JSON. Jeśli dane nie są prawidłowe w formacie JSON, zwróć po prostu te dane.

script.js - JSONTransformer.transform

// CODELAB: Attempt to parse JSON content
try {
  controller.enqueue(JSON.parse(chunk));
} catch (e) {
  controller.enqueue(chunk);
}

Następnie przestaw potok przez JSONTransformer, przechodząc przez LineBreakTransformer. Pozwala to zachować prostotę JSONTransformer, ponieważ wiemy, że plik JSON zostanie kiedykolwiek wysłany tylko w jednym wierszu.

script.js - connect

// CODELAB: Add code to read the stream here.
let decoder = new TextDecoderStream();
inputDone = port.readable.pipeTo(decoder.writable);
inputStream = decoder.readable
  .pipeThrough(new TransformStream(new LineBreakTransformer()))
  .pipeThrough(new TransformStream(new JSONTransformer()));

Wypróbuj

Teraz po naciśnięciu jednego z przycisków micro:bit na stronie powinien się wyświetlić kod [object Object].

  1. Odśwież stronę.
  2. Kliknij przycisk Połącz.
  3. W oknie wyboru portu szeregowego wybierz urządzenie BBC micro:bit i kliknij Połącz.
  4. Na matrycy LED micro:bit powinien pojawić się uśmiech.
  5. Naciśnij przyciski na kluczu micro:bit i sprawdź, czy wyświetla się coś takiego:

Reagowanie na naciśnięcia przycisków

Aby reagować na naciśnięcia przycisków micro:bit, zaktualizuj readLoop i sprawdź, czy otrzymane dane to object z właściwością button. Następnie wywołaj buttonPushed, by obsłużyć naciśnięcie przycisku.

script.js - readLoop()

const { value, done } = await reader.read();
if (value && value.button) {
  buttonPushed(value);
} else {
  log.textContent += value + '\n';
}

Po naciśnięciu przycisku micro:bita ekran matrycy LED powinien się zmienić. Użyj tego kodu, aby ustawić macierz:

script.js - buttonPushed()

// CODELAB: micro:bit button press handler
if (butEvt.button === 'BTN1') {
  divLeftBut.classList.toggle('pressed', butEvt.pressed);
  if (butEvt.pressed) {
    drawGrid(GRID_HAPPY);
    sendGrid();
  }
  return;
}
if (butEvt.button === 'BTN2') {
  divRightBut.classList.toggle('pressed', butEvt.pressed);
  if (butEvt.pressed) {
    drawGrid(GRID_SAD);
    sendGrid();
  }
}

Wypróbuj

Teraz po naciśnięciu jednego z przycisków micro:bit matryca diod LED powinna zmienić się na wesołą lub smutną.

  1. Odśwież stronę.
  2. Kliknij przycisk Połącz.
  3. W oknie wyboru portu szeregowego wybierz urządzenie BBC micro:bit i kliknij Połącz.
  4. Na macierze LED micro:bits powinien pojawić się uśmiech.
  5. Naciśnij przyciski na kluczu micro:bit i sprawdź, czy zmieniła się matryca diod LED.

8. Zamykanie portu szeregowego

Ostatnim krokiem jest podłączenie funkcji odłączania, aby zamknąć port, gdy użytkownik zakończy pracę.

Zamykaj port, gdy użytkownik kliknie przycisk Połącz/Rozłącz

Gdy użytkownik kliknie przycisk Połącz/Rozłącz, musimy zamknąć połączenie. Jeśli port jest już otwarty, wywołaj disconnect() i zaktualizuj UI, by wskazać, że strona nie jest już połączona z urządzeniem szeregowym.

script.js - clickConnect()

// CODELAB: Add disconnect code here.
if (port) {
  await disconnect();
  toggleUIConnected(false);
  return;
}

Zamknij strumienie i port

W funkcji disconnect musimy zamknąć strumień wejściowy oraz strumień wyjściowy i port. Aby zamknąć strumień danych wejściowych, wywołaj reader.cancel(). Wywołanie funkcji cancel jest asynchroniczne, więc musimy użyć polecenia await, aby poczekać na jego zakończenie:

script.js - disconnect()

// CODELAB: Close the input stream (reader).
if (reader) {
  await reader.cancel();
  await inputDone.catch(() => {});
  reader = null;
  inputDone = null;
}

Aby zamknąć strumień wyjściowy, pobierz żądanie writer, wywołaj close() i zaczekaj na zamknięcie obiektu outputDone:

script.js - disconnect()

// CODELAB: Close the output stream.
if (outputStream) {
  await outputStream.getWriter().close();
  await outputDone;
  outputStream = null;
  outputDone = null;
}

Na koniec zamknij port szeregowy i poczekaj na jego zamknięcie:

script.js - disconnect()

// CODELAB: Close the port.
await port.close();
port = null;

Wypróbuj

Teraz możesz w dowolny sposób otwierać i zamykać port szeregowy.

  1. Odśwież stronę.
  2. Kliknij przycisk Połącz.
  3. W oknie wyboru portu szeregowego wybierz urządzenie BBC micro:bit i kliknij Połącz.
  4. Na matrycy LED micro:bit powinien pojawić się uśmiech
  5. Naciśnij przycisk Odłącz i sprawdź, czy matryca LED wyłącza się i nie występują żadne błędy w konsoli.

9. Gratulacje

Gratulacje! Udało Ci się utworzyć pierwszą aplikację internetową, która korzysta z interfejsu Web Serial API.

Na stronie https://goo.gle/fugu-api-tracker znajdziesz najnowsze informacje o interfejsie Web Serial API i innych, ciekawych funkcjach internetowych, nad którymi pracuje zespół Chrome.