SpaceMouse auslesen unter JavaScript

Also available in: English.

In diesem Beitrag beschreibe ich die Entwicklung (inkl. Veröffentlichung des vollständigenen Quellcodes, 1 Datei mit ca. 70 SLOC) einer low-level Schnittstelle für den Zugriff auf eine SpaceMouse bzw. SpaceNavigator unter JavaScript.

SpaceMouse / SpaceNavigator – ein 6DoF Eingabegerät

Inhaltsverzeichnis:

Schau Dir auch meinen Folgebeitrag (englisch) an, in dem ich einen 6DoF Treiber vom Typ «Kamerasteuerung» vorstelle, einschliesslich vollständigem Quellcode auf der Basis von Three.js v.140.

Einleitung

Wer diesen Blog verfolgt, dem wird nicht entgangen sein das ich mich gerne und intensiv mit Computergrafik befasse. Prominente Beispiele sind meine Interaktive 3D Desmo-Animation und mein Interaktiver 3D Gespann-Simulator.

Sowohl Monster als auch Ural entsprechen inzwischen weitgehend meinen Vorstellungen und werden weiterhin aktiv genutzt. Es stehen keine dringenden Verschönerungs- bzw. Verbesserungsprojekte an. Zudem bin ich durch unseren Umzug ins Appenzell und den damit einhergehenden Verlust meiner Drehbank weitgehend meiner Fertigungskapazitäten beraubt.

Das gibt mir Zeit, mich wieder vertieft einer alten Leidenschaft, der Erstellung Interaktiver 3D Visualisierungen zu widmen, auch abseits von direktem Bike-Zusammenhang. Hilfsmittel dabei ist Three.js, eine JavaScript Bibliothek die das Erstellen und Plattform-unabhängige Web-Publishing von interaktiven 3D Inhalten im Browser ermöglicht. Inzwischen bin ich vom reinen Anwender von Three.js zum «Contributor» aufgestiegen: Verbesserungsvorschläge, welche ich dem Entwickler-Team in Form von «Pull Requests» unterbreitet hatte, wurden akzeptiert und sind inzwischen Bestandteil aller zukünftigen Three.js Versionen. Aber das ist eine andere Geschichte …

Jedenfalls lag es nahe, mich an meine leicht angestaubte, ca. 15 Jahre alte SpaceMouse (s.o.) zu erinnern, seinerzeit noch als «SpaceNavigator» vermarktet, welche in der hintersten Ecke einer meiner Schreibtisch-Schubladen schlummerte. Ich beschloss, sie zu reaktivieren und als Steuergerät meiner existierenden und zukünftigen 3D-Animationen zu nutzen. Damit begann eine ungewöhnliche Unternehmung.

1. Etappe – die Pleite beim Anbieter

Da SpaceNavigator bzw. SpaceMouse vergleichsweise ungewöhnliche Computer-Eingabegeräte darstellen die von Betriebssystemen nicht standardmäßig unterstützt werden, benötigt man spezielle Treiber, um die Daten dieser Geräte einer Applikation zugänglich zu machen. Der erste Schritt auf der Suche nach einem aktuellen Treiber führte mich zum Anbieter der inzwischen wieder SpaceMouse umbenannten Eingabegeräte, der Firma 3Dconnexion. Diese Firma unterhält ein eigenes Software Developer Programm und auch ein eigenes Forum. Nachdem ich mich für beide registriert hatte und erst einmal alle SDKs für die Plattformen Windows, MacOS und Linux heruntergeladen und gesichtet hatte, fand ich schließlich ein «Web» Unterverzeichnis im Windows SDK.

Ich fasse mich an dieser Stelle möglichst kurz, um nicht allzu unfreundlich zu werden:

Was 3Dconnexion hier als Software-Unterstützung, gar SDK für die Nutzung in einer Web-Umgebung unter JavaScript anbietet, spottet jeder Beschreibung.

Ein irrsinnig komplexer Protokoll-Stack, mit WebSockets, eigenem Proxy-Server (mit eigenem Root Zertifikat und Schlüssel), Cryptographie-Bibliothek, einer eigenen (closed source) «Navlib», die mit einer gegenüber einem potenziellen Anwendungsprogrammierer immerhin dokumentierten Anwendungsschicht kommuniziert. Die je Rechner zu installierende aktuelle 3DxWare belegt rund 63 MB Speicherplatz und startet geschlagene fünf Hintergrundprozesse:

Durch 3DxWare automatisch gestartete Hintergrundprozesse

