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

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

François Beaufort
François Beaufort

대체 키보드나 이국적인 게임패드 같은 인간 인터페이스 기기 (HID)의 롱테일은 너무 최근에 출시되었거나, 너무 오래되었거나, 시스템의 기기 드라이버가 액세스할 수 없을 정도로 흔하지 않습니다. WebHID API는 JavaScript에서 기기별 로직을 구현하는 방법을 제공하여 이 문제를 해결합니다.

추천 사용 사례

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

일반적이지 않은 HID 기기에 액세스할 수 없다면 대체 보조 키보드 (예: Elgato Stream Deck, Jabra 헤드셋, X키) 및 독특한 게임패드 지원과 관련하여 특히 문제가 됩니다. 데스크톱용 게임패드는 게임패드 입력 (버튼, 조이스틱, 트리거)과 출력(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() 함수는 필터를 정의하는 필수 객체를 사용합니다. 이러한 식별자는 USB 공급업체 식별자 (vendorId), USB 제품 식별자 (productId), 사용 페이지 값 (usagePage), 사용 값 (usage)과 연결된 모든 기기를 일치시키는 데 사용됩니다. 이러한 정보는 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 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 기기로 출력 보고서를 전송하려면 출력 보고서와 연결된 8비트 보고서 ID (reportId) 및 바이트를 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 기기로 기능 보고서를 전송하려면 기능 보고서와 연결된 8비트 보고서 ID (reportId) 및 바이트를 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 기기에서 기능 보고서를 수신하려면 기능 보고서와 연결된 8비트 신고 ID (reportId)를 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에서 HID 디버깅은 모든 HID 및 USB 기기 관련 이벤트를 한곳에서 확인할 수 있는 내부 페이지(about://device-log)를 통해 쉽게 디버깅할 수 있습니다.

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"

위 줄에서 [yourdevicevendor]는 기기가 Nintendo Switch Joy-Con인 경우 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 버그 신고 방법을 확인하세요. 최대한 많은 세부정보를 포함하고, 버그 재현을 위한 간단한 안내를 제공하며, ComponentsBlink>HID로 설정해야 합니다. Glitch는 빠르고 쉬운 재현을 공유하는 데 효과적입니다.

응원하기

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

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

유용한 링크

감사의 말

이 도움말을 리뷰해 주신 Matt ReynoldsJoe Medley에게 감사드립니다. 빨간색과 파란색의 Nintendo Switch 사진은 Sara Kurfeß가, 검은색과 은색으로 된 노트북 컴퓨터는 Unsplash의 Athul Cyriac Ajay가 촬영한 것입니다.