Dostęp do urządzeń USB w internecie

Interfejs WebUSB API sprawia, że USB jest bezpieczniejsze i łatwiejsze w użyciu, ponieważ umożliwia korzystanie z niego w internecie.

François Beaufort
François Beaufort

Jeśli powiem „USB”, prawdopodobnie od razu pomyślisz o klawiaturach, myszach, urządzeniach audio i wideo oraz urządzeniach pamięci masowej. Zgadzasz się, ale na rynku są też inne urządzenia z uniwersalną magistralą szeregową (USB).

Aby można było korzystać z takich niestandardowych urządzeń USB, producenci sprzętu muszą napisać sterowniki i pakiety SDK dla poszczególnych platform. Niestety ten kod specyficzny dla platformy uniemożliwiał korzystanie z tych urządzeń w sieci. I to jest jeden z powodów, dla których powstał interfejs WebUSB API: aby umożliwić udostępnianie usług urządzeń USB w internecie. Dzięki temu interfejsowi API producenci sprzętu będą mogli tworzyć platformy JavaScript SDK na potrzeby swoich urządzeń.

Co najważniejsze, ułatwi to korzystanie z USB i zwiększy bezpieczeństwo tego standardu, ponieważ umożliwi łączenie go z internetem.

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

  1. Kup urządzenie USB.
  2. Podłącz go do komputera. Natychmiast pojawi się powiadomienie z odpowiednią stroną internetową dla tego urządzenia.
  3. Kliknij powiadomienie. Witryna jest gotowa do użycia.
  4. Kliknij, aby połączyć, a w Chrome pojawi się selektor urządzeń USB, za pomocą którego możesz wybrać urządzenie.

Tada!

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

  1. Zainstaluj aplikację przeznaczoną do danej platformy.
  2. Jeśli jest obsługiwany w moim systemie operacyjnym, sprawdź, czy pobrałem odpowiednią wersję.
  3. Zainstaluj urządzenie. Jeśli masz szczęście, nie zobaczysz żadnych ostrzeżeń ani wyskakujących okienek systemu dotyczących instalowania sterowników lub aplikacji z internetu. Jeśli masz pecha, zainstalowane sterowniki lub aplikacje mogą nie działać prawidłowo i uszkodzić komputer. Pamiętaj, że internet jest tak skonstruowany, aby zawierać nieprawidłowo działające strony internetowe.
  4. Jeśli korzystasz z tej funkcji tylko raz, kod pozostaje na komputerze, dopóki nie zdecydujesz się go usunąć. (w internecie nieużywane miejsce jest ostatecznie odzyskiwane).

Zanim zacznę

W tym artykule zakładamy, że masz podstawową wiedzę na temat działania USB. Jeśli nie, przeczytaj artykuł USB w pigułce. Więcej informacji o USB znajdziesz w oficjalnych specyfikacjach USB.

Interfejs WebUSB API jest dostępny w Chrome 61.

Dostępne w wersjach próbnych origin

Aby uzyskać jak najwięcej opinii od programistów korzystających z interfejsu WebUSB, wcześniej dodaliśmy tę funkcję w Chrome 54 i Chrome 57 jako testowanie origin.

Najnowsza wersja próbna zakończyła się we wrześniu 2017 r.

Prywatność i bezpieczeństwo

Tylko HTTPS

Ta funkcja działa tylko w bezpiecznych kontekstach. Oznacza to, że musisz skompilować aplikację z uwzględnieniem protokołu TLS.

Wymagane działanie użytkownika

Ze względów bezpieczeństwa funkcja navigator.usb.requestDevice() może być wywoływana tylko przez użytkownika, np. przez dotknięcie lub kliknięcie myszką.

Zasady dotyczące uprawnień

Zasady dotyczące uprawnień to mechanizm, który pozwala deweloperom selektywnie włączać i wyłączać różne funkcje przeglądarki oraz interfejsy API. Możesz go zdefiniować za pomocą nagłówka HTTP lub atrybutu „allow” w tagu iframe.

Możesz zdefiniować zasady dotyczące uprawnień, które określają, czy atrybut usb jest dostępny w obiekcie Navigator, czyli czy zezwalasz na WebUSB.

Poniżej znajduje się przykład zasady w nagłówku, w której WebUSB jest niedozwolony:

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

