WebHID를 사용하여 Stadia 컨트롤러와 대화하기

플래시된 Stadia 컨트롤러는 표준 게임패드처럼 작동하므로 게임패드 API를 사용하여 모든 버튼에 액세스할 수 있는 것은 아닙니다. 이제 WebHID를 사용하여 누락된 버튼에 액세스할 수 있습니다.

Stadia가 종료된 이후 많은 사람들이 컨트롤러가 매립지에 버려지는 쓸모없는 하드웨어가 될까 봐 우려했습니다. 다행히 Stadia팀은 Stadia 블루투스 모드 페이지로 이동하여 컨트롤러에 플래시할 수 있는 맞춤 펌웨어를 제공하여 Stadia 컨트롤러를 대신 개방하기로 결정했습니다. 이렇게 하면 Stadia 컨트롤러가 USB 케이블을 통해 연결하거나 블루투스를 통해 무선으로 연결할 수 있는 표준 게임패드로 표시됩니다. Project Fugu API Showcase에 소개된 Stadia 블루투스 페이지 자체는 WebHIDWebUSB를 사용하지만 이 도움말의 주제는 아닙니다. 이 게시물에서는 WebHID를 통해 Stadia 컨트롤러와 대화하는 방법을 설명합니다.

Stadia 컨트롤러를 표준 게임패드로 사용

플래시 후 컨트롤러는 운영체제에 표준 게임패드로 표시됩니다. 표준 게임패드의 일반적인 버튼 및 축 배열은 다음 스크린샷을 참고하세요. 게임패드 API 사양에 정의된 대로 표준 게임패드에는 0~16까지의 버튼이 있으므로 총 17개입니다 (d패드는 4개의 버튼으로 계산됨). 게임패드 테스터 데모에서 Stadia 컨트롤러를 사용해 보면 원활하게 작동하는 것을 확인할 수 있습니다.

다양한 축과 버튼에 라벨이 지정된 표준 게임패드의 스키마

하지만 Stadia 컨트롤러의 버튼을 세어 보면 19개입니다. 게임패드 테스터에서 하나씩 체계적으로 시도하면 어시스턴트캡처 버튼이 작동하지 않는다는 것을 알 수 있습니다. 게임패드 사양에 정의된 게임패드 buttons 속성이 개방형이라도 Stadia 컨트롤러는 표준 게임패드로 표시되므로 버튼 0~16만 매핑됩니다. 다른 버튼은 계속 사용할 수 있지만 대부분의 게임에서는 이러한 버튼이 존재하지 않는다고 가정합니다.

WebHID로 문제 해결

WebHID API 덕분에 누락된 버튼 17과 18에 대해 이야기할 수 있습니다. 원하는 경우 Gamepad API를 통해 이미 사용할 수 있는 다른 모든 버튼과 축에 관한 데이터도 가져올 수 있습니다. 첫 번째 단계는 Stadia 컨트롤러가 운영체제에 어떻게 보고되는지 확인하는 것입니다. 한 가지 방법은 임의의 페이지에서 Chrome DevTools 콘솔을 열고 WebHID API에서 필터링되지 않은 기기 목록을 요청하는 것입니다. 그런 다음 추가 검사를 위해 Stadia 컨트롤러를 수동으로 선택합니다. 빈 filters 옵션 배열을 전달하여 필터링되지 않은 기기 목록을 가져옵니다.

const [device] = await navigator.hid.requestDevice({filters: []});

선택기에서 마지막에서 두 번째 항목이 Stadia 컨트롤러처럼 보입니다.

WebHID API 기기 선택기에 관련 없는 기기가 표시되고 Stadia 컨트롤러가 마지막에서 두 번째 위치에 있습니다.

'Stadia Controller rev. A' 기기를 선택한 후 결과 HIDDevice 객체를 콘솔에 로깅합니다. 이렇게 하면 Stadia 컨트롤러의 productId (37888, 16진수로는 0x9400) 및 vendorId (6353, 16진수로는 0x18d1)이 표시됩니다. 공식 USB 공급업체 ID 표에서 vendorID를 조회하면 6353가 예상대로 Google Inc.에 매핑됩니다.

HIDDevice 객체의 로깅 출력을 보여주는 Chrome DevTools 콘솔

위에서 설명한 흐름의 대안은 URL 표시줄에서 chrome://device-log/로 이동하고 지우기 버튼을 누르고 Stadia 컨트롤러를 연결한 다음 새로고침을 누르는 것입니다. 이렇게 하면 동일한 정보가 제공됩니다.

연결된 Stadia 컨트롤러에 관한 정보를 보여주는 chrome://device-log 디버그 인터페이스

컴퓨터에 연결된 HID 기기의 세부정보를 더 자세히 살펴볼 수 있는 HID 탐색기 도구를 사용하는 방법도 있습니다.

이 두 ID(vendorIdproductId)를 사용하여 올바른 WebHID 기기를 올바르게 필터링하여 선택기에 표시되는 항목을 개선합니다.

const [stadiaController] = await navigator.hid.requestDevice({filters: [{
  vendorId: 6353,
  productId: 37888,
}]});

이제 관련 없는 모든 기기의 소음이 사라지고 Stadia 컨트롤러만 표시됩니다.

Stadia 컨트롤러만 표시하는 WebHID API 기기 선택기

다음으로 open() 메서드를 호출하여 HIDDevice를 엽니다.

await stadiaController.open();

HIDDevice을 다시 로깅하면 opened 플래그가 true로 설정됩니다.

HIDDevice 객체를 연 후 로깅한 출력을 보여주는 Chrome DevTools 콘솔

