Toegang tot USB-apparaten op internet

De WebUSB API maakt USB veiliger en gemakkelijker te gebruiken door het naar internet te brengen.

François Beaufort
François Beaufort

Als ik duidelijk en simpel "USB" zou zeggen, is de kans groot dat je meteen aan toetsenborden, muizen, audio-, video- en opslagapparaten denkt. Je hebt gelijk, maar er zijn ook andere soorten Universal Serial Bus (USB)-apparaten verkrijgbaar.

Deze niet-gestandaardiseerde USB-apparaten vereisen dat hardwareleveranciers platformspecifieke stuurprogramma's en SDK's schrijven, zodat u (de ontwikkelaar) hiervan kunt profiteren. Helaas heeft deze platformspecifieke code er historisch gezien voor gezorgd dat deze apparaten niet door het internet konden worden gebruikt. En dat is een van de redenen waarom de WebUSB API is gemaakt: om een ​​manier te bieden om USB-apparaatservices beschikbaar te maken voor het web. Met deze API kunnen hardwarefabrikanten cross-platform JavaScript SDK's voor hun apparaten bouwen.

Maar het allerbelangrijkste is dat USB hierdoor veiliger en gebruiksvriendelijker wordt doordat het op internet wordt aangeboden .

Laten we eens kijken welk gedrag u kunt verwachten met de WebUSB API:

  1. Koop een USB-apparaat.
  2. Sluit hem aan op uw computer. Er verschijnt meteen een melding met de juiste website voor dit toestel.
  3. Klik op de melding. De website is er en klaar voor gebruik!
  4. Klik om verbinding te maken en er verschijnt een USB-apparaatkiezer in Chrome waar u uw apparaat kunt kiezen.

Tada!

Hoe zou deze procedure eruit zien zonder de WebUSB API?

  1. Installeer een platformspecifieke applicatie.
  2. Als het zelfs maar wordt ondersteund op mijn besturingssysteem, controleer dan of ik het juiste heb gedownload.
  3. Installeer het ding. Als u geluk heeft, krijgt u geen enge OS-prompts of pop-ups die u waarschuwen voor het installeren van stuurprogramma's/applicaties vanaf internet. Als u pech heeft, werken de geïnstalleerde stuurprogramma's of toepassingen niet goed, waardoor uw computer beschadigd raakt. (Houd er rekening mee dat het internet is gebouwd om slecht functionerende websites te bevatten ).
  4. Als u de functie slechts één keer gebruikt, blijft de code op uw computer totdat u denkt deze te verwijderen. (Op internet wordt de ruimte voor ongebruikte bestanden uiteindelijk teruggewonnen.)

Voordat ik begin

In dit artikel wordt ervan uitgegaan dat u enige basiskennis hebt van hoe USB werkt. Als dat niet het geval is, raad ik aan USB in een NutShell te lezen. Voor achtergrondinformatie over USB bekijk je de officiële USB-specificaties .

De WebUSB-API is beschikbaar in Chrome 61.

Beschikbaar voor herkomstproeven

Om zoveel mogelijk feedback te krijgen van ontwikkelaars die de WebUSB API in het veld gebruiken, hebben we deze functie eerder toegevoegd aan Chrome 54 en Chrome 57 als origin-proefversie .

De laatste proef is in september 2017 met succes afgerond.

Privacy en veiligheid

Alleen HTTPS

Vanwege de kracht van deze functie werkt deze alleen in beveiligde contexten . Dit betekent dat u moet bouwen met TLS in gedachten.

Gebruikersgebaar vereist

Uit veiligheidsoverwegingen mag navigator.usb.requestDevice() alleen worden aangeroepen via een gebruikersgebaar, zoals een aanraking of een muisklik.

Machtigingenbeleid

Een machtigingsbeleid is een mechanisme waarmee ontwikkelaars verschillende browserfuncties en API's selectief kunnen in- en uitschakelen. Het kan worden gedefinieerd via een HTTP-header en/of een iframe-attribuut "allow".

U kunt een machtigingsbeleid definiëren dat bepaalt of het usb attribuut zichtbaar is in het Navigator-object, of met andere woorden, of u WebUSB toestaat.

Hieronder ziet u een voorbeeld van een headerbeleid waarbij WebUSB niet is toegestaan:

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

Hieronder ziet u nog een voorbeeld van een containerbeleid waarbij USB is toegestaan:

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

Laten we beginnen met coderen

De WebUSB API is sterk afhankelijk van JavaScript- beloften . Als je er niet bekend mee bent, bekijk dan deze geweldige Promises-tutorial . Nog een ding: () => {} zijn eenvoudigweg ECMAScript 2015 Arrow-functies .

