VersaGUI-py/AGENTS.md
Julian Appel 83429363c1 Add architecture/data-model/protocol reference docs
Human-facing reference documentation, split from AGENTS.md's agent-facing
domain rules and bug history (which stays there, not duplicated here):

- docs/architecture.md: layer diagram, module responsibilities, config
  storage location, the three GUI modes, and the port-exclusivity /
  multi-process caveats around concurrent access
- docs/data-model.md: the combined and legacy JSON formats, the binary
  SDeviceConfig/SDeviceProfile/SMacroTable NVM layout byte-for-byte, action
  types, LED fields, macro-slot conventions, button grid geometry
- docs/protocol.md: the 8-byte serial packet format, command/event tables,
  the read/write/status-poll flows, connection lifecycle, and error states

README.md now links to all three from a new "Dokumentation" section, and
AGENTS.md's outdated "docs/ tree isn't warranted yet" note is removed now
that it exists on explicit user request.
2026-08-14 23:19:13 +02:00

18 KiB
Raw Blame History

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.

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.pyVersaPadLink: 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 -- <python> versapad_mcp_server.py). Nutzt MCP-SDK v2 (Paket mcp, Klasse mcp.server.mcpserver.MCPServernicht 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.
  • Board-Serial-Tools schliessen den Link nach jedem Aufruf (get_board_status, load_from_board, write_to_boardfinally: _link.close()). Grund: VersaPadLink schliesst nie von selbst, ein einzelner Aufruf hätte sonst den exklusiven COM-Port dauerhaft für den Rest des MCP-Serverprozesses blockiert und Live-Sync/VersaGUI/den nächsten Aufruf mit "busy" ausgesperrt (am 2026-08-14 live so aufgetreten, siehe unten).
  • Bug beobachtet 2026-08-14: In diesem Agenten-Environment (Claude-Code- VSCode-Extension) können mehrere unabhängige versapad_mcp_server.py- Prozesse gleichzeitig laufen (bis zu 8 beobachtet, vermutlich durch wiederholte Tool-Ladevorgänge/Reconnects innerhalb einer Session) — jeder mit eigenem, nicht geteiltem In-Memory-State (_state["combined"]). Konkret beobachtet: set_button_*/set_macro + save_local() liefen korrekt auf einem Prozess, ein späterer write_to_board()-Aufruf landete aber auf einem anderen (frischen, leeren) Prozess und schrieb versehentlich eine leere Default-Config aufs Board, trotz {"ok": true}-Antwort. Fix: vor write_to_board() immer erst load_local() (liest die Datei frisch von der Platte, unabhängig davon welcher Prozess antwortet), und nach jedem Schreibvorgang mit load_from_board() + get_profile() gegenlesen statt dem ACK allein zu vertrauen — genau dieses Verify-Pattern hat den Fehler hier live aufgedeckt.

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

  • 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, Architekturgrenzen und Bug-Historie — jede Session aktualisieren, die daran etwas ändert oder etwas Wichtiges lernt.
  • docs/ (seit 2026-08-14, auf expliziten User-Wunsch) enthält die menschenlesbare Referenzdoku: architecture.md (Schichten, Prozess-/ Nebenläufigkeitsmodell, Config-Speicherort), data-model.md (JSON- Formate, binäres NVM-Layout, Geometrie, Enums), protocol.md (Serial-Wire-Protokoll). Bug-Historie/Domänenregeln bleiben bewusst nur in AGENTS.md, nicht dupliziert in docs/. Bei Änderungen am Binärformat/Protokoll/Datenmodell docs/data-model.md bzw. docs/protocol.md mitpflegen.

Prüfungen vor einem Commit an Binärformat/Protokoll:

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).