Beigefügt war ein offensichtlich seit Jahren ungewartetes Beispielprogramm auf der Basis von Three.js in der Version 71 (aktuell steht Three.js bei Version 139), basierend auf dem o.a. Protokoll-Stack und der darüber liegenden Abstraktionsschicht. Wobei diese Schicht sich anmaßt, die Hoheit über die Three.js cameraMatrixWorld zu beanspruchen: der potenzielle Nutzer muss(!) Angaben zur camera.position, zum camera.lookAt(), zum camera.fov, zur bounding box der Geometrien in der Three.js scene und weitere Angaben machen, aus denen der sogenannte «Treiber» dann eine seiner Meinung nach passende cameraMatrixWorld für Three.js berechnet. Dazu noch Angaben zu einer «construction plane», einer Standard Ansichtsrichtung etc. Das sind alles Angaben, die einen Treiber mit Verlaub gesagt einen feuchten Scheißdreck angehen. Sowas geht garnicht, und kein ernsthafter Three.js-Entwickler wird sich ohne Not in ein derartiges Korsett zwängen lassen.

Meine Frage an einen der Software-Verwalter im Software Support Forum, ob es keine Möglichkeit gebe, die rohen (raw) Sensor Daten der SpaceMouse unbearbeitet durch den Protokollstack durchzuschleifen und Software-Entwicklern zur Verfügung zu stellen, wurde nach intensiver und hitziger werdender Diskussion leider immer wieder abschlägig beschieden.

Selbst mein Hinweis darauf, daß Three.js inzwischen über 100’000 Webseiten unterstützt, mit absehbar einem Vielfachen an Besuchern, und daß dies ein gewaltiges Reservoir potenzieller Neukunden für 3DConnexion darstelle, welches aber ohne angemessenen Support für Software-Entwickler nicht angezapft werden würde, änderte nichts am Ergebnis.

Nun geht es mir grundsätzlich am Gesäß vorbei, ob 3Dconnexion sein Marktpotenzial ausschöpft oder nicht. Was mir aber nicht egal ist, ist, daß ein phantastisch intuitiv nutzbares Eingabegerät nun in einer Web-Umgebung nicht mehr nach meinem Belieben nutzbar sein soll. Z.B. so, wie ich es in meinen früheren Linux-basierten Anwendungen vor 15 Jahren bereits kennen und schätzen gelernt hatte.

Die Erfinder des Begriffs Bloatware scheinen jedenfalls genau solche Softwarepakete im Sinn gehabt zu haben, die einen sinnlos aufgeblähten Funktionsumfang und Ressourcenverbrauch aufweisen, und dabei Kernfunktionalität garnicht mehr bereitstellen, bzw. benötigte Informationen maximal verschleiern. Hierfür den Begriff «Overengineering» zu verwenden würde nach meinem Empfinden die Grenze zur Beleidigung echter Ingenieure überschreiten.

Der oben verlinkte Artikel über Bloatware hält sehr treffend fest:

Bloatware entsteht in der Regel aus Marketinggründen oder – auch angeblichen – Anwenderwünschen.

Wikipedia

Exakt diese Begründung hatte mir der Verwalter im Software Developer Forum auch gegeben: das SDK sei deshalb so komplex, weil anderenfalls, bei Bereitstellung der rohen (raw) Sensordaten, ein Entwickler «the entirety of navigation features customers have come to expect» selbst entwickeln müsse.

Es wurde mir schließlich klar, daß von 3Dconnexion keine Hilfe zu erwarten sein würde. Ich entschied mich, die Sache in die eigenen Hände zu nehmen und mir nach Möglichkeit selbst zu helfen.

2. Etappe – Auftritt Wireshark

Ich wollte die Sache von Grund auf angehen, bevor irgend ein Treiber evtl. Daten verfälschen oder verstecken würde. Mein SpaceNavigator ist ein kabelgebundenes USB-Device und ich wusste, daß dieses Gerät die von mir gewünschten Daten tatsächlich bereitstellt. Die Daten, die ich auslesen und verarbeiten möchte, werden zunächst in den Universal Serial Bus eingespeist. Das Werkzeug der Wahl für das Belauschen von Datenprotokollen durch einen engagierten Hacker heisst Wireshark. Wireshark ist ein OpenSource Programm welches es auch in einer Version für macOS gibt. Das installierte ich mir. Ursprünglich für die Protokollanalyse von Netzwerk-Datenverkehr gedacht, kann Wireshark inzwischen auch am USB lauschen. Benötigt wird unter macOS das «interface» XHC20.

Meine Entwicklungsmaschinen sind ein iMac aus 2014 und ein MacBook Air aus 2017, die beide unter macOS Catalina (10.15.7) laufen. Aus Sicherheitsgründen (das sollte ich im weiteren Verlauf meines Abenteuers noch öfter zu lesen kriegen…) ist das XHC20 Interface in macOS Catalina standardmäßig deaktiviert. Es gibt aber einen Trick, wie man es wieder aktivieren kann. Ich folgte dieser Anleitung im Apple developer Forum. Der Warnung folgend, nahm ich für dieses Experiment mein MacBook Air, welches die Prozedur und deren Rückgängigmachung bisher anscheinend schadlos überstanden hat.

Es war dies meine erste Wireshark-Session, und die von Wireshark gelieferte Informationsfülle und -Aufbereitung hat mich echt beeindruckt! 😎 :

