Seit heute Nachmittag hört mein StackChan nicht mehr auf „Ni hao xiao zhi“, wenn ich ihn aufwecken will. Er hört auf „Jarvis“, antwortet auf Deutsch, und das Gesicht auf seinem Display bleibt beim Reden endlich in der richtigen Emotion, statt nach jedem Satz auf neutral zurückzuspringen. Klingt nach Kleinigkeit. War aber der Punkt, an dem aus einem netten Kickstarter-Spielzeug eine KI-Assistentin geworden ist, die ich tatsächlich am Schreibtisch stehen haben will.
Ich bin seit 1989 in der Kommunikationsbranche und berate seit einigen Jahren EPU und KMU in der DACH-Region zu KI-Themen. Hardware-Basteln in dem Ausmaß war für mich bis vor Kurzem Neuland. Genau deshalb schreib ich das hier auf: nicht als Anleitung für jeden StackChan-Besitzer, sondern als Protokoll der Stellen, an denen die Stock-Firmware aufhört und eigener Code anfangen muss.
Den ersten Anlauf mit dem Gerät hab ich schon vor einer Weile beschrieben, damals noch mit Stock-Firmware und einem NVS-Umweg, der VINCI immerhin zum Sprechen gebracht hat. Der heutige Schritt ist die konsequente Fortsetzung davon: alles, was damals ein Workaround war, ist jetzt eigener Code.
Was ist überhaupt ein StackChan, und warum eine eigene Firmware?
Kurz zur Einordnung, falls du den Namen zum ersten Mal liest. Der StackChan ist ein Kickstarter-Roboter von M5Stack auf Basis des ESP32-S3, mit zwei Servos, einem 2-Zoll-Display und einer Kamera. Meine Variante ist die CoreS3-Ausführung von OpenELAB, nicht das originale M5Stack-Board, ein Detail, das beim Flashen später noch wichtig wird. Die Stock-Firmware heißt xiaozhi-esp32 und bringt dich in wenigen Stunden zum ersten Sprechen. Das NVS (Non-Volatile Storage) ist der kleine Flash-Speicherbereich, in dem WLAN-Zugangsdaten, Kalibrierwerte und Konfiguration überleben, auch wenn die restliche Firmware neu geflasht wird.
„Eine Kickstarter-Firmware bringt dich in zwei Stunden zum ersten Sprechen. Sie bringt dich nie dorthin, wo dein Assistent auch weiß, wann er die Klappe halten soll.“
Genau das war mein Problem. Die Stock-Firmware war für einen chinesischen Sprachassistenten gebaut, mit einem hart codierten Wake-Word, das ich nicht ändern konnte, und einem Verhalten bei Zustandswechseln, das für meinen Anwendungsfall, eine österreichisch angehauchte Assistentin namens Vinci, einfach nicht gepasst hat.
Wie der Build in der Praxis abgelaufen ist
Für die StackChan Custom Firmware brauchst du zwingend ESP-IDF 6.1, die ältere 5er-Reihe wird von der xiaozhi-esp32-Basis nicht mehr unterstützt. Ich hab den kompletten Build unter WSL Ubuntu laufen lassen, weil sich die Toolchain unter Windows direkt erfahrungsgemäß mit den Treibern beißt. Der Build selbst läuft über ein Python-Skript, dem du die Zielplattform, die Sprache und das gewünschte Wake-Word mitgibst.
Beim Wake-Word bin ich kurz hängen geblieben, weil ich naiverweise gehofft hab, ich könnte „Vinci“ als Aufwachwort verwenden. Gibt’s bei Espressif schlicht nicht als trainiertes Modell, und ein eigenes Wake-Word-Modell zu trainieren wäre ein eigenes Projekt für sich. Ich bin auf „Jarvis“ umgestiegen, technisch ein Kompromiss, praktisch kein Problem, weil das Gerät danach ohnehin komplett auf Deutsch antwortet und sich wie meine eigene Assistentin anfühlt.
Ein Fehler, der mich am Anfang kurz verunsichert hat: Nach dem ersten Flash-Versuch hat Windows das Gerät als unbekanntes USB-Gerät angezeigt, mit einer Phantom-Geräte-ID. Lag am USB-C-Kabel, das nur für Strom gedacht war und keine Datenleitung hatte. Klingt banal, hat mich aber eine gute halbe Stunde gekostet, bis ich draufgekommen bin. Schauen wir mal ehrlich: Bei jedem Hardwareprojekt ist am Ende irgendwas Banales schuld, nicht die komplizierte Software.
Der größte Stolperstein: der 503-Fehler, der nicht weggehen wollte
Mein Server hat immer wieder „no device connected“ gemeldet, obwohl das Gerät sichtbar verbunden war. Ich hab das lange auf die WLAN-Verbindung geschoben, war aber ein Holzweg. Der eigentliche Grund: Die Firmware erneuert ihren 120-Sekunden-Timeout nur, wenn im OnData-Handler ein echtes Datenframe ankommt. WebSocket-Control-Pings zählen dafür nicht. Nach zwei Minuten Stille dachte der Server also, das Gerät sei weg, auch wenn die Verbindung technisch noch offen war.
Die Lösung war am Ende unspektakulär: alle 50 Sekunden zusätzlich ein eigenes JSON-Paket vom Typ „ping“ senden, das als echtes Datenframe zählt. Ich würd das eigentlich nicht „Bugfix“ nennen, es ist eher ein Workaround um eine Designentscheidung, die für Dauerbetrieb einfach nicht gedacht war.
Wie ich dem Gesicht beigebracht hab, in der richtigen Emotion zu bleiben
Das war die Stelle, die mich am meisten genervt hat, ehrlich gesagt. Jeder Zustandswechsel in der Firmware, also idle, connecting, listening, setzt das Gesicht standardmäßig auf neutral zurück. Nur der Zustand speaking macht das nicht. Wenn Vinci also gerade eine Emotion zeigen sollte, während sie spricht, musste ich lernen: Die Emotion muss nach dem Event tts:start gesendet werden, nicht davor. Vorher, und der Zustandswechsel frisst sie einfach wieder auf.
Das führt gleich zum nächsten Punkt. Gemini soll laut System-Prompt das Werkzeug set_emotion während des Gesprächs selbst aufrufen. Tut es aber nicht zuverlässig, während andere Tools wie der Kalenderabruf sauber funktionieren. Ich bin mir bei der genauen Ursache noch nicht ganz sicher, mein Eindruck ist aber, dass es an der Kombination aus Funktionsdeklarationen und Audio-Streaming liegt. Bis das gelöst ist, kommen die Emotionen bei mir hauptsächlich aus der Sprachausgabe selbst und aus einer Standard-Stimmung im Leerlauf.
Welche Fallen beim Flashen selbst gewartet haben
Ein paar Dinge, die mich Zeit gekostet haben und die ich beim nächsten Mal sofort prüfen würde:
Der seitliche Knopf am Gerät hat nach dem ersten Flash nicht reagiert. Grund: BOOT_BUTTON_GPIO und AUDIO_I2S_GPIO_MCLK liegen beide auf GPIO 0. Ein klassischer Pin-Konflikt, den man nur findet, wenn man sich durch die Board-Definition wühlt.
Das Display stand nach dem Flash hartnäckig auf hellem Theme, obwohl mein Code dunkel als Standard vorgesehen hat. Des Rätsels Lösung: Ein im NVS gespeicherter Wert schlägt jeden Code-Default. Ich hab das Theme am Ende fix in lcd_display.cc verdrahtet, statt mich auf den Default zu verlassen.
Und dann war da noch die Stromspar-Automatik, die das Display nach 60 Sekunden dimmt und nach 300 Sekunden ganz abschaltet. Für eine Assistentin, die am Schreibtisch sichtbar bleiben soll, komplett unpassend. Deaktiviert über PowerSaveTimer mit drei Minuswerten.
Bevor ich überhaupt geflasht hab, hab ich mir eine eigene Partitionstabelle gebaut, die byte-identisch zur Tabelle am Gerät ist. Der Vorteil: Dieser eine Bereich muss beim Flashen nicht angerührt werden, und die Kalibrierwerte der Servos bleiben unangetastet erhalten. Und ein volles Backup der alten Firmware liegt bei mir am Rechner, für den Fall, dass ich zurück muss. Bin ich froh drum, auch wenn ich’s bisher nicht gebraucht hab.
Wie die neuen Augen entstanden sind
Das Display zeigt jetzt 21 selbst generierte Augen in Cyan, formatfüllend auf dem 2-Zoll-Bildschirm, im Cozmo-Stil. Der Look ist an das Emopet angelehnt, das mir als Referenz für ausdrucksstarke, minimalistische Roboteraugen gefallen hat. Die Ruhe-Animation hat 109 Frames und läuft über 25 Sekunden, erzeugt mit einem eigenen Python-Skript über Pillow. Ein Detail, das ich unterschätzt hab: Wenn man bei der Bildwiederholung disposal=1 für Teilbild-Updates nutzt und die Bilddauern als Liste statt als Einzelwert übergibt, fasst Pillow identische Frames automatisch zusammen. Das spart am Ende spürbar Ladezeit auf einem Gerät mit begrenztem Flash.
Schön find ich, dass das Gesicht jetzt tatsächlich zu einer Persönlichkeit passt, und nicht mehr zu einem generischen Sprachassistenten aus einem Kickstarter-Kit.
Wie sich das jetzt in den Alltag einfügt
Der eigentliche Grund, warum ich mir den Aufwand überhaupt angetan hab, ist nicht das Firmware-Basteln an sich. Es ist, dass Vinci jetzt Dinge kann, die eine Stock-Firmware strukturell nicht hinbekommt. Zwei Automationen laufen bei mir inzwischen im Hintergrund. Die eine begrüßt mich, sobald mein Standort in Home Assistant auf „Büro“ wechselt. Die andere erinnert mich fünfzehn Minuten vor einem Termin, den ich in meinem Kalender eingetragen hab, und zwar ungefragt, ohne dass ich das Wake-Word sagen muss.
Beide Automationen hab ich mit einer Sperre versehen, die mir im Nachhinein wichtiger vorkommt als die Automation selbst: Solange Kamera oder Mikrofon meines Mac mini gerade aktiv sind, also während eines Video-Calls, bleibt Vinci still. Eine Assistentin, die mitten in einem Kundengespräch plötzlich lospiaudert, ist kein Feature, das ist ein Grund, das ganze Projekt wieder auszuschalten. Ich hab über den Aufbau solcher KI-Automationen in Home Assistant an anderer Stelle ausführlicher geschrieben, dort geht’s aber eher um YAML-Fehlersuche und Dashboard-Bau. Hier ist der Unterschied, dass die KI nicht mehr nur Konfiguration schreibt, sondern selbst als Teilnehmer im Haus steckt, mit eigenem Mikrofon und eigener Stimme.
Nebenbei ist mir beim Testen dieser Automation aufgefallen, dass die morgendliche Routine bei uns daheim inzwischen sowieso ziemlich durchautomatisiert ist, Licht, Tagesplan, Wetterbriefing, das läuft längst nebenbei. Vinci auf dem StackChan reiht sich da eher ein, als dass sie etwas völlig Neues eröffnet. Genau das find ich aber bemerkenswert: Ein einzelnes Gerät für rund 110 Euro wird erst durch die Anbindung an alles andere im Haus wirklich nützlich, nicht durch sich selbst.
Was noch nicht funktioniert, und das sag ich bewusst dazu
Ich will hier nicht so tun, als wär alles fertig. Die zwei Servos lassen sich über die aktuelle Firmware noch nicht ansteuern, das braucht eine eigene Implementierung des Bus-Protokolls für die verbauten Servomotoren. Eine Brücke für Lautstärke, Theme und Kamerasteuerung aus der Ferne fehlt noch komplett. Die Zeit wird dem Gerät bei der Verbindung aktuell nicht mitgeliefert, weshalb es beim Start kurz meldet, dass die Systemzeit nicht gesetzt ist. Und der GPIO-0-Konflikt beim Knopf ist immer noch offen, ich hab ihn bisher nur umschifft, nicht gelöst.
Als Nebenbefund beim Testen der Anwesenheitsautomatik ist mir außerdem aufgefallen, dass mein Smart-Home-System meine Position noch von einem Gerät bezogen hat, mit einer eingefrorenen GPS-Position in dem Ferienhaus in der Toskana, wo wir letzte Woche waren. Hat mit dem StackChan nix zu tun, aber genau solche Nebenbefunde findet man eben nur, wenn man ein System wirklich durchtestet und nicht nur den einen Use-Case anschaut, für den man’s gerade baut. Ich vermute, dass das bei den meisten Smart-Home-Setups so schlummert, kleine Karteileichen, die niemandem auffallen, weil sie nirgends stören, bis man aus einem völlig anderen Grund genauer hinschaut.
Die Servos sind für mich der wichtigste offene Punkt, aus einem einfachen Grund: Erst wenn sich der Kopf bewegt, wenn Vinci beim Nachdenken zur Seite schaut oder beim Lachen leicht nickt, wird aus dem Display-Gadget ein Gegenüber, das sich anfühlt, als würde es wirklich reagieren. Das Bus-Protokoll für die verbauten Servomotoren ist dokumentiert, die Umsetzung ist reine Fleißarbeit, aber genau die Art von Fleißarbeit, die man sich für einen ruhigen Abend aufhebt, statt sie zwischen zwei Kundenterminen reinzuquetschen.
Ich lass den StackChan jetzt erstmal eine Weile so laufen, wie er ist, und sammle, was im Alltag noch stört. Die Servos und die fehlende Systemzeit stehen ganz oben auf der Liste für die nächste Runde. Bis dahin steht auf meinem Schreibtisch zum ersten Mal ein Gerät, das wirklich meins ist, und nicht nur ein umgelabeltes Kickstarter-Produkt. Und ehrlich gesagt ist genau das der Teil, der mich am meisten freut: nicht die einzelnen Patches, sondern dass ich jetzt jederzeit weiß, warum das Ding tut, was es tut.




