Werkzeuge einsammeln
Der Plan klang einfach: Firmware schreiben, auf den ESP32 flashen, Griff-Menü an der Switch öffnen, fertig. Schon beim Werkzeug gab es die ersten Überraschungen.
Die bequeme Arduino-Umgebung für den ESP32 liefert vorkompilierte Bluetooth-Bibliotheken, in denen genau das fehlt, was ein Controller braucht: die Rolle „Bluetooth-HID-Gerät“ (Human Interface Device, also Tastatur, Maus oder Gamepad). Man kann es nicht nachträglich einschalten.
Die Firmware wird direkt mit Espressifs eigenem Framework ESP-IDF gebaut, über PlatformIO als Build-Werkzeug. Dort lässt sich jede Bluetooth-Option in einer Konfigurationsdatei setzen.
Das über Homebrew installierte PlatformIO lief mit Python 3.15. Der ESP-IDF-Builder verlangt aber höchstens 3.14. Und PlatformIOs eigene Umgebung tauschte bei jedem Lauf das Build-System SCons zwischen zwei Versionen hin und her. Ergebnis: kryptische Fehler beim Linken (No module named SCons.Tool.FortranCommon).
Eine eigene virtuelle Umgebung .venv-pio mit Python 3.13, offiziellem PlatformIO 6.2.0 und der Plattform pioarduino 55.03.312-1 (ESP-IDF 5.5.5). Kleinigkeit am Rande: Der Upload-Port muss konkret heißen (/dev/cu.usbserial-110), ein Platzhalter mit Stern schlägt fehl.
Der CH340 auf dem Adeept-Board liefert am Mac oberhalb von 115 200 Baud nur Datensalat. Flash-Lesen bricht bei 460 800 ab, Schreiben geht noch. Das wurde später noch wichtig.
Die eigene Firmware (Bluedroid)
Ein Pro Controller ist aus Sicht der Konsole ein Bluetooth-HID-Gerät mit einem bestimmten Steckbrief: Gerätename „Pro Controller“, Geräteklasse 0x002508 (Gamepad), Hersteller-ID Nintendo (0x057E), Produkt-ID 0x2009. Nach dem Verbinden stellt die Switch dem Controller eine Reihe von Fragen, die er richtig beantworten muss: Wer bist du? Welche Farben hast du? Wie sind deine Sticks kalibriert? Schalte in den Vollmodus. Erst dann zeigt sie ihn an. Dieses Frage-Antwort-Spiel (die „Subcommands“) ist dank der Community gut dokumentiert.
Die erste Firmware nutzte Bluedroid, den Bluetooth-Stack, den Espressif mit ESP-IDF ausliefert. Sie sendete alle 15 ms einen Eingabereport, beantwortete alle Subcommands und sprach über USB ein kleines Binärprotokoll mit dem Mac.
Der Funkchip kann nur senden und empfangen. Alles darüber, also Geräte finden, Koppeln, Verschlüsseln, „ich bin ein Gamepad“ sagen, erledigt eine Software-Schicht, der Stack. Es gibt mehrere davon, wie es mehrere Browser gibt. Sie sollen das Gleiche tun, verhalten sich im Detail aber unterschiedlich. Genau dieses Detail wurde zum Problem.
Test am Mac: macOS kennt den Pro Controller nativ. Der Mac fand den ESP32, koppelte ihn und fuhr das komplette Frage-Antwort-Spiel durch, bis der ESP32 „Verbunden, Spieler 1“ meldete. Die Firmware war also korrekt, zumindest für Apples Treiber.
Test an der Switch 2: Griff-Menü auf, gewartet, nichts. Kein Verbindungsversuch, kein Fehler, einfach Stille.
Die große Fehlersuche
Was folgte, war ein Nachmittag mit neun Hypothesen. Jede klang plausibel, jede wurde sauber getestet, keine half. Zum Mitlesen als Zeitstrahl:
- Der Mac sah den ESP32 nur mit −79 dBm, also schwach. Antenne angesteckt, ESP32 direkt neben die Switch gelegt. Signal −32 dBm, Ergebnis unverändert.
- Vielleicht sucht die Switch nur nach Geräten, die sich als „begrenzt sichtbar“ melden? Umgestellt. Danach wurde der ESP32 nicht einmal mehr vom Mac gefunden: Der ESP32-Funkchip reagiert in diesem Modus nur noch auf die eingeschränkte Suche. Zurückgebaut.
- Plötzlich meldete die Firmware „Verbunden, Spieler 1“. Jubel. Dann die Ernüchterung: Ein Recherche-Helfer hatte zeitgleich vom Mac aus Bluetooth-Abfragen gemacht, und macOS hatte sich selbst mit dem „Pro Controller“ verbunden. Die Firmware speicherte den Mac als Partner und war für die Switch unsichtbar. Lehre: Während eines Switch-Tests keine Bluetooth-Aktionen vom Mac, und die Firmware loggt jetzt, mit wem sie verbunden ist.
- Jedes Bluetooth-Gerät hat eine Adresse, deren erste Hälfte den Hersteller verrät. Echte Controller nutzen den Nintendo-Block
98:B6:E9. Umgestellt. Nichts. Später stellte sich heraus: Der funktionierende Nachbau nutzt eine gar nicht registrierte Adresse, und die später erfolgreiche Firmware sendet mit Espressif-Adresse. Die Switch prüft das also nicht. - Mit einem eigens eingebauten Scanner las der ESP32 den funktionierenden Nachbau aus. Einziger Unterschied: Der Nachbau sendet in seiner Suchantwort nur den Namen, Bluedroid hängt zusätzlich eine Liste der unterstützten Dienste an. Auf „nur Name“ umgestellt. Nichts.
- Ein Bluetooth-Gerät „lauscht“ nur in kurzen Fenstern auf Verbindungsanfragen. Bluedroid: 11 ms alle 1,28 s. Vielleicht gibt die Switch schneller auf als der Mac? Dauerhaftes Lauschen eingeschaltet. Nichts.
- Pokémon Automations erste Switch-2-Firmware lief im Dual-Modus. Nachgebaut. Nichts.
- Ein iPhone zeigte den ESP32 unter „Andere Geräte“ als „Pro Controller“ an, bestand also Apples Filter. Die Switch 2 selbst war im Griff-Menü nicht sichtbar und sendete kein BLE: Sie sucht und verbindet aktiv, rein über Classic. Ein Mitschnitt der untersten Funkschicht (HCI-Trace) zeigte während des Tests kein einziges Ereignis.
- Die fertige Firmware von Pokémon Automation sollte zeigen, ob es an der Hardware liegt. Ging nicht: Ihr Programm spricht mit 921 600 Baud, und genau das kann der CH340 auf diesem Board am Mac nicht (siehe Fallstrick 3).
Alles, was man von außen messen kann, war am Ende identisch mit dem funktionierenden Nachbau: Name, Geräteklasse, Suchantwort, Adresse. Trotzdem sagte die Switch 2 nein, und sie sagt nicht, warum. Ein Hinweis aus der zweiten Recherche erklärte auch, warum der leere HCI-Trace nichts beweist: Eine Namensabfrage oder ein Fähigkeiten-Austausch auf unterster Ebene erzeugt beim antwortenden Gerät kein sichtbares Ereignis. Die Switch kann den ESP32 also durchaus gefunden, befragt und verworfen haben.


