일반적이지 않은 HID 기기에 연결

WebHID API를 사용하면 웹사이트에서 대체 보조 키보드와 독특한 게임패드에 액세스할 수 있습니다.

François Beaufort
François Beaufort

대체 키보드나 이색적인 게임패드와 같이 시스템의 기기 드라이버에서 액세스할 수 없을 만큼 새롭거나 오래되었거나 흔하지 않은 HID (휴먼 인터페이스 기기)가 많이 있습니다. WebHID API는 JavaScript로 기기별 로직을 구현하는 방법을 제공하여 이 문제를 해결합니다.

추천 사용 사례

HID 기기는 인간으로부터 입력을 받거나 인간에게 출력을 제공합니다. 기기의 예로는 키보드, 포인팅 기기 (마우스, 터치스크린 등), 게임패드가 있습니다. HID 프로토콜을 사용하면 운영체제 드라이버를 사용하여 데스크톱 컴퓨터에서 이러한 기기에 액세스할 수 있습니다. 웹 플랫폼은 이러한 드라이버를 사용하여 HID 기기를 지원합니다.

흔하지 않은 HID 기기에 액세스할 수 없는 문제는 보조 키보드 (예: Elgato Stream Deck, Jabra 헤드셋, X-keys) 및 특이한 게임패드 지원과 관련하여 특히 불편합니다. 데스크톱용으로 설계된 게임패드는 게임패드 입력 (버튼, 조이스틱, 트리거) 및 출력(LED, 럼블)에 HID를 사용하는 경우가 많습니다. 안타깝게도 게임패드 입력과 출력은 잘 표준화되어 있지 않으며 웹브라우저에는 특정 기기에 대한 맞춤 로직이 필요한 경우가 많습니다. 이는 지속 불가능하며 오래되고 일반적이지 않은 기기의 롱테일 지원을 제대로 지원하지 않습니다. 또한 브라우저가 특정 기기의 동작에 내재된 버그에 의존하게 됩니다.

용어

HID는 보고서와 보고서 설명자라는 두 가지 기본 개념으로 구성됩니다. 보고서는 기기와 소프트웨어 클라이언트 간에 교환되는 데이터입니다. 보고서 설명자는 기기가 지원하는 데이터의 형식과 의미를 설명합니다.

HID (휴먼 인터페이스 기기)는 인간으로부터 입력을 받거나 인간에게 출력을 제공하는 기기 유형입니다. 또한 설치 절차를 간소화하도록 설계된 호스트와 기기 간의 양방향 통신을 위한 표준인 HID 프로토콜을 나타냅니다. HID 프로토콜은 원래 USB 기기를 위해 개발되었지만 이후 블루투스를 비롯한 다른 여러 프로토콜을 통해 구현되었습니다.

애플리케이션과 HID 기기는 다음 세 가지 보고서 유형을 통해 바이너리 데이터를 교환합니다.

보고서 유형 설명
입력 보고서 기기에서 애플리케이션으로 전송되는 데이터입니다 (예: 버튼을 누름).
보고서 출력 애플리케이션에서 기기로 전송되는 데이터 (예: 키보드 백라이트를 켜기 위한 요청)
기능 보고서 양방향으로 전송될 수 있는 데이터 형식은 기기에 따라 다릅니다.

보고서 설명자는 기기에서 지원하는 보고서의 바이너리 형식을 설명합니다. 구조는 계층적이며 최상위 컬렉션 내에서 개별 컬렉션으로 보고서를 그룹화할 수 있습니다. 설명자의 형식은 HID 사양에 의해 정의됩니다.

HID 사용은 표준화된 입력 또는 출력을 참조하는 숫자 값입니다. 사용 값을 사용하면 기기가 보고서에서 기기의 의도된 용도와 각 필드의 목적을 설명할 수 있습니다. 예를 들어 마우스의 왼쪽 버튼에 하나가 정의되어 있습니다. 사용량은 기기 또는 보고서의 대략적인 카테고리를 나타내는 사용량 페이지로 구성됩니다.

WebHID API 사용

기능 감지

WebHID API가 지원되는지 확인하려면 다음을 사용하세요.

if ("hid" in navigator) {
  // The WebHID API is supported.
}

HID 연결 열기

WebHID API는 입력을 기다릴 때 웹사이트 UI가 차단되지 않도록 설계되어 비동기식입니다. 이것이 중요한 이유는 HID 데이터를 언제든지 수신할 수 있으므로 이를 수신할 방법이 필요하기 때문입니다.