Poniżej znajduje się kolejny przykład zasad kontenera, w których dozwolone jest USB:

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

Zacznijmy kodować

Interfejs WebUSB API w dużej mierze korzysta z obietnic w JavaScript. Jeśli nie znasz obietnic, zapoznaj się z tym świetnym samouczkiem. Jeszcze jedna rzecz: () => {}to po prostu funkcje strzałki w ECMAScript 2015.

Uzyskiwanie dostępu do urządzeń USB

Możesz poprosić użytkownika o wybranie jednego podłączonego urządzenia USB za pomocą metody navigator.usb.requestDevice() lub wywołać metodę 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 definiuje filters. Te filtry służą do dopasowywania dowolnego urządzenia USB do identyfikatorów danego dostawcy (vendorId) i opcjonalnie produktów (productId). Tutaj możesz też zdefiniować klucze classCode, protocolCode, serialNumbersubclassCode.

Zrzut ekranu z prośbą do użytkownika o podłączenie urządzenia USB w Chrome
Prośba o podłączenie urządzenia USB

Poniżej znajdziesz informacje o tym, jak uzyskać dostęp do połączonego urządzenia Arduino skonfigurowanego w taki sposób, aby zezwalało na dostęp do źródła.

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 wymyśliłem tego numeru 0x2341 w magiczny sposób. Wystarczyło wyszukać słowo „Arduino” na tej liście identyfikatorów USB.

Urządzenie USB device zwrócone w ramach spełnienia obietnicy zawiera podstawowe, ale ważne informacje o urządzeniu, takie jak obsługiwana wersja USB, maksymalny rozmiar pakietu, identyfikatory dostawcy i produktu oraz liczba możliwych konfiguracji urządzenia. Zasadniczo zawiera wszystkie pola w Descryptory 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"
  });
})

Jeśli urządzenie USB zgłasza obsługę WebUSB, a także definiuje adres URL strony docelowej, Chrome wyświetli trwałe powiadomienie, gdy urządzenie USB jest podłączone. Kliknięcie tego powiadomienia spowoduje otwarcie strony docelowej.

Zrzut ekranu przedstawiający powiadomienie WebUSB w Chrome
Powiadomienie WebUSB

Komunikacja z płytką Arduino USB

Zobaczmy, jak łatwo można komunikować się z płytą Arduino zgodną z WebUSB przez port USB. Aby umożliwić szkicowi korzystanie z WebUSB, zapoznaj się z instrukcjami na stronie https://github.com/webusb/arduino.

Nie martw się, omówię wszystkie metody WebUSB wymienione poniżej w dalszej części tego artykułu.

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 implementuje tylko jeden przykładowy protokół (oparty na standardowym protokole USB) i że producenci mogą tworzyć dowolne zestawy i typy punktów końcowych. Przesyłanie kontroli jest szczególnie przydatne w przypadku małych poleceń konfiguracyjnych, ponieważ ma priorytet i ma dobrze zdefiniowaną strukturę.

A tutaj jest szkic przesłany na płytkę 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.
}

Biblioteka WebUSB Arduino używana w przykładowym kodzie powyżej wykonuje 2 podstawowe czynności:

  • Urządzenie działa jak urządzenie WebUSB, co umożliwia Chrome odczytanie adresu URL strony docelowej.
  • Udostępnia on interfejs WebUSB Serial API, którego możesz użyć do zastąpienia domyślnego interfejsu.

Ponownie spójrz na kod JavaScript. Gdy otrzymam wybrany przez użytkownika device, device.open() wykona wszystkie kroki specyficzne dla danej platformy, aby rozpocząć sesję z urządzeniem USB. Następnie 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 i liczbę interfejsów. Jeśli chodzi o interfejsy, muszę też poprosić o dostęp wyłączny z użyciem interfejsu device.claimInterface(), ponieważ dane można przesyłać do interfejsu lub powiązanych punktów końcowych tylko wtedy, gdy interfejs jest zadeklarowany. Na koniec należy wywołać funkcję device.controlTransferOut(), aby skonfigurować urządzenie Arduino za pomocą odpowiednich poleceń do komunikacji przez interfejs WebUSB Serial API.

Następnie device.transferIn() wykonuje zbiorczy transfer na urządzenie, aby poinformować je, że host jest gotowy do odbioru danych zbiorczych. Następnie obietnica jest wypełniana za pomocą obiektu result zawierającego widok danych DataView data, który musi być odpowiednio przeanalizowany.