Der Durchbruch: ein anderer Stack
Die zweite Recherche förderte zwei Projekte zutage, die nachweislich mit der Switch 2 funktionieren: die (nicht quelloffene) Firmware von Pokémon Automation und das quelloffene Projekt esp32-switch-controller von JereDev. Beide nutzen heute BTstack statt Bluedroid. JereDev hatte den Bluedroid-Weg sogar ausdrücklich aufgegeben.
Also Gegentest: Das JereDev-Projekt auf denselben WROVER geflasht, Griff-Menü geöffnet, und nach wenigen Sekunden stand der Controller in der Liste. Im Logbuch der Firmware: Verbindung angenommen, Kopplung bestätigt, Subcommands 0x02, 0x08, 0x10, 0x03, also exakt das Frage-Antwort-Spiel, das die eigene Firmware am Mac bestanden hatte.
Hardware, Antenne, Adresse, Name, Steckbrief: alles war in Ordnung. Bluedroid als HID-Gerät wird von der Switch 2 nicht angesprochen, BTstack schon. Der Unterschied liegt in Dingen, die der Stack tief im Funkchip einstellt (Fähigkeitsbits, Verbindungsrichtlinien, Sicherheitseinstellungen) und die man von außen nicht sieht.
Statt die eigene Firmware mühsam auf BTstack zu portieren, wurde das funktionierende Projekt übernommen und angepasst. Für den Bau unter PlatformIO mit ESP-IDF 5.5.5 waren vier kleine Eingriffe nötig:
- Quellordner heißt
main, nichtsrc(src_dir = mainin derplatformio.ini). - Bluedroid komplett abschalten und den Chip auf „nur Controller“ stellen (
CONFIG_BT_CONTROLLER_ONLY=y), sonst gibt es beim Linken doppelte Symbole. - Einen Header-Pfad für
esp_bt_defs.hergänzen. - Den Ordner
3rd-party(Kryptobibliothek micro-ecc) in den Komponenten-Build aufnehmen.
Das Projekt steht unter GPL-3.0. Für den privaten Gebrauch ist das unproblematisch; wer die Firmware weitergibt, muss den Quellcode mit derselben Lizenz offenlegen. Die eigene Bluedroid-Firmware bleibt als Archiv und Referenz im Repo.