기기가 열린 상태에서 이벤트 리스너를 연결하여 수신되는 inputreport 이벤트를 수신 대기합니다.

stadiaController.addEventListener('inputreport', (e) => {
  console.log(e);
});

컨트롤러에서 어시스턴트 버튼을 눌렀다가 떼면 콘솔에 두 개의 이벤트가 로깅됩니다. '어시스턴트 버튼 다운' 및 '어시스턴트 버튼 업' 이벤트로 생각하면 됩니다. timeStamp를 제외하면 두 이벤트는 한눈에 구별할 수 없습니다.

HIDInputReportEvent 객체가 로깅되는 Chrome DevTools 콘솔

HIDInputReportEvent 인터페이스의 reportId 속성은 이 보고서의 1바이트 식별 접두사를 반환하거나 HID 인터페이스가 보고서 ID를 사용하지 않는 경우 0를 반환합니다. 이 경우 3입니다. 보안 비밀은 크기가 10인 DataView으로 표시되는 data 속성에 있습니다. DataView는 바이너리 ArrayBuffer에서 여러 숫자 유형을 읽고 쓰는 하위 수준 인터페이스를 제공합니다. 이 표현에서 더 이해하기 쉬운 것을 얻는 방법은 ArrayBuffer에서 Uint8Array를 만들어 개별 8비트 부호 없는 정수를 볼 수 있도록 하는 것입니다.

const data = new Uint8Array(event.data.buffer);

그런 다음 입력 보고서 이벤트 데이터를 다시 로깅하면 상황이 더 명확해지고 '어시스턴트 버튼 다운' 및 '어시스턴트 버튼 업' 이벤트를 파악할 수 있게 됩니다. 첫 번째 정수 (두 이벤트 모두 8)는 버튼 누르기와 관련이 있는 것으로 보이며 두 번째 정수 (20)는 어시스턴트 버튼을 눌렀는지 여부와 관련이 있는 것으로 보입니다.

각 HIDInputReportEvent에 대해 로깅되는 Uint8Array 객체를 보여주는 Chrome DevTools 콘솔

어시스턴트 버튼 대신 캡처 버튼을 누르면 버튼을 눌렀을 때 두 번째 정수가 1에서 버튼을 뗐을 때 0로 전환되는 것을 확인할 수 있습니다. 이렇게 하면 누락된 두 버튼을 사용할 수 있는 매우 간단한 '드라이버'를 작성할 수 있습니다.

stadia.addEventListener('inputreport', (event) => {
  if (!e.reportId === 3) {
    return;
  }
  const data = new Uint8Array(event.data.buffer);
  if (data[0] === 8) {
    if (data[1] === 1) {
      hidButtons[1].classList.add('highlight');
    } else if (data[1] === 2) {
      hidButtons[0].classList.add('highlight');
    } else if (data[1] === 3) {
      hidButtons[0].classList.add('highlight');
      hidButtons[1].classList.add('highlight');
    } else {
      hidButtons[0].classList.remove('highlight');
      hidButtons[1].classList.remove('highlight');
    }
  }
});

이와 같은 리버스 엔지니어링 접근 방식을 사용하면 버튼별, 축별로 WebHID를 사용하여 Stadia 컨트롤러와 통신하는 방법을 파악할 수 있습니다. 이 방법을 익히면 나머지는 거의 기계적인 정수 매핑 작업입니다.

이제 게임패드 API가 제공하는 원활한 연결 환경이 누락되었습니다. 보안상의 이유로 Stadia 컨트롤러와 같은 WebHID 기기를 사용하려면 항상 초기 선택기 환경을 한 번 거쳐야 하지만, 향후 연결에서는 알려진 기기에 다시 연결할 수 있습니다. getDevices() 메서드를 호출하면 됩니다.

let stadiaController;
const [device] = await navigator.hid.getDevices();
if (device && device.vendorId === 6353 && device.productId === 37888) {
  stadiaController = device;
}

데모

제가 만든 데모에서 Gamepad API와 WebHID API가 공동으로 제어하는 Stadia 컨트롤러를 확인할 수 있습니다. 이 도움말의 스니펫을 기반으로 빌드되는 소스 코드를 확인하세요. 간단하게 하기 위해 게임패드 API로 제어되는 A, B, X, Y 버튼과 WebHID API로 제어되는 어시스턴트, 캡처 버튼만 표시합니다. 컨트롤러 이미지 아래에 원시 WebHID 데이터가 표시되므로 컨트롤러의 모든 버튼과 축을 파악할 수 있습니다.

게임패드 API로 제어되는 A, B, X, Y 버튼과 WebHID API로 제어되는 어시스턴트 및 캡처 버튼을 보여주는 Stadia 컨트롤러 데모 앱

결론

새 펌웨어 덕분에 이제 Stadia 컨트롤러를 17개의 버튼이 있는 표준 게임패드로 사용할 수 있으며, 대부분의 경우 일반적인 웹 게임을 제어하기에 충분합니다. 어떤 이유로든 컨트롤러의 19개 버튼에서 데이터를 가져와야 하는 경우 WebHID를 사용하면 역엔지니어링을 통해 하나씩 해독할 수 있는 하위 수준 입력 보고서에 액세스할 수 있습니다. 이 도움말을 읽은 후 완전한 WebHID 드라이버를 작성한 경우 저에게 연락해 주세요. 프로젝트를 여기에 링크해 드리겠습니다. WebHID를 즐겁게 사용하세요.

감사의 말씀

이 도움말은 프랑수아 보포르가 검토했습니다.