Dostęp do urządzeń USB w internecie

Interfejs WebUSB API zapewnia bezpieczniejsze i łatwiejsze korzystanie z USB dzięki rozpowszechnianiu go w internecie.

François Beaufort
François Beaufort

Jeśli powiedziałem prosto i po prostu „USB”, jest duża szansa, że od razu przypomnisz Ci klawiatury, myszy, urządzenia audio, wideo i urządzenia pamięci masowej. Masz rację, ale znajdziesz tu inne urządzenia USB.

W przypadku tych niestandardowych urządzeń USB dostawcy sprzętu muszą napisać sterowniki i pakiety SDK przeznaczone na daną platformę, aby można było z nich korzystać (deweloper). Niestety, kod związany z konkretną platformą w przeszłości uniemożliwiał wykorzystanie tych urządzeń w internecie. To jeden z powodów stworzenia interfejsu WebUSB API: zapewniania sposobu udostępniania usług związanych z urządzeniami USB w internecie. Dzięki niemu producenci sprzętu będą mogli tworzyć na swoich urządzeniach wieloplatformowe pakiety SDK JavaScript.

Przede wszystkim jednak zwiększy to bezpieczeństwo i ułatwienie korzystania z USB dzięki wprowadzeniu go do internetu.

Oto zachowanie, którego możesz się spodziewać w przypadku interfejsu WebUSB API:

  1. Kup urządzenie USB.
  2. Podłącz go do komputera. Powiadomienie pojawia się od razu i zawiera właściwą stronę internetową dla danego urządzenia.
  3. Kliknij powiadomienie. Strona jest już dostępna. Możesz z niej korzystać.
  4. Kliknij, aby połączyć, a w Chrome pojawi się wybór urządzenia USB, w którym możesz wybrać urządzenie.

Tada!

Jak wyglądałaby ta procedura bez korzystania z interfejsu WebUSB API?

  1. Zainstaluj aplikację odpowiednią dla danej platformy.
  2. Jeśli ten format jest obsługiwany w moim systemie operacyjnym, sprawdź, czy mam pobrany plik w odpowiednim miejscu.
  3. Zainstaluj je. Jeśli przyniesiesz szczęście, nie zobaczysz żadnych przerażających komunikatów ani wyskakujących okienek z ostrzeżeniem o instalowaniu sterowników/aplikacji z internetu. Jeśli nic się nie uda, zainstalowane sterowniki lub aplikacje mogą uszkodzić komputer. (Pamiętaj, że internet jest stworzony po to, by zawierać uszkodzone witryny).
  4. Jeśli zastosujesz tę funkcję tylko raz, kod pozostanie na komputerze, dopóki nie pomyślisz, że trzeba go usunąć. (W przypadku witryn niewykorzystane miejsce zostaje z czasem zajęte).

Zanim zacznę

W tym artykule zakładamy, że masz podstawową wiedzę na temat działania USB. Jeśli nie, zalecamy przeczytanie kodu USB w aplikacji NutShell. Podstawowe informacje o USB znajdziesz w oficjalnych specyfikacjach USB.

Interfejs WebUSB API jest dostępny w Chrome 61.

Dostępne w przypadku testowania origin

Aby uzyskać w tej dziedzinie jak najwięcej opinii od deweloperów korzystających z interfejsu WebUSB API, tę funkcję dodaliśmy wcześniej w Chrome 54 i Chrome 57 w ramach wersji próbnej origin.

Ostatni okres próbny zakończył się we wrześniu 2017 r.

Prywatność i bezpieczeństwo

Tylko HTTPS

Ze względu na potęgę tej funkcji działa ona tylko w bezpiecznych kontekstach. Oznacza to, że musisz pamiętać o TLS.

Wymagany gest użytkownika

Ze względów bezpieczeństwa nazwę navigator.usb.requestDevice() można wywoływać tylko gestami użytkownika, np. dotknięciem lub kliknięciem myszy.

Zasady dotyczące uprawnień

Zasady dotyczące uprawnień to mechanizm, który umożliwia programistom selektywne włączanie i wyłączanie różnych funkcji przeglądarek i interfejsów API. Można ją zdefiniować za pomocą nagłówka HTTP lub atrybutu „allow” w elemencie iframe.