Die Steuerzentrale am Mac
Auf der Mac-Seite läuft ein Python-Programm, das 50-mal pro Sekunde den gewünschten Controller-Zustand an den ESP32 schickt. Die erste Version war ein Terminal-Skript, das Tastendrücke systemweit mitlas. Das fühlte sich nicht gut an und funktionierte beim ersten Versuch gar nicht.
Systemweites Mitlesen der Tastatur (Bibliothek pynput) braucht unter macOS die Berechtigung „Eingabeüberwachung“ für das Terminal. Ohne sie kommt einfach nichts an, ohne Fehlermeldung.
Ein Fenster mit pygame-ce, das Tasten nur entgegennimmt, wenn es den Fokus hat. Das braucht keine Sonderrechte. Dazu ein gezeichneter Pro Controller, bei dem jede gedrückte Taste aufleuchtet, Status-Chips der Firmware, Buttons für Pairing, Reconnect und Reset, und ein Log, das man speichern kann. Nebenbei: Die klassische Python-GUI-Bibliothek tkinter fehlt im Homebrew-Python 3.15 schlicht.

Das Protokoll zwischen Mac und ESP32
Die übernommene Firmware spricht JSON-Zeilen über die serielle Schnittstelle mit 115 200 Baud. Der Mac schickt den vollständigen Zustand, die Firmware antwortet kurz:
Mac → ESP32: {"id":412,"cmd":"state","r":8,"s":0,"l":0,"lx":2048,"ly":2048,"rx":2048,"ry":2048}
ESP32 → Mac: @PC {"id":412,"ok":true,"connected":true,"lag":11}
r, s und l sind die drei Tastenbytes, wie sie auch im echten Controller-Report stehen (hier: A gedrückt). Die Sticks haben 12 Bit, 2048 ist die Mitte. Kommt 500 ms kein Zustand, lässt die Firmware alle Tasten los. Das ist die Sicherheitsleine, falls der Mac hängt.
Der CH340 zieht beim Öffnen des Ports kurz die Reset-Leitung, ein Komfortmerkmal fürs Flashen. Folge: GUI starten, Skript starten, Flashen, alles wirft den ESP32 aus der Bluetooth-Verbindung. Zusätzlich darf nur ein Programm gleichzeitig am Port hängen, Befehle aus einem zweiten Prozess kommen nicht zuverlässig an.
Wird das Kabel gezogen, blieb die GUI an der toten Verbindung hängen („Device not configured“). Jetzt schließt sie den Link und verbindet sich automatisch neu, sobald der Port wieder auftaucht.
Tastenbelegung
| Tasten | Controller | Tasten | Controller |
|---|---|---|---|
| W A S D | linker Stick (Lenken) | Pfeiltasten | rechter Stick (Kamera) |
| Leertaste / K | A (Gas) | J | B (Bremse) |
| Shift | R (Drift) | E | L (Item) |
| Q / U | ZL / ZR | I / O | X / Y |
| Enter / Backspace | Plus / Minus | H / C | Home / Capture |
| 1 2 3 4 | Steuerkreuz | N / M | Stick-Klick L3 / R3 |
Reconnect und Latenz
Von selbst wieder verbinden
Wegen Fallstrick 5 musste nach jedem GUI-Start das Griff-Menü an der Switch geöffnet werden. Die Firmware merkt sich jetzt die Adresse der Konsole im nichtflüchtigen Speicher und versucht nach jedem Neustart und nach jedem Verbindungsabbruch von selbst, sie wieder zu erreichen: erster Versuch nach 1,5 s, dann alle 4 s, bis zu 20-mal. Nach dem Standby der Switch brauchte es bis zu fünf Anläufe, klappte dann aber zuverlässig. Das Griff-Menü ist nur noch beim allerersten Koppeln nötig.
Wie schnell kommt ein Tastendruck an?
Für eine KI, die in Echtzeit fährt, ist Verzögerung der Feind. Gemessen wurde zunächst der Teil, der ohne Capture-Karte messbar ist: Der Mac stempelt jeden Zustand mit einer Nummer, die Firmware antwortet mit derselben Nummer und meldet, wie lange der Zustand bis zum nächsten Funkpaket gewartet hat.
Die ursprüngliche Firmware antwortete auf jeden Zustand mit rund 200 Zeichen. Bei 50 Zuständen pro Sekunde und 115 200 Baud ist die Leitung damit dicht. Nur etwa 30 % der Pakete wurden beantwortet. Die Antwort ist jetzt auf das Nötigste gekürzt, danach kamen 98 % an.
Die Firmware schickt ihren Zustand alle 24 ms an die Switch. Der Versuch, auf 16 ms zu gehen, endete damit, dass die Switch 2 die Verbindung alle vier Sekunden trennte. Es bleibt bei 24 ms.
Die erste Latenzmessung drückte alle 400 ms die A-Taste und bewegte den Stick, während die Switch noch im Menü war. Das Ergebnis war ein Controller, der „wilde Sachen“ macht. Die Messung bewegt den Stick jetzt nur noch um 3 % innerhalb der Totzone, also unsichtbar, und tippt auf Wunsch harmlos links und rechts auf dem Steuerkreuz.
| Messgröße | Median | 95 % |
|---|---|---|
| Rundlauf seriell Mac → ESP32 → Mac | 46 ms | 69 ms |
| Firmware: Zustand übernommen → Funkpaket | 12 ms | 20 ms |
| Tastendruck am Mac → Funkpaket (geschätzt: halber Rundlauf + Firmware) | ≈ 35 ms |
Der Löwenanteil steckt im seriellen Weg über den CH340-Chip und seinen Treiber. Die Firmware selbst braucht im Schnitt nur die halbe Report-Periode. Was in Funk und Konsole dazukommt, zeigt erst die Capture-Karte. Zum Vergleich: Pokémon Automation gibt für den ESP32 drahtlos mindestens 15 ms zwischen zwei Zustandsänderungen an, und eine Taste sollte rund 40 ms gehalten werden, damit die Switch sie sicher sieht.
Ein kurzes Testrennen per Tastatur: „Die Reaktion fühlt sich normal an.“ Welt 1 ist damit geschafft.