Доступ к USB-устройствам через Интернет

API WebUSB делает USB более безопасным и простым в использовании, перенося его в Интернет.

Франсуа Бофор
François Beaufort

Если я скажу прямо и просто «USB», велика вероятность, что вы сразу же подумаете о клавиатурах, мышах, аудио, видео и устройствах хранения данных. Вы правы, но вы найдете и другие типы устройств с универсальной последовательной шиной (USB).

Эти нестандартизированные USB-устройства требуют от поставщиков оборудования написания драйверов и SDK для конкретной платформы, чтобы вы (разработчик) могли ими воспользоваться. К сожалению, этот код, специфичный для платформы, исторически препятствовал использованию этих устройств в Интернете. И это одна из причин, по которой был создан WebUSB API: предоставить возможность предоставлять услуги USB-устройств в Интернете. С помощью этого API производители оборудования смогут создавать кроссплатформенные SDK JavaScript для своих устройств.

Но самое главное, это сделает USB безопаснее и проще в использовании, выведя его в Интернет .

Давайте посмотрим, какое поведение можно ожидать от WebUSB API:

  1. Купите USB-устройство.
  2. Подключите его к компьютеру. Сразу же появится уведомление с указанием нужного веб-сайта, на который можно перейти для этого устройства.
  3. Нажмите на уведомление. Сайт существует и готов к использованию!
  4. Нажмите, чтобы подключиться, и в Chrome появится окно выбора USB-устройства, где вы сможете выбрать свое устройство.

Тада!

Какой была бы эта процедура без WebUSB API?

  1. Установите приложение для конкретной платформы.
  2. Если он вообще поддерживается в моей операционной системе, убедитесь, что я загрузил нужную вещь.
  3. Установите вещь. Если вам повезет, вы не получите пугающих подсказок ОС или всплывающих окон, предупреждающих об установке драйверов/приложений из Интернета. Если вам не повезет, установленные драйверы или приложения будут работать неправильно и навредят вашему компьютеру. (Помните, что Интернет создан для того, чтобы содержать неработающие веб-сайты ).
  4. Если вы используете эту функцию только один раз, код останется на вашем компьютере до тех пор, пока вы не решите его удалить. (В Интернете неиспользуемое место со временем освобождается.)

Прежде чем я начну

В этой статье предполагается, что у вас есть базовые знания о том, как работает USB. Если нет, то рекомендую прочитать USB в NutShell . Дополнительную информацию о USB см. в официальных спецификациях USB .

API WebUSB доступен в Chrome 61.

Доступно для исходных пробных версий

Чтобы получить как можно больше отзывов от разработчиков, использующих API WebUSB в полевых условиях, мы ранее добавили эту функцию в Chrome 54 и Chrome 57 в качестве исходной пробной версии .

Последнее судебное разбирательство успешно завершилось в сентябре 2017 года.

Конфиденциальность и безопасность

только HTTPS

Из-за возможностей этой функции она работает только в безопасных контекстах . Это означает, что вам нужно будет строить с учетом TLS .

Требуется жест пользователя

В целях безопасности navigator.usb.requestDevice() можно вызывать только с помощью жеста пользователя, например касания или щелчка мыши.

Политика разрешений

Политика разрешений — это механизм, который позволяет разработчикам выборочно включать и отключать различные функции браузера и API. Его можно определить с помощью HTTP-заголовка и/или атрибута «allow» iframe.

Вы можете определить политику разрешений, которая контролирует, отображается ли атрибут usb в объекте Navigator или, другими словами, разрешаете ли вы WebUSB.

Ниже приведен пример политики заголовка, в которой WebUSB не разрешен:

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

Ниже приведен еще один пример политики контейнера, в которой разрешен USB:

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

Давайте начнем кодировать

API WebUSB во многом опирается на JavaScript Promises . Если вы с ними не знакомы, ознакомьтесь с этим замечательным руководством по Promises . Еще одна вещь: () => {} — это просто стрелочные функции ECMAScript 2015.