Możesz zdefiniować zasadę uprawnień, która określa, czy atrybut usb jest udostępniany w obiekcie Navigator, lub tzn. jeśli zezwalasz na WebUSB.

Poniżej znajduje się przykład zasady nagłówka, w której zasady WebUSB są niedozwolone:

Feature-Policy: fullscreen "*"; usb "none"; payment "self" https://payment.example.com

Poniżej znajduje się inny przykład zasad dotyczących kontenerów, w których korzystanie z USB jest dozwolone:

<iframe allowpaymentrequest allow="usb; fullscreen"></iframe>

Zacznij kodować

Interfejs WebUSB API w dużym stopniu opiera się na obietnicach JavaScriptu. Jeśli nie znasz jeszcze tych funkcji, zapoznaj się z samouczkiem o obietnicach. I jeszcze () => {} to po prostu funkcje strzałek w standardzie ECMAScript 2015.

Uzyskiwanie dostępu do urządzeń USB

Możesz poprosić użytkownika o wybranie pojedynczego podłączonego urządzenia USB za pomocą funkcji navigator.usb.requestDevice() lub wywołanie navigator.usb.getDevices(), aby uzyskać listę wszystkich podłączonych urządzeń USB, do których witryna ma dostęp.

Funkcja navigator.usb.requestDevice() przyjmuje obowiązkowy obiekt JavaScript, który określa filters. Te filtry pozwalają dopasować dowolne urządzenie USB do podanego identyfikatora dostawcy (vendorId) i opcjonalnie identyfikatora produktu (productId). Klucze classCode, protocolCode, serialNumber i subclassCode też mogą być zdefiniowane.

Zrzut ekranu przedstawiający komunikat użytkownika urządzenia USB w Chrome
Prompt użytkownika urządzenia USB.

Tutaj dowiesz się na przykład, jak uzyskać dostęp do podłączonego urządzenia Arduino, które zostało skonfigurowane tak, aby zezwalało na źródło.

navigator.usb.requestDevice({ filters: [{ vendorId: 0x2341 }] })
.then(device => {
  console.log(device.productName);      // "Arduino Micro"
  console.log(device.manufacturerName); // "Arduino LLC"
})
.catch(error => { console.error(error); });

Zanim zapytasz, nie udało mi się w magiczny sposób wymyślić tej liczby szesnastkowej 0x2341. Na liście identyfikatorów USB wyszukiwałem tylko słowo „Arduino”.

USB device zwrócone w ramach spełnianej obietnicy powyżej zawiera podstawowe, ale ważne informacje o urządzeniu, takie jak obsługiwana wersja USB, maksymalny rozmiar pakietu, dostawca i identyfikatory produktu oraz liczba możliwych konfiguracji urządzenia. To zasadniczo zawiera wszystkie pola deskryptora USB urządzenia.

// Get all connected USB devices the website has been granted access to.
navigator.usb.getDevices().then(devices => {
  devices.forEach(device => {
    console.log(device.productName);      // "Arduino Micro"
    console.log(device.manufacturerName); // "Arduino LLC"
  });
})

Przy okazji – jeśli urządzenie USB ogłosi obsługę WebUSB i określi adres URL strony docelowej, Chrome będzie wyświetlać stałe powiadomienie po podłączeniu urządzenia USB. Kliknięcie tego powiadomienia spowoduje otwarcie strony docelowej.

Zrzut ekranu z powiadomieniem WebUSB w Chrome
Powiadomienie WebUSB.

Mów do płyty USB Arduino

OK. A teraz zobaczmy, jak łatwo można komunikować się przez port USB z płyty Arduino zgodnej z WebUSB. Aby włączyć WebUSB w swoich szkicach, zapoznaj się z instrukcjami na stronie https://github.com/webusb/arduino.

Bez obaw. W dalszej części tego artykułu omówię wszystkie metody z urządzenia WebUSB wymienione poniżej.

let device;

navigator.usb.requestDevice({ filters: [{ vendorId: 0x2341 }] })
.then(selectedDevice => {
    device = selectedDevice;
    return device.open(); // Begin a session.
  })
