Das StackChan-Projekt, von Anlauf 1 bis zum drehenden Kopf

Das komplette StackChan-Projekt: von der gescheiterten Custom Firmware im Mai bis zu Servo-Steuerung und Gesichtsverfolgung heute. Mit Code.

Ein 110-Euro-Desktop-Roboter, zwei gescheiterte Firmware-Anläufe, ein pragmatischer Workaround, der Monate gehalten hat, und am Ende doch noch der Servo-Treiber, den ich im Mai für unmöglich gehalten hab. Hier ist die komplette technische Geschichte des Projekts, von der ersten Idee bis zur heute laufenden Gesichtsverfolgung.

Die Hardware und die Idee

M5Stack StackChan, CoreS3-Variante von OpenELAB. ESP32-S3, 240 MHz Dual-Core, 16 MB Flash, 8 MB PSRAM, zwei Feetech SCS0009-Servos im Hals für Yaw und Pitch, 2-Zoll-IPS-Display, GC0308-Kamera, 12 RGB-LEDs, Dual-Mikrofone. Die Idee dahinter: meiner bestehenden Desktop-KI-Assistentin VINCI ein physisches Gesicht geben, statt sie nur im Chat-Fenster leben zu lassen. Ein Gerät, das mein Smart Home kennt, mein Wetter, meinen Kalender, und das alles mit Servo-Motoren statt nur mit Text.

Anlauf 1: neun Stunden, keine Custom Firmware

Der erste Versuch war Custom Firmware mit vollständiger Kontrolle über Servos, Wake-Word und Persönlichkeit. Start mit AI_StackChan_Ex, einem Arduino/PlatformIO-Framework. M5Unified 0.2.7 hat sich mit der OpenELAB-Boardvariante nicht vertragen, Backlight lief, Panel-Rendering nicht, schwarzer Bildschirm. Vier GitHub-Issues gelesen, keine Lösung. Wechsel auf die Moddable-Toolchain, ESP-IDF 5.3.1 installiert, danach klassisches Python-Environment-Chaos zwischen venv, pyenv und pip-Rollbacks. Nach neun Stunden Abbruch, mit der Diagnose: ein offenes Issue im M5GFX-Repository blockiert den Weg.

Der pragmatische Workaround: Stock-Firmware plus eigener Server

Statt Custom Firmware: die Werksfirmware xiaozhi-esp32 behalten, aber die OTA-URL im NVS-Bereich (Non-Volatile Storage) per esptool und esp-idf-nvs-partition-gen umbiegen, sodass das Gerät bei meinem eigenen Server anklopft statt bei der Xiaozhi-Cloud. Als Server-Basis diente der Go-Fork rudyll/stackchan_ha_addons, erweitert um vier eigene Patches: ota_proxy.go für eine korrekte öffentliche WebSocket-URL, ein set_emotion-Tool für die Gemini Live API, eine Entity-Index-Erweiterung mit drei Fallback-Stufen für die Home-Assistant-Bereichszuweisung, und eine Memory-Persistenz-Schicht unter /data/vinci-memory.json. Deployment als Dockerfile-Resource in Coolify auf Hetzner, Cloudflare-Proxy statt Tunnel für die externe Erreichbarkeit.

Am 21. Mai lief VINCI end-to-end: Smart-Home-Steuerung, Sensorabfrage, Kalender, Wetter, dynamisches Emotion-Display, alles auf Österreichisch, nach einem chinesischen Wake-Word, weil die Stock-Firmware das nicht anders zulässt. Nur die Servos rührten sich nicht. Die Stock-Firmware koppelt Emotion-Frames ausschließlich ans Display.

Nachmittags-Update: Kamera, Push und Briefing

Noch am selben Tag kamen drei Erweiterungen dazu. Ein analyze_camera-Tool holt einen Snapshot über die Home-Assistant-Kameraproxy-Route, schickt ihn an die Gemini Vision API und liefert eine Beschreibung zurück, nur als Audio, weil die Stock-Firmware kein Bild aufs Display bringen kann. Der /vinci/push-Endpoint erlaubt proaktive Ansagen ohne Wake-Word, über Gemini TTS synthetisiert und per PCM-Stream ans Gerät geschickt. Der /vinci/briefing-Endpoint liefert ein Tagesbriefing auf Knopfdruck, mit paralleler Abfrage von Kalender, Wetter und Nachrichten. Eine Home-Assistant-Automation löst diesen Push fünfzehn Minuten vor jedem Kalendertermin aus.

Zwei Lektionen aus dieser Session, die ich seither immer wieder anwende: gemini-2.5-flash-native-audio-latest stürzt zuverlässig ab, sobald komplexe Multi-Step-Tools in der Live-Tool-Liste stehen, also gehören solche Tools als eigener HTTP-Endpoint implementiert, nicht in die Live-API. Und thinkingBudget: 0 muss bei der Text-API explizit gesetzt werden, sonst frisst das Modell seine gesamte Antwortlänge in internen Denk-Tokens auf.

Die Korrektur der Fehldiagnose