Получите доступ к USB-устройствам

Вы можете либо предложить пользователю выбрать одно подключенное USB-устройство, используя navigator.usb.requestDevice() , либо вызвать navigator.usb.getDevices() , чтобы получить список всех подключенных USB-устройств, к которым веб-сайту предоставлен доступ.

Функция navigator.usb.requestDevice() принимает обязательный объект JavaScript, определяющий filters . Эти фильтры используются для сопоставления любого USB-устройства с заданным идентификатором поставщика ( vendorId ) и, при необходимости, продукта ( productId ). Здесь также могут быть определены ключи classCode , protocolCode , serialNumber и subclassCode .

Снимок экрана: приглашение пользователя USB-устройства в Chrome
Подсказка пользователя USB-устройства.

Например, вот как получить доступ к подключенному устройству Arduino, настроенному для разрешения источника.

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

Прежде чем вы спросите, я не волшебным образом придумал это шестнадцатеричное число 0x2341 . Я просто искал слово «Arduino» в этом списке идентификаторов USB .

USB- device возвращенное в рамках выполненного выше обещания, содержит некоторую базовую, но важную информацию об устройстве, такую ​​как поддерживаемая версия USB, максимальный размер пакета, поставщик и идентификаторы продукта, количество возможных конфигураций, которые может иметь устройство. По сути, он содержит все поля USB-дескриптора устройства .

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

Кстати, если USB-устройство объявляет о поддержке WebUSB , а также определяет URL-адрес целевой страницы, Chrome будет отображать постоянное уведомление при подключении USB-устройства. При нажатии на это уведомление откроется целевая страница.

Скриншот уведомления WebUSB в Chrome
Уведомление WebUSB.

Поговорите с USB-платой Arduino

Хорошо, теперь давайте посмотрим, насколько легко обмениваться данными с платы Arduino, совместимой с WebUSB, через порт USB. Ознакомьтесь с инструкциями по адресу https://github.com/webusb/arduino , чтобы включить WebUSB в ваших эскизах .

Не волнуйтесь, позже в этой статье я расскажу обо всех методах устройства 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); });

Имейте в виду, что библиотека WebUSB, которую я использую, реализует лишь один пример протокола (основанный на стандартном последовательном протоколе USB), и что производители могут создавать любой набор и типы конечных точек по своему желанию. Передача управления особенно удобна для небольших команд конфигурации, поскольку они получают приоритет шины и имеют четко определенную структуру.

А вот скетч, загруженный на плату 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.
}

Сторонняя библиотека WebUSB Arduino , использованная в приведенном выше примере кода, выполняет в основном две вещи:

  • Устройство действует как устройство WebUSB, позволяя Chrome читать URL-адрес целевой страницы .
  • Он предоставляет последовательный API WebUSB, который вы можете использовать для переопределения API по умолчанию.

Посмотрите еще раз на код JavaScript. Как только я получаю device , выбранное пользователем, device.open() выполняет все шаги, специфичные для платформы, чтобы начать сеанс с USB-устройством. Затем мне нужно выбрать доступную конфигурацию USB с помощью device.selectConfiguration() . Помните, что конфигурация определяет способ питания устройства, его максимальное энергопотребление и количество интерфейсов. Говоря об интерфейсах, мне также нужно запросить эксклюзивный доступ с помощью device.claimInterface() , поскольку данные могут быть переданы на интерфейс или связанные с ним конечные точки только тогда, когда интерфейс заявлен. Наконец, вызов device.controlTransferOut() необходим для настройки устройства Arduino с соответствующими командами для связи через последовательный API WebUSB.

Отсюда device.transferIn() выполняет массовую передачу на устройство, чтобы сообщить ему, что хост готов к приему массовых данных. Затем обещание выполняется с помощью объекта result , содержащего data DataView , которые необходимо соответствующим образом проанализировать.

