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).

Te niestandaryzowane urządzenia USB wymagają od dostawców sprzętu napisania sterowników przeznaczonych dla danej platformy i pakietów SDK, które umożliwią Tobie (deweloperowi) korzystanie z nich. 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ń.

Najważniejsze jest jednak to, że zwiększy to bezpieczeństwo i ułatwi korzystanie z USB, 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. Od razu pojawi się powiadomienie z linkiem do właściwej strony 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, w którym możesz wybrać odpowiednie urządzenie.

Tada!

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

  1. Zainstaluj aplikację na daną platformę.
  2. Jeśli jest obsługiwany w moim systemie operacyjnym, sprawdź, czy pobrałem odpowiednią wersję.
  3. Zainstaluj to. 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ą uszkodzić komputer. Pamiętaj, że internet jest tak skonstruowany, aby zatrzymywać 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 z czasem odzyskiwane).

Zanim zacznę

W tym artykule zakładamy, że masz podstawową wiedzę na temat działania USB. Jeśli nie, polecam przeczytać artykuł USB w NutShell. Podstawowe informacje 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.

Ostatni okres próbny zakończył 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 połączenie navigator.usb.requestDevice() można wywołać tylko gestem użytkownika, takim jak dotknięcie lub kliknięcie myszy.

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 i interfejsy API. Można ją zdefiniować w nagłówku HTTP lub w atrybucie „allow” elementu 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>

Zacznij kodować

Interfejs WebUSB API w dużym stopniu opiera się na obietnicach JavaScriptu. Jeśli nie znasz tych funkcji, zapoznaj się z tym świetnym samouczkiem dotyczącym obietnic. 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() pobiera wymagany obiekt JavaScript, który definiuje filters. Te filtry służą do dopasowywania dowolnego urządzenia USB do podanych identyfikatorów dostawcy (vendorId) i opcjonalnie produktu (productId). Można tu też zdefiniować klucze classCode, protocolCode, serialNumbersubclassCode.

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

Oto na przykład, jak uzyskać dostęp do połączonego urządzenia Arduino skonfigurowanego w celu zezwolenia 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 spytasz, nie wymyśliłem magicznej liczby szesnastkowej 0x2341. 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 teraz, 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.
  • Interfejs ten udostępnia 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 w usłudze 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 realizowana za pomocą obiektu result zawierającego widok danych DataView data, który musi zostać odpowiednio przeanalizowany.

Jeśli masz doświadczenie w korzystaniu z USB, wszystko to powinno wyglądać znajomo.

Chcę więcej

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

  • Transfery za pomocą funkcji Control, używane do wysyłania lub odbierania parametrów konfiguracyjnych lub poleceń na urządzenie USB, są obsługiwane za pomocą controlTransferIn(setup, length) i controlTransferOut(setup, data).
  • Przerywane transfery danych, używane do przesyłania niewielkiej ilości danych o wysokiej wartości czasowej, 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 komputerze współdzielonym z wieloma urządzeniami, duża liczba gromadzonych uprawnień użytkowników pogarsza 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 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, jak pokazano w przykładzie 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.

Przyda się też strona wewnętrzna about://usb-internals, która pozwala symulować podłączanie i odłączanie wirtualnych urządzeń WebUSB. Jest to przydatne podczas testowania interfejsu użytkownika bez 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 z systemem Linux urządzenia USB są domyślnie mapowane z uprawnieniami tylko do odczytu. Aby umożliwić Chrome otwieranie urządzeń 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. Możesz też dodać ATTR{idProduct}, aby zwiększyć szczegółowość reguły. 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.