Доступ к 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 , и сообщите нам, где и как вы его используете.

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

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