Если вы знакомы с USB, все это должно показаться вам довольно знакомым.

я хочу больше

API WebUSB позволяет взаимодействовать со всеми типами USB-передачи/конечных точек:

  • Передача CONTROL, используемая для отправки или получения параметров конфигурации или команд на USB-устройство, обрабатывается с помощью controlTransferIn(setup, length) и controlTransferOut(setup, data) .
  • Передачи INTERRUPT, используемые для небольшого объема данных, чувствительных ко времени, обрабатываются теми же методами, что и BULK-передачи, с помощью transferIn(endpointNumber, length) и transferOut(endpointNumber, data) .
  • ИЗОХРОННЫЕ передачи, используемые для потоков данных, таких как видео и звук, обрабатываются с помощью isochronousTransferIn(endpointNumber, packetLengths) и isochronousTransferOut(endpointNumber, data, packetLengths) .
  • BULK-передачи, используемые для надежной передачи большого количества независящих от времени данных, обрабатываются с помощью transferIn(endpointNumber, length) и transferOut(endpointNumber, data) .

Вы также можете взглянуть на проект Майка Цао WebLight , который представляет собой базовый пример создания светодиодного устройства с USB-управлением, разработанного для API WebUSB (здесь без использования Arduino). Вы найдете аппаратное обеспечение, программное обеспечение и прошивку.

Отменить доступ к USB-устройству

Веб-сайт может очистить разрешения на доступ к USB-устройству, которое ему больше не нужно, вызвав метод forget() в экземпляре USBDevice . Например, для образовательного веб-приложения, используемого на общем компьютере со многими устройствами, большое количество накопленных разрешений, созданных пользователями, ухудшает взаимодействие с пользователем.

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

Поскольку forget() доступна в Chrome 101 и более поздних версиях, проверьте, поддерживается ли эта функция, с помощью следующего:

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

Ограничения на размер перевода

Некоторые операционные системы накладывают ограничения на объем данных, которые могут быть частью ожидающих транзакций USB. Разделение ваших данных на более мелкие транзакции и отправка только нескольких за раз помогает избежать этих ограничений. Это также уменьшает объем используемой памяти и позволяет вашему приложению сообщать о ходе выполнения передачи.

Поскольку несколько передач, отправленных в конечную точку, всегда выполняются по порядку, можно повысить пропускную способность, отправив несколько фрагментов в очередь, чтобы избежать задержки между передачами USB. Каждый раз, когда фрагмент полностью передается, он уведомляет ваш код о том, что он должен предоставить больше данных, как описано в примере вспомогательной функции ниже.

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

Советы

Отладка USB в Chrome упрощается благодаря внутренней странице about://device-log , где вы можете увидеть все события, связанные с USB-устройствами, в одном месте.

Снимок экрана страницы журнала устройства для отладки WebUSB в Chrome
Страница журнала устройства в Chrome для отладки API WebUSB.

Внутренняя страница about://usb-internals также пригодится и позволяет имитировать подключение и отключение виртуальных устройств WebUSB. Это может быть полезно для тестирования пользовательского интерфейса без использования реального оборудования.

Скриншот внутренней страницы для отладки WebUSB в Chrome
Внутренняя страница в Chrome для отладки WebUSB API.

В большинстве систем Linux USB-устройства по умолчанию имеют разрешения только на чтение. Чтобы разрешить Chrome открывать USB-устройство, вам необходимо добавить новое правило udev . Создайте файл /etc/udev/rules.d/50-yourdevicename.rules со следующим содержимым:

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

где [yourdevicevendor]2341 , если ваше устройство, например, Arduino. ATTR{idProduct} также можно добавить для более конкретного правила. Убедитесь, что ваш user является членом группы plugdev . Затем просто повторно подключите устройство.

Ресурсы

Отправьте твит @ChromiumDev , используя хэштег #WebUSB , и сообщите нам, где и как вы его используете.

Благодарности

Спасибо Джо Медли за рецензирование этой статьи.