Krijg toegang tot USB-apparaten

U kunt de gebruiker vragen om één aangesloten USB-apparaat te selecteren met navigator.usb.requestDevice() of navigator.usb.getDevices() aanroepen om een ​​lijst te krijgen van alle aangesloten USB-apparaten waartoe de website toegang heeft gekregen.

De functie navigator.usb.requestDevice() gebruikt een verplicht JavaScript-object dat filters definieert. Deze filters worden gebruikt om elk USB-apparaat te matchen met de opgegeven leveranciers- ( vendorId ) en, optioneel, product- ( productId )-ID's. De sleutels classCode , protocolCode , serialNumber en subclassCode kunnen daar ook worden gedefinieerd.

Schermafbeelding van de gebruikersprompt van het USB-apparaat in Chrome
Gebruikersprompt USB-apparaat.

Hier leest u bijvoorbeeld hoe u toegang krijgt tot een aangesloten Arduino-apparaat dat is geconfigureerd om de oorsprong toe te staan.

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); });

Voordat je het vraagt: ik heb niet op magische wijze dit hexadecimale getal 0x2341 bedacht. Ik zocht eenvoudigweg naar het woord "Arduino" in deze lijst met USB-ID's .

Het USB- device dat wordt geretourneerd in de bovenstaande belofte, bevat enkele fundamentele, maar belangrijke informatie over het apparaat, zoals de ondersteunde USB-versie, maximale pakketgrootte, leverancier- en product-ID's, en het aantal mogelijke configuraties dat het apparaat kan hebben. In principe bevat het alle velden in de USB-descriptor van het apparaat .

// 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"
  });
})

Trouwens, als een USB-apparaat zijn ondersteuning voor WebUSB aankondigt en een URL voor een landingspagina definieert, zal Chrome een permanente melding weergeven wanneer het USB-apparaat is aangesloten. Als u op deze melding klikt, wordt de landingspagina geopend.

Schermafbeelding van de WebUSB-melding in Chrome
WebUSB-melding.

Praat met een Arduino USB-bord

Oké, laten we nu eens kijken hoe gemakkelijk het is om te communiceren vanaf een WebUSB-compatibel Arduino-bord via de USB-poort. Bekijk de instructies op https://github.com/webusb/arduino om uw schetsen via WebUSB mogelijk te maken.

Maak je geen zorgen, ik zal alle hieronder genoemde WebUSB-apparaatmethoden verderop in dit artikel bespreken.

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); });

Houd er rekening mee dat de WebUSB-bibliotheek die ik gebruik slechts één voorbeeldprotocol implementeert (gebaseerd op het standaard USB-seriële protocol) en dat fabrikanten elke gewenste set en typen eindpunten kunnen maken. Besturingsoverdrachten zijn vooral prettig voor kleine configuratieopdrachten, omdat deze busprioriteit krijgen en een goed gedefinieerde structuur hebben.

En hier is de schets die naar het Arduino-bord is geüpload.

// 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.
}

De WebUSB Arduino-bibliotheek van derden die in de bovenstaande voorbeeldcode wordt gebruikt, doet in principe twee dingen:

  • Het apparaat fungeert als een WebUSB-apparaat waardoor Chrome de URL van de bestemmingspagina kan lezen.
  • Het stelt een WebUSB Serial API bloot die u kunt gebruiken om de standaard API te overschrijven.

Kijk opnieuw naar de JavaScript-code. Zodra ik het device door de gebruiker heb uitgekozen, voert device.open() alle platformspecifieke stappen uit om een ​​sessie met het USB-apparaat te starten. Vervolgens moet ik een beschikbare USB-configuratie selecteren met device.selectConfiguration() . Houd er rekening mee dat een configuratie specificeert hoe het apparaat wordt gevoed, het maximale stroomverbruik en het aantal interfaces. Over interfaces gesproken, ik moet ook exclusieve toegang aanvragen met device.claimInterface() omdat gegevens alleen kunnen worden overgedragen naar een interface of bijbehorende eindpunten wanneer de interface wordt geclaimd. Ten slotte is het aanroepen van device.controlTransferOut() nodig om het Arduino-apparaat in te stellen met de juiste opdrachten om te communiceren via de WebUSB Serial API.

Van daaruit voert device.transferIn() een bulkoverdracht uit naar het apparaat om het te informeren dat de host gereed is om bulkgegevens te ontvangen. Vervolgens wordt de belofte vervuld met een result dat DataView- data bevat die op de juiste manier moeten worden geparseerd.

Als u bekend bent met USB, zou dit er allemaal redelijk bekend uit moeten zien.

ik wil meer