.then(() => device.selectConfiguration(1)) // Select configuration #1 for the device.
.then(() => device.claimInterface(2)) // Request exclusive control over interface #2.
.then(() => device.controlTransferOut({
    requestType: 'class',
    recipient: 'interface',
    request: 0x22,
    value: 0x01,
    index: 0x02})) // Ready to receive data
.then(() => device.transferIn(5, 64)) // Waiting for 64 bytes of data from endpoint #5.
.then(result => {
  const decoder = new TextDecoder();
  console.log('Received: ' + decoder.decode(result.data));
})
.catch(error => { console.error(error); });

Pamiętaj, że używana przeze mnie biblioteka WebUSB po prostu implementuje jeden przykładowy protokół (oparty na standardowym protokole szeregowym USB), a producenci mogą tworzyć dowolne zestawy i typy punktów końcowych. Transfery kontrolne są szczególnie przydatne w przypadku małych poleceń konfiguracyjnych, ponieważ otrzymują priorytet magistrali i mają dobrze zdefiniowaną strukturę.

Oto szkic, który został przesłany na tablicę Arduino.

// Third-party WebUSB Arduino library
#include <WebUSB.h>

WebUSB WebUSBSerial(1 /* https:// */, "webusb.github.io/arduino/demos");

#define Serial WebUSBSerial

void setup() {
  Serial.begin(9600);
  while (!Serial) {
    ; // Wait for serial port to connect.
  }
  Serial.write("WebUSB FTW!");
  Serial.flush();
}

void loop() {
  // Nothing here for now.
}

Zewnętrzna biblioteka WebUSB Arduino użyta w przykładowym kodzie powyżej spełnia zasadniczo 2 działania:

  • Urządzenie działa jak urządzenie WebUSB, umożliwiając Chrome odczytanie adresu URL strony docelowej.
  • Ujawnia on interfejs WebUSB Serial API, którego można użyć do zastąpienia domyślnego.

Ponownie sprawdź kod JavaScript. Gdy użytkownik wybierze device, device.open() wykonuje wszystkie kroki związane z platformą, aby rozpocząć sesję na urządzeniu USB. Potem muszę wybrać dostępną konfigurację USB za pomocą device.selectConfiguration(). Pamiętaj, że konfiguracja określa sposób zasilania urządzenia, jego maksymalne zużycie energii oraz liczbę interfejsów. Jeśli chodzi o interfejs, muszę też poprosić o dostęp na wyłączność w device.claimInterface(), ponieważ dane można przenosić do interfejsu lub powiązanych punktów końcowych tylko po jego zarezerwowaniu. Na koniec trzeba wywołać device.controlTransferOut(), aby skonfigurować urządzenie Arduino za pomocą odpowiednich poleceń do komunikacji z interfejsem WebUSB Serial API.

Następnie device.transferIn() wykonuje przenoszenie zbiorcze na urządzenie, aby poinformować go, że host jest gotowy do odbierania danych zbiorczych. Następnie obietnica jest realizowana przez obiekt result zawierający obiekt data DataView, który należy odpowiednio przeanalizować.

Jeśli znasz USB, wszystkie te funkcje powinny wyglądać znajomo.

Chcę więcej

Interfejs WebUSB API umożliwia obsługę wszystkich typów punktów końcowych i transferu USB:

  • Transfery CONTROL, służące do wysyłania i odbierania parametrów konfiguracyjnych lub poleceń na urządzenie USB, są obsługiwane przez controlTransferIn(setup, length) i controlTransferOut(setup, data).
  • INTERRUPT transfery, które są używane w przypadku niewielkich ilości danych wrażliwych, są obsługiwane tak samo jak w przypadku przesyłania zbiorczego za pomocą transferIn(endpointNumber, length) i transferOut(endpointNumber, data).
  • Przesyłanie ISOCHRONOUSOWE w przypadku strumieni danych, takich jak wideo i dźwięk, jest obsługiwane za pomocą isochronousTransferIn(endpointNumber, packetLengths) i isochronousTransferOut(endpointNumber, data, packetLengths).
  • Zbiorcze transfery, które służą do przesyłania w niezawodny sposób dużej ilości danych, które nie są pilne na czas, są obsługiwane przez transferIn(endpointNumber, length) i transferOut(endpointNumber, data).