Jeśli znasz interfejs USB, wszystko powinno wyglądać znajomo.

Chcę więcej

Interfejs WebUSB API umożliwia interakcję ze wszystkimi typami transferu/punktów końcowych USB:

  • Przesyłanie danych sterujących służy do wysyłania i odbierania parametrów konfiguracji lub poleceń do urządzenia USB. Jest obsługiwane przez controlTransferIn(setup, length)controlTransferOut(setup, data).
  • Przerywane transfery danych, używane do przesyłania niewielkiej ilości danych o charakterze czasochłonnym, są obsługiwane za pomocą tych samych metod co transfery zbiorcze z użyciem parametrów transferIn(endpointNumber, length)transferOut(endpointNumber, data).
  • Przesyłanie ISOCHRONICZNE, używane do strumieni danych, takich jak wideo i dźwięk, jest obsługiwane za pomocą funkcji isochronousTransferIn(endpointNumber, packetLengths) i isochronousTransferOut(endpointNumber, data, packetLengths).
  • Przesyłanie zbiorcze służy do niezawodnego przesyłania dużych ilości danych, które nie są pilne. Do tego celu służą funkcje transferIn(endpointNumber, length) i transferOut(endpointNumber, data).

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

Odwoływanie dostępu do urządzenia USB

Witryna może usunąć uprawnienia do dostępu do urządzenia USB, którego już nie potrzebuje, wywołując funkcję forget() w instancji USBDevice. Na przykład w przypadku edukacyjnej aplikacji internetowej używanej na współdzielonym komputerze z wiele urządzeniami duża liczba gromadzonych uprawnień generowanych przez użytkowników może pogorszyć wrażenia użytkowników.

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

Funkcja forget() jest dostępna w Chrome w wersji 101 lub nowszej. Sprawdź, czy jest obsługiwana w przypadku tych urządzeń:

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

Limity rozmiaru przesyłanych danych

Niektóre systemy operacyjne nakładają limity na ilość danych, które mogą być częścią oczekujących transakcji USB. Aby uniknąć tych ograniczeń, podziel dane na mniejsze transakcje i przesyłaj tylko kilka z nich naraz. Zmniejsza też ilość używanej pamięci i pozwala aplikacji zgłaszać postępy w przenoszeniu danych.

Ponieważ wiele przesyłanych do punktu końcowego transferów jest zawsze wykonywanych w kolejności, można zwiększyć przepustowość, przesyłając wiele fragmentów z kolejki, aby uniknąć opóźnień między transferami przez USB. Za każdym razem, gdy fragment zostanie w pełni przesłany, kod zostanie poinformowany, że powinien przekazać więcej danych. W tym celu możesz użyć przykładu funkcji pomocniczej poniżej.

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 możesz zobaczyć wszystkie zdarzenia związane z urządzeniami USB w jednym miejscu.

Zrzut ekranu przedstawiający stronę dziennika urządzenia do debugowania WebUSB w Chrome
Strona dziennika urządzenia w Chrome do debugowania interfejsu WebUSB API.

Przydatna jest też strona wewnętrzna about://usb-internals, która umożliwia symulowanie łączenia i rozłączania urządzeń WebUSB. Jest to przydatne podczas testowania interfejsu użytkownika bez potrzeby korzystania z prawdziwego sprzętu.

Zrzut ekranu z wewnętrzną stroną do debugowania WebUSB w Chrome
Wewnętrzna strona w Chrome do debugowania interfejsu WebUSB API.

W większości systemów Linux urządzenia USB są domyślnie mapowane z dostępem tylko do odczytu. Aby umożliwić Chrome otwieranie urządzenia USB, musisz dodać nową regułę udev. Utwórz plik w folderze /etc/udev/rules.d/50-yourdevicename.rules z tą treścią:

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

gdzie [yourdevicevendor] to 2341, jeśli Twoje urządzenie to na przykład Arduino. ATTR{idProduct} można też dodać, aby utworzyć bardziej szczegółową regułę. Upewnij się, że konto user jest członkiem grupy plugdev. Następnie ponownie połącz urządzenie.

Zasoby

Wyślij tweeta do @ChromiumDev, używając hashtaga #WebUSB, i podaj, gdzie i jak go używasz.

Podziękowania

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