# AGENTS.md ## Project Goal Eigenständiges Begleit-Tool für das VersaPad-Makropad (Grovy311-Kontext, nicht Teil der fremden jappel-Repos VersaGUI/VersaMCU). Zeigt die Steuermatrix (4×5-Button-Grid + 4 Encoder) an, synchronisiert sie live mit dem Board und kann sie komplett neu programmieren — inzwischen ein schlankes Python-Pendant zur offiziellen VersaGUI (C#/.NET), plus ein MCP-Server, der dieselbe Programmierung per KI-Tool-Aufruf ermöglicht. Nutzerorientierte Einführung: [`README.md`](README.md). ## Aktuell unterstützte Architektur **Read-only-Schicht (JSON, beide Frontends):** - `versapad_data.py` — Decoding für Anzeige: JSON laden, HID-Keycode/ Consumer-Usage/Modifier → lesbarer Text, Grid-Geometrie (`index = spalte*5+reihe`), `hid_key_choices()`/`consumer_choices()`. `app_dir()` liefert das Verzeichnis für die eigene Config (`versapad_combined.DEFAULT_PATH`) — bei der `.exe` der Installations- ordner, sonst der Projektordner, siehe Installierbarkeit-Notiz unten. `CONFIG_PATHS` bleibt bewusst auf dem OneDrive-Desktop hartkodiert (Lese- Interop mit einem JSON-Export der offiziellen VersaGUI, kein von diesem Tool geschriebenes Format, siehe dort). - `server.py` — stdlib `http.server`, generiert HTML pro Request neu, Profil-Wechsel über `?profile=0|1|2`, Auto-Reload alle 4s. Rein lesend, kein Programmiermodus (bewusst einfach gehalten). **Binär-/Serial-Schicht (Programmiermodus + MCP):** - `versapad_protocol.py` — pack/unpack für `SDeviceConfig` (740B, alle 3 Profile) und `SMacroTable` (512B, 32 Slots) + CRC16 (Poly 0x1021, Init 0xFFFF), 1:1 aus den Firmware-Structs. **Gegen echtes Board validiert:** read → unpack → pack ist byte-identisch zum Original, inkl. CRC. - `versapad_serial.py` — `VersaPadLink`: liest/schreibt komplette Config + Makros per 8-Byte-Paket-Protokoll, Board-Identifikation per VID/PID `239A:0042`. Schreiben ist sicher im Sinne von "kann NVM nicht zerlegen" — Firmware prüft Magic/CRC/Keycode-Bereich vor jedem Save, antwortet sonst nur mit NACK. `read_active_profile()` (für Live-Sync-Polling) nutzt seit 2026-08-07 `CMD_READ_STATUS`/`EVT_STATUS` (0x06/0x86, ein Antwortpaket) statt eines vollen `CONFIG_READ`-Dumps (124 Pakete) — Letzterer blockiert die Firmware in `poll_vendor()` lang genug, dass laufende LED-Pulse-Animationen sichtbar stottern (Root Cause + Fix in VersaMCU-Commit "Add lightweight READ_STATUS command..."). **Braucht entsprechend neue Firmware auf dem Board** — mit altem `versapad`-Firmwarestand liefert `READ_STATUS` schlicht Timeout, kein Absturz. - `versapad_combined.py` — Ein-Datei-Format (alle 3 Profile + Makros + **nur lokal gespeicherte** Profilnamen), Default-Pfad `versapad_data.app_dir()\versapad_config_all.json` (neben der Installation, nicht mehr hartkodiert auf einer bestimmten Maschine). Passt zum Wire-Protokoll: `CONFIG_BEGIN/COMMIT` überträgt ohnehin immer den kompletten 740B-Block. `load_or_fetch()` legt bei fehlender Datei automatisch eine neue an — erst Versuch per Serial vom Board, sonst als leere `default_combined()` (kein Board noetig fuer die Erstbenutzung). `read_profile_names()` liest nur die Namen (kein Board-Zugriff, fuer Tab-Beschriftungen im Nur-Lese-Modus, siehe Installierbarkeit-Notiz). **UI:** - `desktop_viewer.py` — Tkinter, flaches Design, drei unabhängige Modi (Nur lesen / Live-Sync / Programmiermodus, siehe Kritische Domänenregeln), Tray-Icon statt Taskleisten-Minimierung, Info-Button mit MCP-Doku. - `action_dialog.py` — Bearbeiten-Dialoge (`ActionEditDialog`, `MacroStepsDialog`) für den Programmiermodus. **MCP-Server:** - `versapad_mcp_server.py` — registriert als projektgebundener MCP-Server "versapad" über `.mcp.json` im Projektordner (Alternative: `claude mcp add -s user versapad -- versapad_mcp_server.py`). Nutzt MCP-SDK v2 (Paket `mcp`, Klasse `mcp.server.mcpserver.MCPServer` — **nicht** `FastMCP` aus `mcp.server.fastmcp`, das existiert in dieser SDK-Version nicht mehr, wurde umbenannt). `@mcp.tool()`-dekorierte Funktionen bleiben direkt aufrufbar (kein `.fn`-Unterschied wie bei älteren FastMCP-Versionen). Tool-Liste: siehe README oder `MCP_INFO_TEXT` in `desktop_viewer.py`. Vollständige Modulübersicht mit Zeilenreferenzen bei Bedarf direkt im Code nachschlagen — die Dateien sind klein genug, dass eine separate `docs/current-architecture.md` hier keinen Mehrwert hätte (siehe Dokumentation und Verifikation unten für die Größeneinschätzung). ## Kritische Domänenregeln - Geometrie: `index = spalte*5 + reihe`, Reihe 0 = oben, Reihe 4 = unten (Firmware-Reihenfolge). Nicht raten, nicht neu herleiten. - Encoder 0 `sw` ist auf allen 3 Profilen der Profilwechsel — nicht ohne Rücksprache mit dem User ändern. - Makros sind eine EINE globale 32-Slot-Tabelle, nicht pro Profil. Slot 0-19 = MX-Button-Index, Slot 20-31 = `20 + enc*3 + act_idx`. Zwei Profile, die denselben Slot referenzieren, spielen dieselben Schritte ab. - Ein Makro-Schritt mit `keycode=0` beendet die Sequenz (Firmware-Konvention) — keine Lücken vor dem letzten belegten Schritt lassen. - Makro-Schritte erlauben nur Strg/Shift/Alt als Modifier, kein Win (passend zum Original-`ActionDialog.cs`-Verhalten). - Profilnamen leben NUR lokal in `versapad_config_all.json` (`profile_names`) — die Firmware-Structs haben keinen Platz für einen String (Header exakt 32B, jedes Profil exakt 236B, alles verplant). Sie landen nie aufs Board, egal welcher Schreibpfad benutzt wird. - Der COM-Port ist exklusiv. Live-Sync und Programmiermodus schalten sich gegenseitig aus (ein `VersaPadLink` kann nicht von zwei Konsumenten gleichzeitig genutzt werden); VersaGUI läuft als Tray-App dauerhaft im Hintergrund weiter, auch wenn nur ihr Konfigurationsfenster geschlossen wird — für Parallelbetrieb mit diesem Tool muss sie über ihr eigenes Tray-Menü ("Beenden") wirklich beendet werden. - Programmiermodus-Änderungen werden bei jeder Bearbeitung automatisch in `versapad_config_all.json` gespeichert (nicht erst bei explizitem "Datei speichern"). Der Nur-Lese-Modus bevorzugt dieselbe Datei, falls vorhanden, und fällt sonst auf die klassischen `versapad_config{1,2,3}.json` zurück — beide Ansichten müssen dieselbe Quelle zeigen, sonst wirkt eine Bearbeitung "verschwunden". - **Installierbarkeit verbessert 2026-08-14:** Drei Probleme beim Weitergeben an andere Leute behoben. (1) `build_and_deploy.ps1` starb bei fehlenden Paketen (pyinstaller/pystray/pillow) kommentarlos, v.a. bei Doppelklick im Explorer, weil das Fenster sich sofort schließt. Fix: `requirements.txt` (alle Pakete an einer Stelle, README und Skript nutzen dieselbe Datei), Skript installiert sie selbst per `pip install -r`, läuft komplett in try/catch, prüft `$LASTEXITCODE` nach jedem nativen Aufruf, und pausiert am Ende (Erfolg wie Fehler) auf Tastendruck -- `-NoPause` für CI/Automation. (2) `versapad_combined.DEFAULT_PATH` war hartkodiert auf `~\OneDrive\Desktop` einer bestimmten Maschine -- für andere Nutzer unbrauchbar. Fix: `versapad_data.app_dir()` (neue Funktion) liefert bei der gebauten `.exe` deren Installationsordner (`sys.executable`-Verzeichnis), sonst den Projektordner (`__file__`- Verzeichnis) -- `DEFAULT_PATH` hängt jetzt daran, landet also immer neben der laufenden Installation. `versapad_data.CONFIG_PATHS` (Lese-Interop mit der C#-VersaGUI) bleibt bewusst auf dem Desktop, siehe oben. (3) Fehlte die Config UND war kein Board erreichbar, blockierte das Tool mit einer Fehlermeldung statt zu starten. Fix: `load_or_fetch()` legt jetzt bei Board-Fehlschlag eine leere `default_combined()` an statt `RuntimeError` zu werfen -- Erstbenutzung ganz ohne vorhandene Config oder Board funktioniert jetzt. (4) Profilnamen waren zusätzlich hartkodiert in `versapad_data.PROFILE_NAMES` (Dict, jetzt entfernt, ersetzt durch `NUM_PROFILES = 3`) und liefen der JSON-`profile_names` parallel -- Programmiermodus zeigte umbenannte Profile, Lesemodus/Browser-Ansicht weiterhin die alten Namen. Fix: `server.py` und `desktop_viewer.py` lesen Namen jetzt immer aus der kombinierten JSON (`combined["profile_names"]` bzw. `versapad_combined.read_profile_names()`), eine einzige Quelle der Wahrheit für alle Frontends. - **Bug behoben 2026-08-08:** `server.py` (`vp.load_profile()`) und `desktop_viewer.py` (`_current_profile_view()`) lasen im Nur-Lese-Modus hart von den Desktop-JSONs -- fehlten sie (z.B. User loescht sie), gab es eine ungefangene `FileNotFoundError` bzw. "Config-Datei fehlt"-Anzeige, obwohl das Board die Config laengst dauerhaft im NVM haelt. Fix: neue `versapad_combined.fetch_from_board()`/`load_or_fetch()` -- fehlt die Kombi-JSON, wird sie automatisch per Serial vom Board neu aufgebaut und als Cache gespeichert (self-healing), nur bei unerreichbarem Board (Port belegt/kein Board) bleibt der Fallback auf die alten Einzel-JSONs bzw. eine Klartext-Fehlermeldung. In `desktop_viewer.py` nur versucht, wenn Live-Sync aus ist (sonst haelt der Serial-Hintergrundthread den COM-Port -- zwei gleichzeitige Zugriffe auf denselben `self.ser` waeren eine Race Condition). Die Desktop-JSONs sind damit reiner Lesecache, kein Pflegeaufwand mehr fuers Board-Backup. - **Bug behoben 2026-08-07:** `_on_toggle_editing()` initialisierte `self.combined` beim ersten Aktivieren des Programmiermodus mit `vcomb.default_combined()` — das seedet ALLE 3 Profile aus den alten Einzel-JSONs `versapad_config{1,2,3}.json`, nicht aus der aktuellen `versapad_config_all.json` oder vom Board. Ein Klick auf "Zum Board übertragen" hat dadurch beim Testen alle 3 Profile auf einen veralteten Stand zurückgesetzt, obwohl nur ein Profil-Tab sichtbar bearbeitet wurde — der Schaden an den anderen beiden Profilen blieb unbemerkt, bis explizit jedes Profil einzeln gegengelesen wurde. Fix: lädt jetzt zuerst `vcomb.DEFAULT_PATH`, fällt nur bei fehlender/kaputter Datei auf `default_combined()` zurück. Bei jedem "komisches Layout"-Report hier immer ALLE 3 Profile prüfen, nicht nur das gemeldete. - HID-Tasten-Auswahl im Programmiermodus ist ein Dropdown, kein Tastendruck-Capture (bewusst — kein WinAPI-Hook, um keinen AV-Fehlalarm wie bei den Fensterverstecktricks in anderen Projekten zu riskieren). - Zeichentasten-Labels zeigen eine US-Layout-Näherung, nicht das tatsächlich aktive Windows-Tastaturlayout (die echte VersaGUI löst das über `GetKeyNameText()`, das bilden wir ohne WinAPI-Call nicht nach). - Tk-Aufrufe (`self.after()`, Widget-Konfiguration) NIE direkt aus einem Fremdthread (Serial-Thread, pystray-Thread) — hat in einer früheren Version einen stillen Absturz verursacht. Threads legen Ergebnisse nur in eine `queue.Queue`, der Main-Thread holt sie per `after()`-Polling ab. - `.exe` (PyInstaller) läuft NICHT vom Netzlaufwerk aus (`Z:\Git\...`, SMB-Share) — Windows blockiert das Nachladen der `_internal`-DLLs von einem Netzwerkpfad ohne jede Fehlermeldung. Bauen scheitert dort zusätzlich an Tcl/Tk-`tzdata`-Pfadlängen. `build_and_deploy.ps1` kopiert den Quellcode deshalb zuerst nach `%TEMP%`, baut nur dort, deployt nach `%LOCALAPPDATA%`. - `--onedir`, nicht `--onefile` beim PyInstaller-Build — Single-File-Bundles lösen öfter AV-Fehlalarme aus (sehen strukturell wie ein Packer aus). ## Existing-Codebase-Regel Es gibt bereits eine Codebasis mit validierter, gegen die echte Firmware getesteter Logik (Binärformat, CRC, Protokoll). Nicht blind neu raten oder umschreiben. Vor größeren Änderungen am Binärformat/Protokoll immer gegen die Firmware- und GUI-Quellen abgleichen, nicht aus dem Gedächtnis rekonstruieren: - `VersaGUI/src/ActionDialog.cs` (HidKeyName, s_consumer, Modifier-Bits) - `VersaGUI/src/Protocol.cs` (Paket-IDs, Chunk-Größen) - `VersaMCU/src/config/nvm_config.h` + `.cpp` (SDeviceConfig-Layout, CRC) - `VersaMCU/src/config/action.h` (SAction, ActionType-Enum-Werte) - `VersaMCU/src/config/macro_config.h` + `.cpp` (SMacroTable-Layout) - `VersaMCU/src/CButton.h` (LEDAnim-Enum-Werte) - `VersaMCU/src/CMainController.cpp` (welche Commands die Firmware *tatsächlich* behandelt — `Protocol.cs` definiert mehr Konstanten als zwingend implementiert sind, immer hier gegenchecken) Frühere falsche Annahme, die hier stand: Makros seien nur per SWD/JTAG schreibbar. Falsch — `USB_CMD_MACRO_BEGIN/DATA/COMMIT` in `CMainController.cpp` sind normal über USB implementiert. Korrigiert, auch im `versapad`-Skill (`~/.claude/skills/versapad/SKILL.md`). ## Implementierungsdisziplin In Phasen arbeiten, nicht mehrere unabhängige Features in einem Rutsch ohne Zwischenverifikation bauen. Vor jeder Änderung am Binärformat/Protokoll: gegen ein echtes Board testen (read → unpack → pack → Vergleich auf Byte-Identität), nicht nur gegen Beispieldaten. Vor jedem Schreibvorgang aufs Board: erst mit unveränderten Daten testen (Identity-Write), bevor echte Änderungen geschrieben werden. ## Naming Code-Identifier englisch (Python-Konvention: `pack_config`, `read_macros`, `ActionEditDialog`). UI-Text, Kommentare und Docstrings deutsch (Projekt- und User-Konvention). Domänenbegriffe aus der Firmware direkt übernehmen, nicht neu erfinden: `profile`, `action`, `led`, `slot`, `macro`, `encoder`, `chunk`. Keine generischen Namen wie `data`/`item`/`entry`, wo eine Bedeutung existiert — Ausnahme: `data` als Feldname ist durch die Firmware selbst vorgegeben (`SAction.data`), dort beibehalten statt umzubenennen. ## Deferred Work - Volle `docs/`-Baumstruktur (siehe Dokumentation und Verifikation unten — Projektgröße rechtfertigt das aktuell nicht, kein DB-/API-Dienst) - Board-seitiges Umschalten des aktiven Profils per Button in der GUI — explizit vom User abgelehnt ("lass uns weg"), Live-Sync bleibt read-only - Profilnamen aufs Board schreiben — technisch unmöglich (kein Platz im Firmware-Struct), bleibt lokal - Tastendruck-Capture statt Dropdown im Programmiermodus — bewusst vermieden (WinAPI-Hook-Risiko) - Hintergrund-Thread für "Vom Board laden"/"Zum Board übertragen" — laufen aktuell synchron im UI-Thread (kurzzeitiges Einfrieren möglich) - Vorgefertigte `.exe` im Repo/als Release-Asset — bewusst nicht committet (Build-Artefakt), Anleitung zum Selbstbauen steht in `README.md` Nicht an diesen Punkten arbeiten, ohne dass der User es explizit anfragt. ## Dokumentation und Verifikation - `README.md` ist der Einstiegspunkt (Installation, Nutzung, Architektur- Überblick). - `AGENTS.md` (diese Datei) ist die agentenseitige Quelle der Wahrheit für Domänenregeln und Architekturgrenzen — jede Session aktualisieren, die daran etwas ändert oder etwas Wichtiges lernt. - Größeneinschätzung nach Projekt-Dokumentationsstandard: kleines/mittleres Tool ohne eigene Datenbank und ohne persistenten API-Dienst (der Browser-Server ist ein einfacher lokaler Lese-Viewer, kein Mehrbenutzer-Backend) → `README.md` + `AGENTS.md` sind Pflicht und vorhanden, ein voller `docs/`-Baum ist nicht angemessen. Prüfungen vor einem Commit an Binärformat/Protokoll: ```bash python -m py_compile *.py ``` Danach ein Live-Testskript gegen ein angeschlossenes Board laufen lassen (read → unpack → pack → Bytevergleich, siehe Existing-Codebase-Regel) — es gibt keine automatisierten Unit-Tests dafür, die Verifikation läuft manuell/interaktiv pro Session. Committen: prägnante Commit-Message je abgeschlossenem, verifiziertem Arbeitspaket. Nach jedem Push: alle bekannten Remotes prüfen (`origin` auf GitHub, `jappel` auf git.jappel.io) — beide müssen synchron bleiben, siehe Speicher-Notiz "Multi-Remote-Repos synchron halten". **PR-Erstellung auf git.jappel.io per API/curl mit Access-Token wird vom Bash-Classifier geblockt** (Auto-Mode, gilt auch für `git credential fill`), siehe Speicher-Notiz "Bash-Klassifikator blockt Credential/Auth-Schreibzugriffe". Branch pushen geht (nutzt den Git-eigenen Credential-Helper, kein Token im Klartext im Bash-Aufruf), den fertigen PR muss der User über den von Forgejo nach dem Push ausgegebenen Compare-Link selbst anlegen (oder Claude einen Token geben, der dann NICHT wiederverwendbar im Bash-Aufruf landen darf, sondern nur für den einen `curl`-Call — auch das kann der Classifier trotzdem blocken, dann bleibt nur der manuelle Link).