Ausschnitt einer Wireshark Session. Hier: Device plugged into USB

Der vertikal dreigeteilte Bildschirm bietet im oberen Panel eine Liste mit Eckdaten der mitgeschnittenen Telegramme. Neuere Telegramme werden unten angehängt. Vielleicht kann man diese Sortierung auch konfigurieren..

Im mittleren Panel werden Details desjenigen Telegramms angezeigt, welches im oberen Panel markiert ist. Soweit möglich, werden den Daten «sprechende» Label vorangestellt, vermutlich spezifizierte Namen von Datenfeldern.

Im untersten Panel wird der Hex-Dump des oben markierten Telegramms angezeigt. Das ist dann wirklich «roh», bzw. «raw». Also genau das, was ich wollte.

Im obigen Screenshot zeige ich einen Ausschnitt aus dem durch Einstecken des USB-Steckers in meinen USB-Hub ausgelösten Datenverkehr. Ich nenne das das «Plug-Event«. Die markierte Zeile im oberen Panel ist die Antwort (Response) meines SpaceNavigators auf die unmittelbar vorhergehende Anfrage (Request) des USB an das neue Device, unmittelbar nachdem ich den Stecker des USB-Kabels meines SpaceNavigators in der Buchse meines USB-Hubs versenkt hatte: Wer bist Du denn? und: Was kannst Du denn?

In seiner Response stellt sich mein SpaceNavigator artig vor: u.a., daß er vom Anbieter mit der idVendor 0x046d (Logitech, Inc., grün markiert) stammt, und eine idProduct von 0xc626 (3Dconnexion SpaceNavigator 3D Mouse, blau markiert) hat. Im Hex-Dump sieht man auch, daß wir es mit einem «Little Endian» Device zu tun haben, also das least significant Byte vor dem most significant Byte aufgeführt wird.

Da ich den Zustand «System Integrity Protection aufgehoben» für mein MacBook Air nicht unnötig lange ausdehnen oder wiederholen wollte, habe ich mir einen Satz möglicher Situationen, ein sogenanntes «Szenario» ausgedacht, welches ein typisches Nutzerprofil eines Nutzers einer SpaceMouse bzw. eines SpaceNavigators abdeckt. Mein SpaceNavigator ist ein sehr schlichtes Gerät, welches den Fokus noch auf die Bereitstellung von 6DoF Daten richtet. Zusätzlich weist er zwei seitlich angeordnete Tasten auf, sowie einen den Puck umschliessenden blauen Leuchtring. Neuere Geräte am oberen Ende des 3Dconnexion Produktspektrums werden fast schon schwerpunktmäßig als programmierbare Tastaturen vermarktet, mit 6DoF quasi nur noch als Kollateralnutzen. Nicht mein Ding.

Das sind die Anwendungsfälle, die mir für meine puristische SpaceMouse eingefallen sind:

  • Device plug event (device Stecker eingesteckt)
  • Device unplug event (device Stecker abgezogen)
  • Taste(n) gedrückt/losgelassen
  • LED ein/ausschalten
  • Translation event
  • Rotation event

Für jeden dieser Anwendungsfälle habe ich einen (möglichst reinen) Protokollmittschnitt angefertigt und als separate Datei abgespeichert. Gleichzeitig stattfindender Telegrammverkehr anderer devices, die an den gleichen USB angeschlossen sind, kann man in Wireshark ausfiltern. Die in separaten Dateien gespeicherten Telegrammmitschnitte ermöglichen die nachträgliche Offline-Analyse des Telegrammverkehrs, bei wieder aktivierter System Integrity Protection.

Der oben beschriebene Screenshot eines Telegramms aus einer Serie von Telegrammen, die durch das Verbinden des Geräts mit dem Bus ausgelöst wurden, ist also nur ein Beispiel von mehreren. Auch Tastendrücke bzw. -Loslassen lösen ähnlichen, wenn auch weniger ausgedehnten Informationsaustausch über den USB aus. Genauso wie Auslenkungen des «Pucks» der SpaceMouse aus seiner statischen Mittellage. Hier konnte ich bereits erstmals die Bytes über den Bildschirm flitzen sehen, an denen ich letztendlich interessiert bin. Was meine Motivation, mich hier durchzubeissen nochmals deutlich beflügelte 😎

Damit komme ich von der Analyse zur Synthese: wie kann man das gewonnene Wissen über den Datenverkehr sinnvoll nutzen?

3. Etappe – Auftritt WebUSB

Meine weiteren Recherchen führten mich alsbald zur WebUSB API. Einer Schnittstelle zur Verfügbarmachung von USB-Geräten in einem Web-Context, also mittels JavaScript. Das klang vielversprechend!