Im September bin ich aus Sturheit noch einmal an die Custom Firmware ran. Das M5GFX-Issue war längst beantwortet und spielte gar keine Rolle mehr. Der eigentliche Blocker war meine ESP-IDF-Version. xiaozhi-esp32 verlangt inzwischen zwingend 6.0.1 oder neuer, ich hatte 5.3.1 im Einsatz. Mit ESP-IDF 6.1 unter WSL Ubuntu lief der Build sofort durch, inklusive vollständiger CoreS3-Boarddefinition mit GC0308-Kamerasupport. Build-Kommando: python ./scripts/build.py m5stack/core-s3 --language de-DE --wake-word wn9_jarvis_tts.

Die Partitionstabelle des Geräts wurde per esptool read-flash 0x8000 0xc00 ausgelesen und byte-identisch nachgebaut, MD5-verifiziert, damit NVS bei 0x9000 unangetastet bleibt und WLAN-Zugangsdaten sowie die alte Servo-Kalibrierung jeden Flash überleben. Ergebnis: deutsches UI, Wake-Word Jarvis, 21 selbst gerenderte cyanfarbene Augen, dunkles Theme, Power-Save abgeschaltet, Kanal zum Server bleibt dauerhaft offen statt nur nach Wake-Word.

Dabei fiel auch ein monatelanger Bug: /vinci/push meldete regelmäßig „kein Gerät verbunden“, weil der interne Firmware-Timeout nur bei echten Datenframes zurückgesetzt wird, mein Server aber nur stille WebSocket-Pings schickte. Fix: alle 50 Sekunden zusätzlich ein JSON-Ping mitschicken, das die Firmware zwar ignoriert, aber als Datenframe zählt.

Code-Review mit Opus 5.5

Bevor ich mich an die Servos gewagt hab, hab ich das gesamte Projekt von Claude Opus 5.5 durchsehen lassen. Ergebnis: zwei Fehler in der Home-Assistant-Entity-Abfrage, die zu verstümmelter Sprachausgabe führten, bevor der Text überhaupt bei der Text-zu-Sprache-Strecke ankam. Genau die Art Fehler, die man beim x-ten Mal Drüberschauen selbst übersieht, weil man die Ursache am falschen Ende sucht.

Servo-Treiber und Gesichtsverfolgung

head_motion.h spricht direkt mit den zwei Feetech-Servos über UART1, 1 Mbaud, TX auf GPIO 6, RX auf GPIO 7, ID 1 für Yaw, ID 2 für Pitch, 0,3125 Grad pro Schritt, Servo-Strom über den PY32-IO-Expander via I²C. Jede Bewegung läuft über einen kritisch gedämpften Nachführer mit Tempolimit (25 °/s) und Beschleunigungsgrenze (80 °/s²), 20 Grad in etwa 1,7 Sekunden, kein Überschwingen.

face_tracker.h nutzt esp-dl mit human_face_detect, rund 2,5 Bilder pro Sekunde, das größte Gesicht steuert den Kopf. Vier Bugs hintereinander: invertiertes Pitch-Vorzeichen drückte den Kopf gegen den Anschlag, die 0-Grad-Ruhehaltung zeigte nur Tastatur und T-Shirt, das Kamerabild war seitenverkehrt, und die Standard-Erkennungsschwelle von 0,5 fand von Tischhöhe aus nur rund 7 Prozent der Bilder, mit 0,25 bis 0,3 waren es 81 Prozent, per A/B-Test direkt auf dem Gerät verifiziert.

Proaktives Sprechen hatte am Ende drei getrennte Ursachen: die Firmware öffnete den Kanal nur beim Wake-Word, ein ungepinnter git clone main im Dockerfile zog stillschweigend einen Upstream-Bug mit, der den Audio-Stream stumm hielt, und der Home-Assistant-Timeout von 10 Sekunden war zu kurz für Formulierung plus Synthese.

Aktueller Stand und offene Baustellen

Der Kopf bewegt sich, folgt meinem Gesicht, sucht mich, wenn ich den Raum verlasse, legt sich nach vier Sekunden ohne Treffer schlafen. Vinci meldet sich proaktiv bei Kalenderterminen, begrüßt mich im Büro, und ein Gespräch endet jetzt nach 20 Sekunden ohne Antwort, statt endlos zuzuhören.

Offen bleiben sporadische Neustarts nach Server-Redeploys, eine Diagnose-Firmware läuft mit. set_emotion wird vom Modell trotz explizitem Prompt-Hinweis nie aufgerufen, Emotionen kommen aktuell nur aus speakPCM und der zeitabhängigen idleMood(). Uhrzeiten werden von Gemini gelegentlich falsch ausgesprochen.

Firmware als Patch gegen xiaozhi-esp32, Server-Dockerfile und Home-Assistant-Beispiele liegen offen auf GitHub: github.com/7H3-CH053N/StackChan-CustomFW

share that. please.

LinkedIn
Facebook
Threads
Telegram
WhatsApp
Email
Alex Januschewsky, KI-Berater und Prompt Rocker
Über den Autor
Alex Januschewsky
KI-Berater & Prompt Rocker // Salzburg, AT

Ich arbeite seit 1989 in Kommunikation und Werbung, heute als KI-Berater unter der Marke Prompt Rocker. Ich baue Dinge selbst: mit Claude Code, n8n, WordPress und Coolify auf Hetzner. Vibe Coding ist für mich kein Begriff, sondern tägliche Praxis. Auf vibecraft.rocks dokumentiere ich, was dabei entsteht.

ship safe.