WebHID API: управление почти чем угодно из браузера

Никита Дубко, Яндекс

WebHID API: управление почти чем угодно из браузера

Никита Дубко, Яндекс

Никита
Дубко

GDE Directory

Знания 🪄

Как всё
началось...

Фронтенд — это не настоящее программирование!!!
Кто-то в твиттере
У вас даже нет доступа к реальным устройствам!
Кто-то в твиттере

Правда? 🤔

Где-то в C++

HID

usb.org

Human
Interface
Devices

USB
&
Bluetooth HID-Class

USB
&
Bluetooth HID-Class

Драйверы

Драйверы

#include "clisrv.h"
#include "device.h"
#include "server.h"
#if defined(EVENT_TRACING)
#include "server.tmh"
#endif
_IRQL_requires_max_(PASSIVE_LEVEL)
NTSTATUS
BthEchoSrvQueryInterfaces(_In_ PBTHECHOSAMPLE_SERVER_CONTEXT DevCtx);
#ifdef ALLOC_PRAGMA
#pragma alloc_text (PAGE, BthEchoSrvInitialize)
#pragma alloc_text (PAGE, BthEchoSrvUnregisterPSM)
#pragma alloc_text (PAGE, BthEchoSrvUnregisterL2CAPServer)
#pragma alloc_text (PAGE, BthEchoSrvPublishSdpRecord)
#pragma alloc_text (PAGE, BthEchoSrvRemoveSdpRecord)
#pragma alloc_text (PAGE, BthEchoSrvQueryInterfaces)
#endif

void InsertConnectionEntryLocked(
    PBTHECHOSAMPLE_SERVER_CONTEXT devCtx,
    PLIST_ENTRY ple
    )
{
    WdfSpinLockAcquire(devCtx->ConnectionListLock);
    InsertTailList(&devCtx->ConnectionList, ple);
    WdfSpinLockRelease(devCtx->ConnectionListLock);
}
microsoft / Windows-driver-samples

Polling

125 Hz

125 раз в секунду

Input Reports,
Feature Reports и
Output Reports

60 FPS

Что такое report?

Offset(d) 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15

00000000  01 7d 80 7f 80 08 00 f0 00 00 98 5b fd 07 00 57
00000016  00 9b fe 8d 0d 1b 1f 6d 05 00 00 00 00 00 1b 00
00000032  00 01 2b a9 b8 41 16 ab 86 e5 12 00 80 00 00 00
00000048  80 00 00 00 00 80 00 00 00 80 00 00 00 00 80 00
Offset(d) 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15

00000000  01 7e 81 7f 7e 08 00 bc 00 00 17 a1 fd fd ff 01
00000016  00 01 00 8d 00 a3 20 d1 07 00 00 00 00 00 1b 00
00000032  00 02 3c 0b 5b 94 1a 87 de c0 1b 41 0b 5e 94 1a
00000048  87 de c0 1b 00 80 00 00 00 80 00 00 00 00 80 00
Device Class Definition for Human Interface Devices

Подключение

Перечисление

Get_Descriptor

Стандартные дескрипторы (USB)

HID-специфичные дескрипторы

Read Only Memory

Device Descriptor

12 01 01 00 00 00 00
08 ff ff 00 01 01 00
04 0e 30 01

Device Descriptor

12 01 01 00 00 00 00
08 ff ff 00 01 01 00
04 0e 30 01

0x12 = 18 — размер дескриптора

Device Descriptor

12 01 01 00 00 00 00
08 ff ff 00 01 01 00
04 0e 30 01

0x01 — тип дескриптора

Device Descriptor

12 01 01 00 00 00 00
08 ff ff 00 01 01 00
04 0e 30 01

0x100 — USB HID Specification Release 1.0.

Device Descriptor

12 01 01 00 00 00 00
08 ff ff 00 01 01 00
04 0e 30 01

0x00 — код класса (требования к передаче данных)

Device Descriptor

12 01 01 00 00 00 00
08 ff ff 00 01 01 00
04 0e 30 01

0x00 — код подкласса (boot или нет)

Device Descriptor

12 01 01 00 00 00 00
08 ff ff 00 01 01 00
04 0e 30 01

