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 wspomnisz po prostu „USB”, zapewne od razu przychodzą Ci na myśl klawiatury, myszy, urządzenia audio, wideo i urządzenia pamięci masowej. Zgadza się, ale istnieją inne rodzaje urządzeń 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ń.

Jednak przede wszystkim dzięki temu umożliwi to łatwiejsze korzystanie z USB przez jego udostępnienie w internecie.

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.

Tadam!

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

  1. Zainstaluj aplikację na daną platformę.
  2. Jeśli mój system operacyjny obsługuje tę funkcję, sprawdź, czy pobrałem właściwą 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ą nie działać prawidłowo i 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 miejsce na nieużywane są w końcu odzyskane).

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 przypadku testowania 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

Ze względu na zaawansowane możliwości działa ona tylko w przypadku bezpiecznych kontekstów. Oznacza to, że musisz pamiętać o TLS.

Wymagany gest 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 umożliwia programistom selektywne włączanie i wyłączanie różnych funkcji przeglądarki i interfejsów 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 nagłówka, w której niedozwolony jest protokół WebUSB:

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 tych funkcji, zapoznaj się z tym samouczkiem. Jeszcze jedna rzecz: () => {}to po prostu funkcje strzałki w ECMAScript 2015.

Uzyskaj dostęp 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). Można tu też zdefiniować klucze classCode, protocolCode, serialNumbersubclassCode.

Zrzut ekranu przedstawiający prompt użytkownika urządzenia USB w Chrome
Prośba o podłączenie 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 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

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

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

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, umożliwiając 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 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 musisz wywołać device.controlTransferOut(), aby skonfigurować urządzenie Arduino za pomocą odpowiednich poleceń do komunikacji przez 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 znasz 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 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 przedstawia podstawowy przykład budowania urządzenia LED sterowanego przez USB zaprojektowanego z myślą o interfejsie WebUSB API (w tym przypadku bez wykorzystania Arduino). Znajdziesz tam 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 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 strony dziennika urządzenia służącej 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 połączenia i rozłączenia 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 Linux urządzenia USB są domyślnie mapowane z dostępem 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.