Über WebHID mit dem Stadia-Controller kommunizieren

Der geflashte Stadia-Controller funktioniert wie ein herkömmliches Gamepad. Das bedeutet, dass nicht alle Tasten über die Gamepad API zugänglich sind. Mit WebHID können Sie jetzt auf die fehlenden Schaltflächen zugreifen.

Nachdem Stadia eingestellt wurde, befürchteten viele, dass der Controller als unbrauchbares Gerät auf der Mülldeponie landen würde. Glücklicherweise hat das Stadia-Team beschlossen, den Stadia-Controller stattdessen mit einer benutzerdefinierten Firmware zu öffnen, die auf dem Controller auf der Seite Bluetooth-Modus von Stadia blinken kann. Der Stadia-Controller sieht so aus wie ein standardmäßiger Gamepad, den Sie über ein USB-Kabel oder kabellos über Bluetooth verbinden können. Die Bluetooth-Seite von Stadia ist im Project Fugu API Showcase zu sehen. Dabei werden WebHID und WebUSB verwendet. Das Thema dieses Artikels ist das aber nicht. In diesem Beitrag möchte ich Ihnen erklären, wie Sie über WebHID mit dem Stadia-Controller kommunizieren können.

Der Stadia-Controller wie ein normales Gamepad

Nach dem Blinken wird der Controller für das Betriebssystem als Standard-Gamepad angezeigt. Im folgenden Screenshot sehen Sie eine gängige Anordnung von Tasten und Achsen auf einem Standard-Gamepad. Wie in der Spezifikation der Gamepad API definiert, haben Standard-Gamepads Tasten von 0 bis 16, sodass insgesamt 17 Tasten möglich sind (das Steuerkreuz zählt als vier Tasten). Wenn Sie den Stadia-Controller in der Gamepad-Tester-Demo ausprobieren, werden Sie feststellen, dass er einzigartig ist.

Schema eines Standard-Gamepads mit den verschiedenen Achsen und Tasten beschriftet.

Wenn Sie jedoch die Tasten auf dem Stadia-Controller zählen, gibt es 19. Wenn Sie sie systematisch einzeln im Gamepad-Tester ausprobieren, werden Sie feststellen, dass die Tasten Assistant und Aufnahme nicht funktionieren. Auch wenn das in der Gamepad-Spezifikation definierte Gamepad-Attribut buttons offen ist, werden nur die Tasten 0–16 zugeordnet, da der Stadia-Controller wie ein Standard-Gamepad angezeigt wird. Die anderen Schaltflächen können Sie weiterhin verwenden, in den meisten Spielen wird sie jedoch nicht erwartet.

WebHID zur Lösung

Dank der WebHID-API können Sie mit den fehlenden Schaltflächen 17 und 18 sprechen. Wenn Sie möchten, können Sie sogar Daten zu allen anderen Schaltflächen und Achsen abrufen, die bereits über die Gamepad API verfügbar sind. Als Erstes müssen Sie herausfinden, wie der Stadia-Controller sich an das Betriebssystem meldet. Dazu können Sie z. B. die Konsole der Chrome-Entwicklertools auf einer beliebigen Seite öffnen und eine ungefilterte Liste von Geräten von der WebHID API anfordern. Anschließend wählen Sie den Stadia-Controller manuell aus, um ihn weiter zu prüfen. Übergeben Sie einfach ein leeres filters-Optionsarray, um eine ungefilterte Liste von Geräten zu erhalten.

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

In der Auswahl sieht der vorletzte Eintrag aus wie der Stadia-Controller.

Die WebHID API-Geräteauswahl zeigt Geräte, die nichts miteinander zu tun haben, und der Stadia-Controller auf der vorletzten Position.

Nachdem du das Gerät „Stadia Controller rev. A“ ausgewählt hast, protokolliere das resultierende HIDDevice-Objekt in der Console. Dadurch werden productId (37888 im Hexadezimalformat 0x9400) und vendorId (6353 als Hexadezimalwert 0x18d1) des Stadia-Controllers angezeigt. Wenn du vendorID in der offiziellen Tabelle der USB-Anbieter-IDs nachschlägst, siehst du, dass 6353 der erwarteten Entsprechung entspricht: Google Inc..

Chrome DevTools-Konsole mit der Ausgabe der Protokollierung des HIDDevice-Objekts.

Alternativ zu den oben beschriebenen Schritten können Sie auch in der URL-Leiste zu chrome://device-log/ gehen, die Schaltfläche Löschen drücken, den Stadia-Controller anschließen und dann Aktualisieren auswählen. Dadurch erhalten Sie dieselben Informationen.

