Parler à la manette Stadia avec le WebHID

La manette Stadia flashée se comporte comme une manette de jeu standard. Par conséquent, tous ses boutons ne sont pas accessibles à l'aide de l'API Gamepad. Avec WebHID, vous pouvez désormais accéder aux boutons manquants.

Depuis l'arrêt de Stadia, beaucoup craignaient que la manette ne finisse dans une décharge. Heureusement, l'équipe Stadia a décidé d'ouvrir la manette Stadia en fournissant un micrologiciel personnalisé que vous pouvez flasher sur votre manette en accédant à la page Mode Bluetooth Stadia. Votre manette Stadia apparaît alors comme une manette de jeu standard que vous pouvez connecter via un câble USB ou sans fil via le Bluetooth. Fièrement présentée dans la présentation des API du projet Fugu, la page sur le Bluetooth Stadia utilise elle-même WebHID et WebUSB, mais ce n'est pas l'objet de cet article. Dans cet article, je vais vous expliquer comment communiquer avec la manette Stadia via WebHID.

Utiliser la manette Stadia comme une manette de jeu standard

Après le flashage, la manette apparaît comme une manette de jeu standard pour le système d'exploitation. La capture d'écran suivante montre un agencement de boutons et d'axes courant sur une manette de jeu standard. Comme défini dans la spécification de l'API Gamepad, les manettes de jeu standards comportent des boutons de 0 à 16, soit 17 au total (la croix directionnelle compte pour quatre boutons). Si vous essayez la manette Stadia sur la démo du testeur de manettes, vous constaterez qu'elle fonctionne parfaitement.

Schéma d'une manette de jeu standard avec les différents axes et boutons nommés.

Toutefois, si vous comptez les boutons de la manette Stadia, vous en trouverez 19. Si vous les essayez systématiquement un par un dans le testeur de manette, vous constaterez que les boutons Assistant et Capturer ne fonctionnent pas. Même si l'attribut buttons de la manette de jeu tel que défini dans les spécifications de la manette de jeu est ouvert, comme la manette Stadia apparaît comme une manette de jeu standard, seuls les boutons 0 à 16 sont mappés. Vous pouvez toujours utiliser les autres boutons, mais la plupart des jeux ne s'attendent pas à leur existence.

WebHID à la rescousse

Grâce à l'API WebHID, vous pouvez communiquer avec les boutons 17 et 18 manquants. Si vous le souhaitez, vous pouvez même obtenir des données sur tous les autres boutons et axes déjà disponibles via l'API Gamepad. La première étape consiste à déterminer comment la manette Stadia se signale au système d'exploitation. Pour ce faire, vous pouvez ouvrir la console des outils pour les développeurs Chrome sur n'importe quelle page et demander une liste non filtrée d'appareils à l'API WebHID. Vous choisissez ensuite manuellement la manette Stadia à inspecter. Obtenez une liste d'appareils non filtrée en transmettant simplement un tableau d'options filters vide.

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

Dans le sélecteur, l'avant-dernière entrée ressemble à la manette Stadia.

Sélecteur d'appareils de l'API WebHID affichant des appareils sans rapport et la manette Stadia à l'avant-dernière position.

Après avoir sélectionné l'appareil "Manette Stadia rev. A", consignez l'objet HIDDevice obtenu dans la console. Cela révèle les productId (37888, qui correspond à 0x9400 en hexadécimal) et vendorId (6353, qui correspond à 0x18d1 en hexadécimal) de la manette Stadia. Si vous recherchez vendorID dans la table officielle des ID de fournisseur USB, vous constaterez que 6353 correspond à ce que vous attendez: Google Inc..

Console Chrome DevTools affichant la sortie de la journalisation de l'objet HIDDevice.

Vous pouvez également accéder à chrome://device-log/ dans la barre d'URL, appuyer sur le bouton Effacer, brancher votre manette Stadia, puis appuyer sur Actualiser. Vous obtenez ainsi les mêmes informations.

Interface de débogage chrome://device-log affichant des informations sur la manette Stadia branchée.

Vous pouvez également utiliser l'outil HID Explorer, qui vous permet d'explorer encore plus en détail les appareils HID connectés à votre ordinateur.

Utilisez ces deux ID, vendorId et productId, pour affiner ce qui s'affiche dans le sélecteur en filtrant correctement le bon appareil WebHID.

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

Le bruit de tous les appareils sans rapport a disparu, et seule la manette Stadia s'affiche.

Sélecteur d'appareil de l'API WebHID n'affichant que la manette Stadia.

