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

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

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

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

플래시 후 컨트롤러가 운영체제에 표준 게임패드로 표시됩니다. 표준 게임패드의 일반적인 버튼 및 축 배열은 다음 스크린샷을 참고하세요. Gamepad API 사양에 정의된 대로 표준 게임패드에는 0~16개의 버튼이 있으므로 총 17개 (d-pad는 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 컨트롤러처럼 보입니다.

관련 없는 기기와 Stadia 컨트롤러가 마지막 자리에 표시된 WebHID API 기기 선택 도구

'Stadia Controller rev. A' 기기를 선택한 후 결과 HIDDevice 객체를 Console에 로깅합니다. 이렇게 하면 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 Explorer 도구를 사용하는 것입니다.

이제 이 두 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 콘솔

어시스턴트 버튼 대신 Capture 버튼을 누르면 두 번째 정수가 버튼을 누르면 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 컨트롤러와 통신하는 방법을 파악할 수 있습니다. 개념을 파악하면 나머지는 거의 기계적인 정수 매핑 작업입니다.

이제 Gamepad 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 컨트롤러를 확인할 수 있습니다. 이 도움말의 스니펫을 기반으로 빌드되는 소스 코드를 확인하세요. 간단히 하기 위해 A, B, X, Y 버튼 (Gamepad API로 제어됨)과 AssistantCapture 버튼 (WebHID API로 제어됨)만 표시합니다. 컨트롤러 이미지 아래에는 원시 WebHID 데이터가 표시되므로 컨트롤러의 모든 버튼과 축을 파악할 수 있습니다.

Gamepad API로 제어되는 A, B, X, Y 버튼과 WebHID API로 제어되는 어시스턴트 및 캡처 버튼을 보여주는 데모 앱(https://stadia-controller-webhid-gamepad.glitch.me/)

결론

새로운 펌웨어 덕분에 이제 Stadia 컨트롤러를 17개 버튼이 있는 표준 게임패드로 사용할 수 있습니다. 이는 대부분의 경우 일반적인 웹 게임을 제어하기에 충분합니다. 어떠한 이유로든 컨트롤러의 19개 버튼 모두에서 데이터가 필요한 경우 WebHID를 사용하면 하위 수준 입력 보고서에 액세스할 수 있으며, 이를 하나씩 역엔지니어링하여 해독할 수 있습니다. 이 도움말을 읽은 후 전체 WebHID 드라이버를 작성했다면 문의해 주세요. 그러면 여기에 프로젝트를 연결해 드리겠습니다. 즐거운 WebHID 사용 되시길 바랍니다.

감사의 말씀

이 도움말은 프랑소와 보포르님이 검토했습니다.