Die Debug-Oberfläche (chrome://device-log) mit Informationen zum angeschlossenen Stadia-Controller.

Eine weitere Alternative ist der HID Explorer, mit dem Sie noch mehr Details der mit Ihrem Computer verbundenen HID-Geräte untersuchen können.

Mit diesen beiden IDs, der vendorId und der productId, kannst du eingrenzen, was in der Auswahl angezeigt wird, indem du jetzt korrekt nach dem richtigen WebHID-Gerät filterst.

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

Jetzt ist der Lärm von Geräten, die nichts miteinander zu tun haben, weg und nur der Stadia-Controller wird angezeigt.

Die WebHID API-Geräteauswahl, auf der nur der Stadia-Controller angezeigt wird

Öffnen Sie als Nächstes das HIDDevice, indem Sie die Methode open() aufrufen.

await stadiaController.open();

Protokollieren Sie das HIDDevice noch einmal und das Flag opened wird auf true gesetzt.

Die Chrome DevTools-Konsole mit der Ausgabe des Loggings des HIDDevice-Objekts nach dem Öffnen.

Warten Sie bei geöffnetem Gerät auf eingehende inputreport-Ereignisse, indem Sie einen Event-Listener anhängen.

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

Wenn Sie die Assistant-Taste auf dem Controller gedrückt halten, werden zwei Ereignisse in der Konsole protokolliert. Sie können sich die Ereignisse als „Assistant-Taste runter“ und „Assistant-Taste auf“ vorstellen. Abgesehen vom timeStamp scheinen die beiden Ereignisse auf den ersten Blick nicht zu unterscheiden.

Die Chrome-Entwicklertools-Konsole mit der Anzeige der protokollierten HIDInputReportEvent-Objekte.

Die Eigenschaft reportId der HIDInputReportEvent-Schnittstelle gibt das Ein-Byte-Identifizierungspräfix für diesen Bericht oder 0 zurück, wenn die HID-Schnittstelle keine Berichts-IDs verwendet. In diesem Fall ist das 3. Das Secret befindet sich in der data-Property, die als DataView der Größe 10 dargestellt wird. Ein DataView bietet eine Low-Level-Schnittstelle zum Lesen und Schreiben mehrerer Zahlentypen in einer binären ArrayBuffer. Um diese Darstellung besser verständlich zu machen, erstellen Sie einen Uint8Array aus ArrayBuffer, sodass Sie die einzelnen vorzeichenlosen 8-Bit-Ganzzahlen sehen können.

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

Wenn Sie die eingegebenen Ereignisdaten für den Bericht dann noch einmal protokollieren, machen die Dinge Sinn und die Ereignisse Assistant-Taste nach unten und Assistant-Taste oben sind lesbar. Die erste Ganzzahl (bei beiden Ereignissen 8) scheint sich auf das Drücken von Tasten zu beziehen, und die zweite Ganzzahl (2 und 0) hängt davon ab, ob die Assistant-Taste gedrückt wurde oder nicht.

Die Chrome-Entwicklertools-Konsole mit der für jedes HIDInputReportEvent protokollierten Uint8Array-Objekte.

Drücke die Aufnahme-Taste anstelle der Assistant-Taste. Die zweite Ganzzahl wechselt von 1, wenn die Taste gedrückt wird, bis 0. So können Sie einen sehr einfachen „Treiber“ schreiben, mit dem Sie die beiden fehlenden Schaltflächen nutzen können.

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');
    }
  }
});

Mit einem Reverse-Engineering-Ansatz wie diesem können Sie Taste für Taste und Achse für Achse herausfinden, wie WebHID mit dem Stadia-Controller verbunden wird. Sobald Sie sich damit vertraut gemacht haben, besteht der Rest aus einer fast mechanischen Integer-Zuordnung.

Jetzt fehlt nur noch die nahtlose Verbindung, die Ihnen die Gamepad API bietet. Aus Sicherheitsgründen müssen Sie die anfängliche Auswahl immer einmal durchlaufen, um mit einem WebHID-Gerät wie dem Stadia-Controller zu funktionieren. Bei zukünftigen Verbindungen können Sie jedoch wieder eine Verbindung zu bekannten Geräten herstellen. Rufen Sie dazu die Methode getDevices() auf.

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

Demo

In einer von mir erstellten Demo siehst du den Stadia-Controller, der gemeinsam über die Gamepad API und die WebHID API gesteuert wird. Sehen Sie sich auf jeden Fall den Quellcode an, der auf den Snippets aus diesem Artikel aufbaut. Der Einfachheit halber zeige ich nur die Schaltflächen A, B, X und Y (gesteuert über die Gamepad API) sowie die Schaltflächen Assistant und Aufnahme (gesteuert über die WebHID API) an. Unter dem Controller-Bild sehen Sie die WebHID-Rohdaten, um sich einen Eindruck von den Schaltflächen und Achsen des Controllers zu verschaffen.

Die Demo-App unter https://stadia-controller-webhid-gamepad.glitch.me/ mit den Schaltflächen A, B, X und Y, die von der Gamepad API gesteuert werden, sowie die Assistant- und die Aufnahmetaste, die von der WebHID API gesteuert werden.

Ergebnisse

Dank der neuen Firmware kann der Stadia-Controller jetzt als Standard-Gamepad mit 17 Tasten verwendet werden. Das reicht in den meisten Fällen für die Steuerung gängiger Webspiele mehr als aus. Sollten Sie aus irgendeinem Grund Daten von allen 19 Tasten auf dem Controller benötigen, können Sie mit WebHID auf Low-Level-Eingabeberichte zugreifen, die sich durch Reverse Engineering nacheinander entschlüsseln lassen. Wenn Sie nach dem Lesen dieses Artikels einen vollständigen WebHID-Treiber schreiben, kontaktieren Sie mich bitte. Ich verlinke dann Ihr Projekt hier. Viel Erfolg mit WebHIDing!

Danksagungen

Dieser Artikel wurde von François Beaufort verfasst.