Możesz też zapoznać się z projektem WebLight Mike'a Tsao, który przedstawia gruntowny przykład stworzenia sterowanego przez USB urządzenia LED przeznaczonego do WebUSB API (nie używając tutaj Arduino). Znajdziesz tu sprzęt, oprogramowanie i oprogramowanie układowe.

Unieważnij dostęp do urządzenia USB

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

// Voluntarily revoke access to this USB device.
await device.forget();

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

if ("usb" in navigator && "forget" in USBDevice.prototype) {
  // forget() is supported.
}

Limity rozmiaru transferu

Niektóre systemy operacyjne nakładają limity na ilość danych, które mogą być uwzględniane w oczekujących transakcjach na USB. Podzielenie danych na mniejsze transakcje i przesłanie ich tylko po kilka naraz pomaga uniknąć tych ograniczeń. Zmniejsza też ilość używanej pamięci i umożliwia aplikacji raportowanie postępów w trakcie transferu.

Wiele transferów przesłanych do punktu końcowego jest zawsze wykonywane w określonej kolejności, dlatego można zwiększyć przepustowość przez przesyłanie wielu fragmentów w kolejce, aby uniknąć opóźnień między transferami USB. Za każdym razem, gdy fragment zostanie w pełni przesłany, kod otrzyma powiadomienie, że powinien dostarczyć więcej danych, co widać w poniższym przykładzie funkcji pomocniczej.

const BULK_TRANSFER_SIZE = 16 * 1024; // 16KB
const MAX_NUMBER_TRANSFERS = 3;

async function sendRawPayload(device, endpointNumber, data) {
  let i = 0;
  let pendingTransfers = [];
  let remainingBytes = data.byteLength;
  while (remainingBytes > 0) {
    const chunk = data.subarray(
      i * BULK_TRANSFER_SIZE,
      (i + 1) * BULK_TRANSFER_SIZE
    );
    // If we've reached max number of transfers, let's wait.
    if (pendingTransfers.length == MAX_NUMBER_TRANSFERS) {
      await pendingTransfers.shift();
    }
    // Submit transfers that will be executed in order.
    pendingTransfers.push(device.transferOut(endpointNumber, chunk));
    remainingBytes -= chunk.byteLength;
    i++;
  }
  // And wait for last remaining transfers to complete.
  await Promise.all(pendingTransfers);
}

Wskazówki

Debugowanie USB w Chrome jest łatwiejsze dzięki wewnętrznej stronie about://device-log, na której znajdziesz wszystkie zdarzenia związane z urządzeniami USB w jednym miejscu.

Zrzut ekranu strony dziennika urządzenia, na której można debugować WebUSB w Chrome
Strona dziennika urządzenia w Chrome służąca do debugowania interfejsu WebUSB API.

Przyda się też strona wewnętrzna about://usb-internals, która umożliwia symulowanie podłączania i rozłączania wirtualnych urządzeń WebUSB. Przydaje się to do testowania interfejsu użytkownika bez fizycznego sprzętu.

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

W większości systemów z systemem Linux urządzenia USB są domyślnie mapowane z uprawnieniami tylko do odczytu. Aby zezwolić Chrome na otwieranie urządzenia USB, musisz dodać nową regułę udev. W /etc/udev/rules.d/50-yourdevicename.rules utwórz plik z tą zawartością:

SUBSYSTEM=="usb", ATTR{idVendor}=="[yourdevicevendor]", MODE="0664", GROUP="plugdev"

gdzie [yourdevicevendor] to 2341, jeśli masz np. urządzenie Arduino. Można też dodać regułę ATTR{idProduct} dla bardziej szczegółowej reguły. Upewnij się, że user jest członkiem grupy plugdev. Potem po prostu ponownie podłącz urządzenie.

Zasoby

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

Podziękowania

Dziękujemy Joe Medley za przeczytanie tego artykułu.