HID 연결을 열려면 먼저 HIDDevice 객체에 액세스합니다. 이를 위해 navigator.hid.requestDevice()를 호출하여 사용자에게 기기를 선택하라는 메시지를 표시하거나 웹사이트에 이전에 액세스 권한이 부여된 기기 목록을 반환하는 navigator.hid.getDevices()에서 기기를 선택할 수 있습니다.

navigator.hid.requestDevice() 함수는 필터를 정의하는 필수 객체를 사용합니다. 이러한 ID는 USB 공급업체 식별자 (vendorId), USB 제품 식별자 (productId), 사용량 페이지 값 (usagePage), 사용량 값 (usage)과 연결된 기기를 일치시키는 데 사용됩니다. 이러한 ID는 USB ID 저장소HID 사용 표 문서에서 가져올 수 있습니다.

이 함수에서 반환하는 여러 개의 HIDDevice 객체는 동일한 물리적 기기의 여러 HID 인터페이스를 나타냅니다.

// Filter on devices with the Nintendo Switch Joy-Con USB Vendor/Product IDs.
const filters = [
  {
    vendorId: 0x057e, // Nintendo Co., Ltd
    productId: 0x2006 // Joy-Con Left
  },
  {
    vendorId: 0x057e, // Nintendo Co., Ltd
    productId: 0x2007 // Joy-Con Right
  }
];

// Prompt user to select a Joy-Con device.
const [device] = await navigator.hid.requestDevice({ filters });
// Get all devices the user has previously granted the website access to.
const devices = await navigator.hid.getDevices();
웹사이트의 HID 기기 메시지 스크린샷
Nintendo Switch Joy-Con 선택을 위한 사용자 메시지입니다.

예를 들어 navigator.hid.requestDevice()의 선택적 exclusionFilters 키를 사용하여 작동하지 않는 것으로 알려진 일부 기기를 브라우저 선택 도구에서 제외할 수도 있습니다.

// Request access to a device with vendor ID 0xABCD. The device must also have
// a collection with usage page Consumer (0x000C) and usage ID Consumer
// Control (0x0001). The device with product ID 0x1234 is malfunctioning.
const [device] = await navigator.hid.requestDevice({
  filters: [{ vendorId: 0xabcd, usagePage: 0x000c, usage: 0x0001 }],
  exclusionFilters: [{ vendorId: 0xabcd, productId: 0x1234 }],
});

HIDDevice 객체에는 기기 식별을 위한 USB 공급업체 및 제품 식별자가 포함됩니다. collections 속성은 기기의 보고서 형식에 관한 계층적 설명으로 초기화됩니다.

for (let collection of device.collections) {
  // An HID collection includes usage, usage page, reports, and subcollections.
  console.log(`Usage: ${collection.usage}`);
  console.log(`Usage page: ${collection.usagePage}`);

  for (let inputReport of collection.inputReports) {
    console.log(`Input report: ${inputReport.reportId}`);
    // Loop through inputReport.items
  }

  for (let outputReport of collection.outputReports) {
    console.log(`Output report: ${outputReport.reportId}`);
    // Loop through outputReport.items
  }

  for (let featureReport of collection.featureReports) {
    console.log(`Feature report: ${featureReport.reportId}`);
    // Loop through featureReport.items
  }

  // Loop through subcollections with collection.children
}

HIDDevice 기기는 기본적으로 '닫힌' 상태로 반환되며 데이터를 전송하거나 수신하기 전에 open()를 호출하여 열어야 합니다.

// Wait for the HID connection to open before sending/receiving data.
await device.open();

입력 보고서 수신

HID 연결이 설정되면 기기에서 "inputreport" 이벤트를 수신 대기하여 수신되는 입력 보고서를 처리할 수 있습니다. 이러한 이벤트에는 HID 데이터가 DataView 객체 (data), 속한 HID 기기 (device), 입력 보고서와 연결된 8비트 보고서 ID(reportId)로 포함됩니다.

빨간색과 파란색 Nintendo Switch 사진
Nintendo Switch Joy-Con 기기

이전 예를 이어서 아래 코드는 사용자가 Joy-Con Right 기기에서 누른 버튼을 감지하는 방법을 보여줍니다. 집에서 직접 사용해 보세요.

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

  // Handle only the Joy-Con Right device and a specific report ID.
  if (device.productId !== 0x2007 && reportId !== 0x3f) return;

  const value = data.getUint8(0);
  if (value === 0) return;

  const someButtons = { 1: "A", 2: "X", 4: "B", 8: "Y" };
  console.log(`User pressed button ${someButtons[value]}.`);
});

출력 보고서 전송