0x00 — код протокола (1 — клавиатура, 2 — мышь)

Device Descriptor

12 01 01 00 00 00 00
08 ff ff 00 01 01 00
04 0e 30 01

0x08 — максимальный размер пакета 0-го эндпоинта

Device Descriptor

12 01 01 00 00 00 00
08 ff ff 00 01 01 00
04 0e 30 01

0xffff — Vendor ID

Device Descriptor

12 01 01 00 00 00 00
08 ff ff 00 01 01 00
04 0e 30 01

0x0001 — Product ID

Device Descriptor

12 01 01 00 00 00 00
08 ff ff 00 01 01 00
04 0e 30 01

0x0100 — релизный номер устройства

Device Descriptor

12 01 01 00 00 00 00
08 ff ff 00 01 01 00
04 0e 30 01

0x04, 0x0e, 0x30 — индексы строковых дескрипторов

Device Descriptor

12 01 01 00 00 00 00
08 ff ff 00 01 01 00
04 0e 30 01

0x01 — количество конфигураций

Device Class Definition for Human Interface Devices
static u8 dualshock4_usb_rdesc[] = {
    0x05, 0x01,         /*  Usage Page (Desktop),               */
    0x09, 0x05,         /*  Usage (Gamepad),                    */
    0xA1, 0x01,         /*  Collection (Application),           */
    0x85, 0x01,         /*      Report ID (1),                  */
    0x09, 0x30,         /*      Usage (X),                      */
    0x09, 0x31,         /*      Usage (Y),                      */
    0x09, 0x32,         /*      Usage (Z),                      */
    0x09, 0x35,         /*      Usage (Rz),                     */
    0x15, 0x00,         /*      Logical Minimum (0),            */
    0x26, 0xFF, 0x00,   /*      Logical Maximum (255),          */
    0x75, 0x08,         /*      Report Size (8),                */
    0x95, 0x04,         /*      Report Count (4),               */
    0x81, 0x02,         /*      Input (Variable),               */
    0x09, 0x39,         /*      Usage (Hat Switch),             */
    0x15, 0x00,         /*      Logical Minimum (0),            */
    0x25, 0x07,         /*      Logical Maximum (7),            */
    0x35, 0x00,         /*      Physical Minimum (0),           */
    0x46, 0x3B, 0x01,   /*      Physical Maximum (315),         */
    0x65, 0x14,         /*      Unit (Degrees),                 */
android / kernel / common.git / brillo-m9-release / . / drivers / hid / hid-sony.c
HID Descriptor Tool

Поллинг

Есть чё?

Дай мне

Pull

GET_REPORT 0x01

Push

SET_REPORT

Push

SET_FEATURE

How to Sniff USB Traffic/Reverse Engineer USB Device Interactions
libusb / hidapi

HID уже
в браузерах!

services / device / public / mojom / hid.mojom

const uint16 kGenericDesktopUndefined = 0x00;
const uint16 kGenericDesktopPointer = 0x01;
const uint16 kGenericDesktopMouse = 0x02;
const uint16 kGenericDesktopJoystick = 0x04;
const uint16 kGenericDesktopGamePad = 0x05;
const uint16 kGenericDesktopKeyboard = 0x06;
const uint16 kGenericDesktopKeypad = 0x07;
const uint16 kGenericDesktopMultiAxisController = 0x08;
const uint16 kGenericDesktopX = 0x30;
const uint16 kGenericDesktopY = 0x31;
const uint16 kGenericDesktopZ = 0x32;
const uint16 kGenericDesktopRx = 0x33;
const uint16 kGenericDesktopRy = 0x34;
const uint16 kGenericDesktopRz = 0x35;
const uint16 kGenericDesktopSlider = 0x36;
const uint16 kGenericDesktopDial = 0x37;
const uint16 kGenericDesktopWheel = 0x38;
const uint16 kGenericDesktopHatSwitch = 0x39;
const uint16 kGenericDesktopCountedBuffer = 0x3a;
const uint16 kGenericDesktopByteCount = 0x3b;
const uint16 kGenericDesktopMotionWakeup = 0x3c;
const uint16 kGenericDesktopStart = 0x3d;
const uint16 kGenericDesktopSelect = 0x3e;
const uint16 kGenericDesktopVx = 0x40;
...
Chromium Source Code

Human
Interface
Devices

WebHID

Браузерный API

НЕ W3C стандарт

WebHID API

Chrome 89+ ✅

Chrome Platform Status

Mozilla 🚫

mozilla / standards-positions

Safari 🚫

Tracking Prevention in WebKit

Не работает с
доверенным вводом

bool IsAlwaysProtected(const mojom::HidUsageAndPage& hid_usage_and_page) {
    const uint16_t usage = hid_usage_and_page.usage;
    const uint16_t usage_page = hid_usage_and_page.usage_page;

    if (usage_page == mojom::kPageKeyboard)
        return true;
    if (usage_page != mojom::kPageGenericDesktop)
        return false;
    if (usage == mojom::kGenericDesktopPointer ||
        usage == mojom::kGenericDesktopMouse ||
        usage == mojom::kGenericDesktopKeyboard ||
        usage == mojom::kGenericDesktopKeypad) {
        return true;
    }
    if (usage >= mojom::kGenericDesktopSystemControl &&
        usage <= mojom::kGenericDesktopSystemWarmRestart) {
        return true;
    }
    if (usage >= mojom::kGenericDesktopSystemDock &&
        usage <= mojom::kGenericDesktopSystemDisplaySwap) {
        return true;
    }

    return false;
}
Chromium Source Code

Требует
пользовательский жест
для разрешения работы

Как работать с HID?

navigator.hid

if ("hid" in navigator) {
    const opts = {
        filters: [
            {
                vendorId: 0x0fd9,
                productId: 0x006d
            }
        ]
    };
    const devices = await navigator.hid.requestDevice(opts);
}
if ("hid" in navigator) {
    const opts = {
        filters: [
            {
                usagePage: 0x000c, // Consumer
                usage: 0x0001 // Consumer Control
            }
        ]
    };
    const devices = await navigator.hid.requestDevice(opts);
}

about://device-log

devicehunt.com

All USB Vendors
const device = devices[0];

await device.open();

device.addEventListener("inputreport", (event) => {
    const { data, device, reportId } = event;

    if (reportId === 0x01) { // <== You need specs here
        // Что-то делаем
    }
});

DataView

DS4-USB
const buttonsLeftRight = data.getUint8(4);
const dPad = buttonsLeftRight & 0x0f;
const additionalButtons = data.getUint8(5);
const serviceButtons = data.getUint8(6);

const buttons = {
    triangle: !!(buttonsLeftRight & 0x80),
    circle: !!(buttonsLeftRight & 0x40),
    cross: !!(buttonsLeftRight & 0x20),
    square: !!(buttonsLeftRight & 0x10),

    up: dPad === 7 || dPad === 0 || dPad === 1,
    right: dPad === 1 || dPad === 2 || dPad === 3,
    down: dPad === 3 || dPad === 4 || dPad === 5,
    left: dPad === 5 || dPad === 6 || dPad === 7,
    // ...
};
const report = new Uint8Array(16);

// Report ID
report[0] = 0x05;

// Enable Rumble (0x01), Lightbar (0x02)
report[1] = 0xf0 | 0x01 | 0x02;

// Light / Heavy rumble motor
report[4] = 64;
report[5] = 127;

// Lightbar Red / Green / Blue
report[6] = 255;
report[7] = 0;
report[8] = 0;

return device.sendReport(report[0], report.slice(1));

Время демо 🎮

WebHID-DS4

🤔

Stream Deck

Elgato Stream Deck

Pete LePage
+
StreamDeck
+
Google Meet
=

petele / StreamDeck-Meet

Bramus Van Damme
+
StreamDeck Helper
=

WebHID Demo: Elgato Stream Deck Daft Punk Soundboard
Stream Deck Protocol

Время демо 🎸

device.close()

Что дальше?

Awesome WebHID
Connecting to uncommon HID devices

Берегите себя!

mefody.dev/talks/webhid/holyjs.html
@dark_mefody
n.a.dubko@gmail.com QR-код со ссылкой на доклад