Ouvrez ensuite HIDDevice en appelant la méthode open().

await stadiaController.open();

Enregistrez à nouveau HIDDevice. L'indicateur opened est alors défini sur true.

Console DevTools Chrome affichant la sortie de la journalisation de l'objet HIDDevice après son ouverture.

Lorsque l'appareil est ouvert, écoutez les événements inputreport entrants en associant un écouteur d'événements.

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

Lorsque vous appuyez sur le bouton Assistant de la manette et le relâchez, deux événements sont consignés dans la console. Vous pouvez les considérer comme des événements "Bouton Assistant enfoncé" et "Bouton Assistant relevé". À première vue, les deux événements sont impossibles à distinguer, à l'exception de timeStamp.

Console des outils de développement Chrome montrant l'enregistrement des objets HIDInputReportEvent.

La propriété reportId de l'interface HIDInputReportEvent renvoie le préfixe d'identification d'un octet pour ce rapport, ou 0 si l'interface HID n'utilise pas d'ID de rapport. Dans ce cas, il s'agit de 3. Le secret se trouve dans la propriété data, qui est représentée par un DataView de taille 10. Un DataView fournit une interface de bas niveau pour lire et écrire plusieurs types de nombres dans un ArrayBuffer binaire. Pour obtenir une représentation plus lisible, créez un Uint8Array à partir du ArrayBuffer afin de voir les entiers non signés de 8 bits individuels.

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

Lorsque vous enregistrez à nouveau les données d'événement du rapport d'entrée, les choses commencent à prendre plus de sens, et les événements "Boutons Assistant enfoncés" et "Boutons Assistant relevés" deviennent lisibles. Le premier entier (8 dans les deux événements) semble être lié aux pressions sur les boutons, et le deuxième entier (2 et 0) semble être lié à l'appui ou non sur le bouton de l'Assistant.

Console des outils pour les développeurs Chrome montrant les objets Uint8Array consignés pour chaque HIDInputReportEvent.

Appuyez sur le bouton Capture au lieu du bouton Assistant. Vous verrez que le deuxième entier passe de 1 lorsque le bouton est enfoncé à 0 lorsqu'il est relâché. Vous pouvez ainsi écrire un "pilote" très simple qui vous permet d'utiliser les deux boutons manquants.

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

Grâce à une approche d'ingénierie inverse comme celle-ci, vous pouvez déterminer comment communiquer avec la manette Stadia avec WebHID, bouton par bouton et axe par axe. Une fois que vous avez compris, le reste est presque un travail mécanique de mappage d'entiers.

La seule chose qui manque à présent est l'expérience de connexion fluide que l'API Gamepad vous offre. Pour des raisons de sécurité, vous devez toujours passer par l'expérience de sélection initiale une fois pour utiliser un appareil WebHID tel que la manette Stadia. Toutefois, pour les futures connexions, vous pouvez vous reconnecter à des appareils connus. Pour ce faire, appelez la méthode getDevices().

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

Démo

Vous pouvez voir le contrôleur Stadia contrôlé conjointement par l'API Gamepad et l'API WebHID dans une démo que j'ai créée. N'oubliez pas de consulter le code source, qui s'appuie sur les extraits de code de cet article. Par souci de simplicité, je n'affiche que les boutons A, B, X et Y (contrôlés par l'API Gamepad), ainsi que les boutons Assistant et Capture (contrôlés par l'API WebHID). Sous l'image du contrôleur, vous pouvez voir les données WebHID brutes, ce qui vous permet de vous familiariser avec tous les boutons et axes du contrôleur.

Application de démonstration disponible sur https://stadia-controller-webhid-gamepad.glitch.me/, montrant que les boutons A, B, X et Y sont contrôlés par l'API Gamepad, et que les boutons Assistant et Capture sont contrôlés par l'API WebHID.

Conclusions

Grâce au nouveau micrologiciel, la manette Stadia peut désormais être utilisée comme une manette de jeu standard avec 17 boutons, ce qui est largement suffisant dans la plupart des cas pour contrôler les jeux Web courants. Si, pour une raison quelconque, vous avez besoin des données de l'ensemble des 19 boutons de la manette, WebHID vous permet d'accéder aux rapports d'entrée de bas niveau que vous pouvez déchiffrer en les analysant à l'envers, un par un. Si vous écrivez un pilote WebHID complet après avoir lu cet article, n'hésitez pas à me contacter. Je serais ravi de mettre un lien vers votre projet ici. Bonne utilisation de WebHID !

Remerciements

Cet article a été relu par François Beaufort.