HID 기기에 출력 보고서를 전송하려면 출력 보고서 (reportId)와 연결된 8비트 보고서 ID와 바이트를 BufferSource (data)로 device.sendReport()에 전달합니다. 반환된 프로미스는 보고서가 전송되면 결정됩니다. HID 기기에서 보고서 ID를 사용하지 않으면 reportId를 0으로 설정합니다.

아래 예는 Joy-Con 기기에 적용되며 출력 보고서를 사용하여 기기를 울리는 방법을 보여줍니다.

// First, send a command to enable vibration.
// Magical bytes come from https://github.com/mzyy94/joycon-toolweb
const enableVibrationData = [1, 0, 1, 64, 64, 0, 1, 64, 64, 0x48, 0x01];
await device.sendReport(0x01, new Uint8Array(enableVibrationData));

// Then, send a command to make the Joy-Con device rumble.
// Actual bytes are available in the sample below.
const rumbleData = [ /* ... */ ];
await device.sendReport(0x10, new Uint8Array(rumbleData));

기능 보고서 보내고 받기

지형지물 보고서는 양방향으로 전송할 수 있는 유일한 유형의 HID 데이터 보고서입니다. HID 기기와 애플리케이션이 표준화되지 않은 HID 데이터를 교환할 수 있도록 허용합니다. 입력 및 출력 보고서와 달리 기능 보고서는 애플리케이션에서 정기적으로 수신하거나 전송하지 않습니다.

검은색과 은색 노트북 컴퓨터 사진
노트북 키보드

HID 기기로 기능 보고서를 전송하려면 기능 보고서 (reportId)와 연결된 8비트 보고서 ID 및 바이트를 BufferSource (data)로 device.sendFeatureReport()에 전달합니다. 반환된 프로미스는 보고서가 전송되면 확인됩니다. HID 기기에서 보고서 ID를 사용하지 않으면 reportId를 0으로 설정합니다.

아래 예에서는 Apple 키보드 백라이트 기기를 요청하고, 열고, 깜박이게 하는 방법을 보여줌으로써 기능 보고서의 사용을 보여줍니다.

const waitFor = duration => new Promise(r => setTimeout(r, duration));

// Prompt user to select an Apple Keyboard Backlight device.
const [device] = await navigator.hid.requestDevice({
  filters: [{ vendorId: 0x05ac, usage: 0x0f, usagePage: 0xff00 }]
});

// Wait for the HID connection to open.
await device.open();

// Blink!
const reportId = 1;
for (let i = 0; i < 10; i++) {
  // Turn off
  await device.sendFeatureReport(reportId, Uint32Array.from([0, 0]));
  await waitFor(100);
  // Turn on
  await device.sendFeatureReport(reportId, Uint32Array.from([512, 0]));
  await waitFor(100);
}

HID 기기에서 기능 보고서를 수신하려면 기능 보고서 (reportId)와 연결된 8비트 보고서 ID를 device.receiveFeatureReport()에 전달합니다. 반환된 프로미스는 지형지물 보고서의 콘텐츠가 포함된 DataView 객체로 확인됩니다. HID 기기가 보고서 ID를 사용하지 않는 경우 reportId를 0으로 설정합니다.

// Request feature report.
const dataView = await device.receiveFeatureReport(/* reportId= */ 1);

// Read feature report contents with dataView.getInt8(), getUint8(), etc...

연결 및 연결 해제 리슨

웹사이트에 HID 기기에 액세스할 권한이 부여되면 "connect""disconnect" 이벤트를 리슨하여 연결 및 연결 해제 이벤트를 적극적으로 수신할 수 있습니다.

navigator.hid.addEventListener("connect", event => {
  // Automatically open event.device or warn user a device is available.
});

navigator.hid.addEventListener("disconnect", event => {
  // Remove |event.device| from the UI.
});

HID 기기 액세스 취소

웹사이트는 HIDDevice 인스턴스에서 forget()를 호출하여 더 이상 유지하지 않으려는 HID 기기에 액세스하는 권한을 정리할 수 있습니다. 예를 들어 여러 기기가 있는 공유 컴퓨터에서 사용되는 교육용 웹 애플리케이션의 경우 사용자가 생성한 권한이 누적되면 사용자 환경이 저하됩니다.

단일 HIDDevice 인스턴스에서 forget()를 호출하면 동일한 물리적 기기의 모든 HID 인터페이스에 대한 액세스가 취소됩니다.

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

forget()는 Chrome 100 이상에서 사용할 수 있으므로 다음을 사용하여 이 기능이 지원되는지 확인합니다.

if ("hid" in navigator && "forget" in HIDDevice.prototype) {
  // forget() is supported.
}

개발자 팁

