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패드는 버튼 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 컨트롤러 버전 A' 기기를 선택한 후 결과로 반환되는 HIDDevice 객체를 Console에 로깅합니다. 그러면 Stadia 컨트롤러의 productId (37888, 16진수 0x9400) 및 vendorId (6353, 16진수의 0x18d1)가 표시됩니다. 공식 USB 공급업체 ID 표에서 vendorID을 찾아보면 6353가 예상한 Google Inc.에 매핑된다는 것을 알 수 있습니다.

HIDDevice 객체 로깅 결과를 보여주는 Chrome DevTools Console

위에 설명된 절차 대신 URL 표시줄의 chrome://device-log/로 이동하여 지우기 버튼을 누르고 Stadia 컨트롤러를 연결한 다음 새로고침을 누르세요. 이렇게 해도 동일한 정보를 확인할 수 있습니다.

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

또 다른 방법은 컴퓨터에 연결된 HID 기기의 세부정보를 더 자세히 살펴볼 수 있는 HID Explorer 도구를 사용하는 것입니다.

vendorIdproductId라는 두 ID를 사용하여 이제 올바른 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 Console

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

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

컨트롤러의 어시스턴트 버튼을 눌렀다가 손을 떼면 콘솔에 두 개의 이벤트가 기록됩니다. 이를 '어시스턴트 버튼 아래로' 및 '어시스턴트 버튼 위로' 이벤트로 생각하면 됩니다. timeStamp를 제외하면 두 이벤트는 언뜻 보기에 구별되지 않습니다.

로깅되고 있는 HIDInputReportEvent 객체를 보여주는 Chrome DevTools Console

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 콘솔

Assistant 버튼 대신 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로 제어됨)과 Assistant, Capture 버튼 (WebHID API로 제어)만 표시합니다. 컨트롤러 이미지 아래에서 원시 WebHID 데이터를 볼 수 있으므로 컨트롤러의 모든 버튼과 축의 느낌을 알 수 있습니다.

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

결론

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

감사의 말

프랑수아 보퍼트에서 이 문서를 검토했습니다.