Met de WebUSB API kunt u communiceren met alle typen USB-overdracht/eindpunten:

  • CONTROL-overdrachten, die worden gebruikt om configuratie- of opdrachtparameters naar een USB-apparaat te verzenden of te ontvangen, worden afgehandeld met controlTransferIn(setup, length) en controlTransferOut(setup, data) .
  • INTERRUPT-overdrachten, die worden gebruikt voor een kleine hoeveelheid tijdgevoelige gegevens, worden afgehandeld met dezelfde methoden als BULK-overdrachten met transferIn(endpointNumber, length) en transferOut(endpointNumber, data) .
  • ISOCHRONOUS-overdrachten, gebruikt voor gegevensstromen zoals video en geluid, worden afgehandeld met isochronousTransferIn(endpointNumber, packetLengths) en isochronousTransferOut(endpointNumber, data, packetLengths) .
  • BULK-overdrachten, die worden gebruikt om een ​​grote hoeveelheid niet-tijdgevoelige gegevens op een betrouwbare manier over te dragen, worden afgehandeld met transferIn(endpointNumber, length) en transferOut(endpointNumber, data) .

Misschien wil je ook eens kijken naar het WebLight-project van Mike Tsao, dat een basisvoorbeeld biedt van het bouwen van een USB-gestuurd LED-apparaat dat is ontworpen voor de WebUSB API (hier wordt geen Arduino gebruikt). U vindt hardware, software en firmware.

Toegang tot een USB-apparaat intrekken

De website kan de machtigingen voor toegang tot een USB-apparaat dat het niet langer nodig heeft, opschonen door forget() aan te roepen op de USBDevice instantie. Voor een educatieve webapplicatie die op een gedeelde computer met veel apparaten wordt gebruikt, zorgt een groot aantal verzamelde, door gebruikers gegenereerde machtigingen bijvoorbeeld voor een slechte gebruikerservaring.

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

Omdat forget() beschikbaar is in Chrome 101 of hoger, controleer je of deze functie wordt ondersteund met het volgende:

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

Limieten op de overdrachtsgrootte

Sommige besturingssystemen stellen limieten aan de hoeveelheid gegevens die deel kunnen uitmaken van lopende USB-transacties. Door uw gegevens op te splitsen in kleinere transacties en er slechts een paar tegelijk in te dienen, kunt u deze beperkingen vermijden. Het vermindert ook de hoeveelheid geheugen die wordt gebruikt en zorgt ervoor dat uw toepassing de voortgang kan rapporteren zodra de overdracht is voltooid.

Omdat meerdere overdrachten die naar een eindpunt worden verzonden altijd in volgorde worden uitgevoerd, is het mogelijk om de doorvoer te verbeteren door meerdere in de wachtrij geplaatste chunks in te dienen om latentie tussen USB-overdrachten te voorkomen. Elke keer dat een deel volledig is verzonden, wordt uw code ervan op de hoogte gesteld dat het meer gegevens moet leveren, zoals gedocumenteerd in het onderstaande voorbeeld van de helperfunctie.

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);
}

Tips

Fouten opsporen in USB in Chrome is eenvoudiger met de interne pagina about://device-log , waar u alle USB-apparaatgerelateerde gebeurtenissen op één plek kunt zien.

Schermafbeelding van de apparaatlogpagina voor het debuggen van WebUSB in Chrome
Apparaatlogpagina in Chrome voor het opsporen van fouten in de WebUSB API.

De interne pagina about://usb-internals komt ook van pas en stelt u in staat het aansluiten en loskoppelen van virtuele WebUSB-apparaten te simuleren. Dit is handig voor het uitvoeren van UI-testen zonder echte hardware.

Schermafbeelding van de interne pagina voor het debuggen van WebUSB in Chrome
Interne pagina in Chrome voor het debuggen van de WebUSB API.

Op de meeste Linux-systemen worden USB-apparaten standaard toegewezen met alleen-lezen-rechten. Om Chrome toe te staan ​​een USB-apparaat te openen, moet u een nieuwe udev-regel toevoegen. Maak een bestand op /etc/udev/rules.d/50-yourdevicename.rules met de volgende inhoud:

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

waarbij [yourdevicevendor] 2341 is als uw apparaat bijvoorbeeld een Arduino is. ATTR{idProduct} kan ook worden toegevoegd voor een specifiekere regel. Zorg ervoor dat uw user lid is van de plugdev groep. Sluit vervolgens uw apparaat opnieuw aan.

Bronnen

Stuur een tweet naar @ChromiumDev met de hashtag #WebUSB en laat ons weten waar en hoe u deze gebruikt.

Dankbetuigingen

Met dank aan Joe Medley voor het beoordelen van dit artikel.