Chrome의 내부 페이지(about://device-log)를 사용하면 HID를 쉽게 디버깅할 수 있습니다. 여기에서 모든 HID 및 USB 기기 관련 이벤트를 한곳에서 확인할 수 있습니다.

HID를 디버그하는 내부 페이지의 스크린샷
HID를 디버그하기 위한 Chrome의 내부 페이지입니다.

HID 기기 정보를 사람이 읽을 수 있는 형식으로 덤프하려면 HID 탐색기를 확인하세요. 각 HID 사용량의 사용 값을 이름에 매핑합니다.

대부분의 Linux 시스템에서는 HID 기기가 기본적으로 읽기 전용 권한으로 매핑됩니다. Chrome에서 HID 기기를 열 수 있도록 허용하려면 새 udev 규칙을 추가해야 합니다. 다음 콘텐츠로 /etc/udev/rules.d/50-yourdevicename.rules에 파일을 만듭니다.

KERNEL=="hidraw*", ATTRS{idVendor}=="[yourdevicevendor]", MODE="0664", GROUP="plugdev"

위의 줄에서 기기가 Nintendo Switch Joy-Con인 경우 [yourdevicevendor]057e입니다. ATTRS{idProduct}를 추가하여 더 구체적인 규칙을 지정할 수도 있습니다. userplugdev 그룹의 구성원인지 확인합니다. 그런 다음 기기를 다시 연결합니다.

브라우저 지원

WebHID API는 Chrome 89의 모든 데스크톱 플랫폼 (ChromeOS, Linux, macOS, Windows)에서 사용할 수 있습니다.

데모

일부 WebHID 데모는 web.dev/hid-examples에 있습니다. 확인해 보세요.

보안 및 개인 정보 보호

사양 작성자는 사용자 제어, 투명성, 인체공학을 비롯하여 강력한 웹 플랫폼 기능에 대한 액세스 제어에 정의된 핵심 원칙을 사용하여 WebHID API를 설계하고 구현했습니다. 이 API를 사용할 수 있는 기능은 주로 한 번에 하나의 HID 기기에만 액세스 권한을 부여하는 권한 모델에 의해 제한됩니다. 사용자 메시지에 응답하여 사용자는 활성 단계를 따라 특정 HID 기기를 선택해야 합니다.

보안 절충점을 이해하려면 WebHID 사양의 보안 및 개인 정보 보호 고려사항 섹션을 확인하세요.

또한 Chrome은 각 최상위 컬렉션의 사용을 검사하며 최상위 컬렉션에 보호된 사용 (예: 일반 키보드, 마우스)이 있는 경우 웹사이트는 해당 컬렉션에 정의된 보고서를 주고받을 수 없습니다. 보호되는 사용의 전체 목록은 공개적으로 사용할 수 있습니다.

보안에 민감한 HID 기기 (예: 강화된 인증에 사용되는 FIDO HID 기기)도 Chrome에서 차단됩니다. USB 차단 목록HID 차단 목록 파일을 참고하세요.

의견

Chrome팀은 WebHID API에 대한 의견과 경험을 듣고자 합니다.

API 설계에 대해 알려주세요.

API에서 예상대로 작동하지 않는 부분이 있나요? 아니면 아이디어를 구현하는 데 필요한 메서드나 속성이 누락되어 있나요?

WebHID API GitHub 저장소에서 사양 문제를 제출하거나 기존 문제에 의견을 추가하세요.

구현 문제 신고

Chrome 구현에서 버그를 발견하셨나요? 아니면 구현이 사양과 다른가요?

WebHID 버그 신고 방법을 확인하세요. 가능한 한 많은 세부정보를 포함하고, 버그 재현을 위한 간단한 안내를 제공하고, 구성요소Blink>HID로 설정해야 합니다. Glitch는 빠르고 간편한 재현을 공유하는 데 적합합니다.

지지 표시

WebHID API를 사용할 계획인가요? 공개적으로 지원하면 Chrome팀에서 기능의 우선순위를 지정하는 데 도움이 되며 다른 브라우저 공급업체에 기능을 지원하는 것이 얼마나 중요한지 보여줍니다.

#WebHID 해시태그를 사용하여 @ChromiumDev에 트윗을 보내고 사용 위치와 사용 방법을 알려주세요.

유용한 링크

감사의 말씀

이 도움말을 검토해 주신 맷 레이놀즈님과 조 메들리님께 감사드립니다. 빨간색과 파란색 Nintendo Switch 사진: Sara Kurfeß, 검은색과 은색 노트북 컴퓨터 사진: Athul Cyriac Ajay(Unsplash)