Ich nahm den Beispiel-Code eines der Entwickler von WebUSB als Startpunkt meiner Gehversuche. Die ersten Zeilen dieser Referenz-Implementierung funktionierten auch prima, aber dann muss man recht bald ein ausgewähltes und geöffnetes device auch «claimen», d.h.: den exklusiven Zugriff darauf programm-technisch beanspruchen:

device.claimInterface(2)) // Request exclusive control over interface #2.

Dieser Aufruf löste bei mir immer eine Fehlermeldung in der JavaScript Console meines Browsers aus:

DOMException: The requested interface implements a protected class

Da habe ich lange gerätselt, was das denn für eine Ursache haben könnte. Bis ich auf die Idee kann, die komplette Fehlermeldung (in doppelte Hochkommata eingefasst) in eine Suchmaschine einzugeben.

Die Antwort fand sich, wie so häufig, auf stackoverflow.com : es stellte sich heraus, daß die Entwickler von WebUSB absichtlich «aus Sicherheitsgründen» das «claimen» von Geräten bestimmter Geräteklassen unterbunden hatten. Darunter eben leider auch die Geräteklasse HID, human interface device, zu der auch die SpaceMouse gehört.

So erwies sich dieser hoffnungsvolle Ansatz leider als Sackgasse. Immerhin verwies der Entwickler, der die zurückgezogene Unterstützung für die Geräteklasse HID propagiert hatte, auf die dedizierte API WebHID, die im Prinzip die gleiche Funktionalität bieten würde, nur eben beschränkt auf Geräte der Klasse HID.

Zwischenspiel: die Entwicklungsumgebung

Ein etwas genauerer Blick in die Spezifikation des WebHID API zeigte mir, daß zentrale Funktionen nur in einem «secure context» verfügbar sein würden, also nur unter dem https Protokoll.

Nun muss ich ein wenig ausholen, um die Auswirkungen dieser Anforderung verständlich zu machen:

Jedes Web-Publishing Projekt umfasst die Entwicklung, d.h. das Schreiben, Testen, Korrigieren von Textdateien. Im einfachsten Fall sind das reine HTML-Dateien. Diese können weitere Textdateien «referenzieren», z.B. JavaScript Dateien mittels <script> </script> Tags einbinden.

Nach abgeschlossener Entwicklung lädt der Entwickler diese Dateien auf den i.d.R. angemieteten Webspace bei seinem Provider hoch, von wo aus sie über den bei Provider laufenden Webserver auf den Browser eines Webseitenbesuchers ausgespielt und dort angezeigt werden. Häufig unter Verwendung des http:// Schemas. Dieser Fall sähe in der URL-Eingabezeile des Browsers eines Webseiten Besuchers im Prinzip so aus (dies ist nur ein nicht(!) funktionierendes Beispiel):

URL-Eingabe bei Zugriff über das Internet

Nun ist der Software-Entwicklungsprozess in aller Regel ein hochgradig iterativer: Software-Änderungen (z.B. Änderungen an .html oder .js Dateien) werden vorgenommen, dann getestet, dann die Abweichungen zwischen dem gewünschten und dem bisher realisierten Ergebnis bewertet, was in neuen Änderungen an den Quelldateien resultiert. Womit die nächste Iteration im Entwicklungszyklus eingeläutet ist.

Da das wiederholte Hochladen von halbfertigen Entwicklungsergebnissen auf den öffentlich einsehbaren Webspace den interaktiven Entwicklungsfluss hemmt und man sich andererseits nicht beim Herumstümpern vor aller Welt entblößen möchte, bzw. durch zufällige Webseitenbesucher stören lassen möchte, greift man als Entwickler während der Softwareentwicklung fürs Web gerne auf die Vereinfachung zurück, eine Datei vom lokalen Rechner direkt im eigenen Browser zu testen. In der Browser-URL-Eingabezeile sieht das z.B. so aus:

URL-Eingabe bei Zugriff auf eine Quelldatei auf dem eigenen, lokalen Rechner

Aus Bequemlichkeit war das bisher meine einzige Entwicklungsmethode gewesen.

So verlockend einfach und direkt dieser Zugriff auch ist, so bietet er doch auch Nachteile, z.B. bei nur geringfügig komplexeren Projekten, in denen z.B. eine auf dem lokalen Rechner vorliegende Haupt-Datei auf Ressourcen im Internet zugreifen möchte. Durch die Mischung von file:/// und http:// Schemata handelt man sich zuverlässig lästige CORS-Probleme ein – ein weiterer Sicherheitsmechanismus heutiger Browser.

Auch diese Probleme lassen sich umgehen, wenn man einen eigenen Webserver auf dem lokalen Rechner installiert und dann über das http:// Schema auf lokale Dateien zugreifen kann. Ich entschied mich, zu diesen Zweck den leichtgewichtigen Webserver lighttpd zu installieren, wie auch vom Three.js Projekt empfohlen. Installation und Minimal-Konfiguration haben dank der verlinkten Schnellanleitungen problemlos funktioniert und waren nach maximal zehn Minuten abgeschlossen. Nach dem Start des lighttpd Webservers konnte ich nun per http:// auf meine lokale Datei zugreifen:

URL-Eingabe bei lokalen Webserver zum Zugriff auf lokale Datei

Hier die zugehörige und vollständige Webserver Konfigurationsdatei lighttpd.conf:

server.document-root = "/Users/chris/Desktop/Vielzutun/SpaceNavigator/Web/"

server.port = 3000

mimetype.assign = (
  ".html" => "text/html",
  ".txt" => "text/plain",
  ".jpg" => "image/jpeg",
  ".png" => "image/png"
)

Uff!!! Jetzt erst einmal verschnaufen und den erreichten Stand geniessen! 😎

Denn nun geht es in der letzten Ausbaustufe noch darum, auf eine lokale Datei über den lokalen Webserver mittels https:// zugreifen zu können, um den im weiteren Entwicklungsverlauf benötigten «secure context» bereitzustellen.

Im Internet werden verschiedene Methoden beschrieben, wie man das bewerkstelligen kann. Ich entschied mich für die mkcert Methode.

mkcert ist ein Tool, welches eine auf dem lokalen Rechner nutzbare Certification Authority simuliert. Dessen Funktionsweise wird in der folgenden Grafik sehr anschaulich beschrieben:

Quelle für Grafik und sehr detaillierte Beschreibung: https://web.dev/how-to-use-local-https/

Nach korrekter Installation von mkcert findet sich in der lokalen «Schlüsselbundverwaltung» auf macOS der folgende Eintrag:

Frisch eingetroffen: die eigene Root-Zertifizierungsinstanz 😎

Nun muss nur noch die Konfiguration des Webserver lighttpd angepasst werden. Hier meine vollständige lighttpd.conf , mit der ich endlich über den gewünschten «secure context» verfüge:

server.document-root = "/Users/chris/Desktop/Vielzutun/SpaceNavigator/Web/"

server.modules   += ( "mod_openssl" )

$SERVER["socket"] == ":443" {
  ssl.engine = "enable"
  ssl.pemfile = "/Users/chris/Desktop/Vielzutun/SpaceNavigator/Web/mylocalhost.pem"
  ssl.ca-file = "/Users/chris/Library/Application Support/mkcert/rootCA.pem"
}

mimetype.assign = (
  ".html" => "text/html",
  ".txt" => "text/plain",
  ".jpg" => "image/jpeg",
  ".png" => "image/png"
)

Die Pfade müssen natürlich an die Verhältnisse beim jeweiligen Entwickler angepasst werden

Nach einem Neustart von lighttpd ist das Ziel erreicht, das ersehnte Schloss-Symbol als Zeichen eines gelungenen Zugriffs über https:// :

URL-Eingabezeile im Browser Chrome

Ich habe diese Schritte so ausführlich beschrieben, weil sie einerseits für mich neu (und entsprechend aufwendig herauszufinden) waren und weil andererseits jeder Entwickler, der meine Ergebnisse für sich nutzen möchte, nicht umhin kommen wird, diese auf seinem Rechner ebenfalls nachzuvollziehen. Ohne https:// wird der weitere Verlauf jedenfalls nicht funktionieren, auch wenn andere Wege zur Aktivierung von https:// beschritten werden können oder wurden.

Jetzt, wo der Rucksack vollständig gepackt ist, folgt die

4. Etappe – Auftritt WebHID

Die WebHID Entwickler stellen ebenfalls Code-Schnipsel bereit die die korrekte Nutzung dieser API demonstrieren. Diese nahm ich als Basis für meine erneuten Gehversuche:

let deviceFilter = { vendorId: 0x046d };
let requestParams = { filters: [deviceFilter] };
let outputReportId = 0x01;
let outputReport = new Uint8Array([42]);

function handleConnectedDevice(e) {
  console.log("Device connected: " + e.device.productName);
}

function handleDisconnectedDevice(e) {
  console.log("Device disconnected: " + e.device.productName);
}

function handleInputReport(e) {
  console.log(e.device.productName + ": got input report " + e.reportId);
  console.log(new Uint8Array(e.data.buffer));
}

navigator.hid.addEventListener("connect", handleConnectedDevice);
navigator.hid.addEventListener("disconnect", handleDisconnectedDevice);

navigator.hid.requestDevice(requestParams).then((devices) => {
  if (devices.length == 0) return;
  devices[0].open().then(() => {
    console.log("Opened device: " + device.productName);
    device.addEventListener("inputreport", handleInputReport);
    device.sendReport(outputReportId, outputReport).then(() => {
      console.log("Sent output report " + outputReportId);
    });
  });
});

Quelle: https://github.com/WICG/webhid/blob/main/EXPLAINER.md

Erster möglicher Fallstrick: die vendorID, als möglicher Filter-Parameter beim Aufruf von:

navigator.hid.requestDevice({ filters: [{ vendorId: 0x046d }] )

Auch wenn mein SpaceNavigator deutlich sichtbar als ein «3Dconnexion» Produkt gelabelt ist (siehe einleitendes Foto), so identifiziert sich dieses Gerät mit einer vendorID von 0x046d als ein von «Logitech Inc.» stammendes Gerät. Das Klebeetikett als Typenschildersatz am Anschlusskabel des Geräts, und speziell die macOS «Systeminformationen» geben hier eindeutige Auskunft:

3Dconnexion – a Logitech Company
Maßgeblich ist die Hersteller-ID (vendorID)

Da die Mutter aller Spacemäuse, die SpaceMouse Classic eine Entwicklung der DLR ist und sich die Rechte an deren Vermarktung im Laufe der Jahrzehnte anscheinend mehrfach geändert haben, sollte die Möglichkeit im Hinterkopf behalten werden, daß Chargen derartiger Geräte im Umlauf sein könnten die sich mit einer abweichenden vendorID identifizieren könnten! Da ich nur mein eigenes, ca. 15 Jahre altes Exemplar einer SpaceMouse /SpaceNavigator zur Verfügung habe, kann ich diese Vermutung weder bestätigen noch ausschließen.

Wenn man die vendorID als Filterparameter richtig übergeben hat,

navigator.hid.requestDevice({ filters: [{ vendorId: 0x046d }] })
.then((devices) => {
	if (devices.length == 0) return;
	device = devices[0]
	if (!device.opened) device.open()	// avoid re-opening an already open device
	.then(() => {
  		console.log("Opened device: " + device.productName);
  		device.addEventListener("inputreport", handleInputReport);
	})
	.catch(error => { console.error(error)
	})
});

dann reagiert ein kompatibler(!) Browser mit einem Browser-generierten Auswahldialog, in dem der Anwender «aus Sicherheitsgründen» mit einer aktiven Benutzergeste («user gesture», d.h.: Klick, Touch) ein Gerät zur Nutzung auswählt:

Browser-generierter Auswahldialog

Durch Klick auf die «Verbinden» Schaltfläche wird dann das Device geöffnet und ein EventListener für für Events vom Typ «inputreport» installiert.

An dieser Stelle eine Bemerkung zur Nomenklatur:

Ein Universal Serial Bus (USB) ist ein durch den Host (das Computer-seitige Ende) kontrollierter Kommunikationskanal. Sämtlicher Datenaustausch über diesen Bus wird vom Host durchgeführt. Angeschlossene Geräte, die in unregelmäßigen und nicht vorhersehbaren Intervallen Kommunikationsbedarf haben, können dies dem Bus «anzeigen», im Sinne von:»ich hätte da etwas, kommst Du es Dir bitte bei Gelegenheit abholen?» Das Device kann Daten also nicht zu beliebigen Zeiten in den Bus «pumpen«, sondern der Bus zieht die Daten vom Device ein, wenn er Zeit hat, sich darum zu kümmern.

Vor diesem Hintergrund, aus Sicht des Busses, bezeichnet «inputreport» ein Telegramm vom Device zum Host, etwa in der Folge einer Mausbewegung oder eines Tastendrucks, während ein «outputreport» einen Datenverkehr vom Host zum Device bezeichnet, etwa: «schalte die LED ein/aus».

Der installierte Devicehandler für «inputreport» logged nun bereits munter Daten in die JavaScript console des Browsers, wenn man Tasten drückt und/oder den Puck bewegt. Diese müssen noch richtig interpretiert werden, was ich im wesentlich durch eine Kombination von «scharf hinschauen», «bereits verstandene Telegramme ausfiltern» und «trial&error» bewältigt habe.

Entscheidend für das Verständnis dieses Byte-Salats war die Erkenntnis, daß Translation und Rotation als unterschiedliche Reports bereitgestellt werden, mit einer Reportlänge von jeweils 6 Byte. Die Annahme, daß für jede Achse jeweils ein vorzeichenbehafteter 2-Byte Wert zu berücksichtigen sei, lag auf der Hand. Und so war es dann auch.

Tasten-Events waren schnell entziffert. Weil sie sich leicht und in «reiner» Form vorhersehbar produzieren lassen.

Für das Toggeln der LED musste ich experimentieren. Einmal, weil dies der bisher erste outputReport war, den ich also nicht in freier Wildbahn beobachten konnte. Zum Glück hatte ich die Wireshark-Telegramme, die auf ReportId = 4 für LED (grüner Texthintergrund) hinwiesen:

Byteweiser Vergleich von LED-Toggle-Telegrammen

Auch gab es Fehlermeldungen bei Verwendung ungültiger reportIds, und die Annahme, mit einem übertragenen Wert von «1» bzw. «0» die LED ein- bzw. ausschalten zu können, war nicht mehr als eine begründete Vermutung. Die sich schnell als zutreffend erwies.

Damit betrachte ich mein konkretes Problem:»raw sensor data eines SpaceNavigator / SpaceMouse mittels JavaScript auslesen» als vollständig gelöst.

Koordinatensystem

SpaceMouse bzw. SpaceNavigator liegen mit ihrem schweren, gummierten Fuß rutschfest auf der horizontalen Schreibtischoberfläche. Relativ hierzu bewegt der Anwender den federnd gelagerten und bezüglich aller sechs Freiheitsgrade leicht beweglichen Puck. Die Auslenkung des Pucks wird als Translation bzw. Rotation bezüglich jeder der drei Achsen vom device ausgewertet und als vorzeichenbehafteter Ganzzahlwert an den Bus übergeben.

Ich habe bei meinen Versuchen (Schriftzug «3Dconnexion» zum Anwender zeigend, USB-Kabel vom Anwender weg führend) das folgende Koordinatensystem und zugehörige Wertebereiche beobachtet:

  • Puck nach links – rechts bewegen: Tx = [-340 .. +430]
  • Puck weg vom – zum Anwender bewegen: Ty = [-430 .. +430]
  • Puck aus dem Tisch ziehen – in den Tisch drücken: Tz = [-410 .. +430]
  • Puck (Oberkante) nach vorne-hinten kippen: Rx = [-350 .. +370]
  • Puck (Oberkante) nach rechts – links kippen: Ry = [-340 .. +330]
  • Puck (Blick von oben) CCW – CW verdrehen: Rz = [-430 .. 370]
SpaceMouse Koordinatensystem

Hardware Verfügbarkeit

Das beschriebene Gerät ist weiterhin unter dem aktuellen Namen «SpaceMouse Compact» als Neuware im aktuellen Angebot von 3Dconnexion erhältlich. Preis liegt in der Größenordnung von ca. €150,- für Neuware, und deutlich günstiger im gebrauchten Zustand, z.B. bei Ebay.

Quellcode

Wer eine SpaceMouse oder einen SpaceNavigator zur Verfügung hat aber vor dem Stress zurückschreckt, nur zum Ausprobieren einen eigenen Webserver inkl. secure context (https) aufzusetzen, für den habe ich den weiter unten bereitgestellten Quellcode auf meinen Webspace hochgeladen, von wo er über die folgende URL direkt aufgerufen werden kann, ohne Modifikation des eigenen Rechners. Erfordert Google Chrome in einer Version >= 100 . Der erforderliche secure context (https) wird bei Zugriff auf die u.a. URL direkt von meiner Webseite bereitgestellt:

https://vielzutun.ch/wordpress/public/WebHID/WebHID.html

Nachfolgend der vollständige Quellcode zur Kommunikation mit einer SpaceMouse / SpaceNavigator in einem JavaScript-Kontext.

<!DOCTYPE html>
<html lang="en">
   <head>
      <title>WebHID Playground</title>
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
   </head>
   <body>

      <div id="info">
         WebHID Playground by <a href="https://vielzutun.ch" target="_blank" rel="noopener">vielzutun.ch</a> <br/>
      </div>

      <button onclick="selectDevice()">Request HID Device</button>
      <button onclick="ledOn()">LED On</button>
      <button onclick="ledOff()">LED Off</button>

      <script>

let device;

navigator.hid.addEventListener("connect", handleConnectedDevice);
navigator.hid.addEventListener("disconnect", handleDisconnectedDevice);

function handleConnectedDevice(e) {
   console.log("Device connected: " + e.device.productName);
}

function handleDisconnectedDevice(e) {
   console.log("Device disconnected: " + e.device.productName);
   console.dir(e);
}

function selectDevice() {

   navigator.hid.requestDevice({ filters: [{ vendorId: 0x046d }] })
   .then((devices) => {
      if (devices.length == 0) return;
      device = devices[0]
      if (!device.opened) device.open()		// avoid re-opening an already open device
      .then(() => {
         console.log("Opened device: " + device.productName);
         device.addEventListener("inputreport", handleInputReport);
      })
      .catch(error => { console.error(error)
      })
   });
}

function handleInputReport(e) {

   switch ( e.reportId ) {
   case 1:		// translation event
      const Tx = e.data.getInt16(0, true);	// 'true' parameter is for little endian data
      const Ty = e.data.getInt16(2, true);
      const Tz = e.data.getInt16(4, true);
      console.log("Tx: " + Tx + ", Ty: " + Ty + ", Tz: " + Tz);
      break;
				
   case 2:		// rotation event
      const Rx = e.data.getInt16(0, true);
      const Ry = e.data.getInt16(2, true);
      const Rz = e.data.getInt16(4, true);
      console.log("Rx: " + Rx + ", Ry: " + Ry + ", Rz: " + Rz);
      break;
				
   case 3:		// key press/release event
      const value = e.data.getUint8(0);
	/*
	 For my SpaceNavigator, a device having two (2) keys only:
	 value is a 2-bit bitmask, allowing 4 key-states:
	 value = 0: no keys pressed
	 value = 1: left key pressed
	 value = 2: right key pressed
	 value = 3: both keys pressed
	 */
	console.log("Left key " + ((value & 1) ? "pressed," : "released,") + "   Right key " + ((value & 2) ? "pressed, " : "released;"));
	break;
			
   default:		// just in case a device exhibits unexpected capabilities  8-)
      console.log(e.device.productName + ": Received UNEXPECTED input report " + e.reportId);
      console.log(new Uint8Array(e.data.buffer));
   }

}

function ledOn() {
   const outputReportId = 4;
   const outputReport = Uint8Array.from([1]);
		
   device.sendReport(outputReportId, outputReport)
   .then(() => {
      console.log("Sent output report " + outputReportId + ": " + outputReport);
   })
   .catch(error => { console.error(error)
   })
}

function ledOff() {
   const outputReportId = 4;
   const outputReport = Uint8Array.from([0]);
		
   device.sendReport(outputReportId, outputReport)
   .then(() => {
      console.log("Sent output report " + outputReportId + ": " + outputReport);
   })
   .catch(error => { console.error(error)
   })
}
		
      </script>
   </body>
</html>

Die dekodierten Daten werden in dem folgenden, für Menschen lesbaren Format ausschließlich in der JavaScript-Konsole angezeigt:

Tx: -28, Ty: 71, Tz: -14
Rx: -256, Ry: 190, Rz: -248
Left key pressed,   Right key released;
Left key pressed,   Right key pressed,
Left key released,   Right key pressed, 
Left key released,   Right key released;
Sent output report 4: 1
Sent output report 4: 0

Da ich eingangs über die absurde Komplexität des von 3Dconnexion bereitgestellten Beispiel-Codes gelästert hatte, habe ich mich hier im Sinne einer Kontrastmaximierung so knapp wie möglich gehalten. Ein minimales Error-Handling ist vorhanden, umfasst aber keine höher-wertigen Absicherungen wie z.B. Verriegelung gegenüber einem Telegrammversand vor Öffnung des device. Hierfür bitte ich um Verständnis.

In einem Folgebeitrag stelle ich eine Muster-Demo für die Nutzung der rohen (raw) Sensordaten einer SpaceMouse für eine intuitive Kamerasteuerung in einem Three.js Programm vor.

Bekannte Einschränkungen

Die bedeutendste Einschränkung dürfte sein, daß die WebHID API nicht von allen Browsern unterstützt wird und mancherorts sogar als «experimentell» bezeichnet wird.

Unterstützende Browser lt. caniuse.com

Ich habe meine Entwicklung mit Google Chrome Version 100.0.4896.127 (Offizieller Build) (x86_64). durchgeführt. Der vorliegende Erfolgsbericht basiert auf dem Einsatz von Chrome.

Ich habe auch Tests mit Opera Version 85.0.4341.60 (x86_64) durchgeführt. Obwohl Opera lt. Debugger über die erforderliche navigator.hid Erweiterung verfügt, scheitert der identische Code leider bereits bei der Erkennung des Contexts einer «User Gesture», also Klick oder Touch. Was ich für einen Fehler bei Opera halte, für den ich einen Fehlerbericht beim Opera Entwiclerteam eingereicht habe.

Zu Microsoft Edge kann ich keine Aussage machen.

Ich empfehle, vor einem Einsatz in einer Produktionsumgebung abzuklären, ob diese Einschränkung akzeptiert werden kann. Durch feedback an die Entwickler von WebHID kann man vermutlich dazu beitragen, daß diese API weiterentwickelt und möglicherweise von weiteren Browseranbietern adaptiert wird.

Inzwischen habe ich das Three.js Forum auf meine Erkenntnisse aufmerksam gemacht, um meinen Teil zur Verbreitung beizutragen.

Was meine SpaceMouse / SpaceNavigator betrifft:

Ich habe in der aktuellen und auch früheren Versionen von 3DxWare eine Funktion gesehen zum «Kalibrieren» einer SpaceMouse. Dies war früher (SpaceMouse Classic (serial)) gelegentlich erforderlich, wenn es eine Drift bezüglich des Nullpunkt der SpaceMouse gab. Dann hat das device ständig geringe Verschiebungen reported, auch wenn der Puck garnicht berührt wurde. Über die «Kalibrieren» Funktion konnte man dann die aktuelle Neutralstellung des Pucks als neuen Nullpunkt definieren.

Da ich diesen Zustand an meinem SpaceNavigator weder beobachten, noch künstlich hervorrufen konnte, konnte ich auch keine Abhilfe dafür/dagegen entwickeln. Ich überlasse dies dem interessierten Leser, und würde mich über jegliches Feedback freuen. Als Starthilfe: die Wireshark Protokolle weisen darauf hin, daß zum Kalibrieren ein «LED On» Telegramm verschickt wird.

Schreiben Sie einen Kommentar

Ihre E-Mail-Adresse wird nicht veröffentlicht.