Reconciles two independent lines of work: main cherry-picked and then extended dev/jappel's build-script/config-portability/rename-tab/ COM-port fixes, additionally fixing a config-loss bug (rebuild wiped the config when it lived in the install dir -- moved to roaming %APPDATA% instead) and adding free-text notes per action plus a frameless resizable window. dev/jappel keeps its .mcp.json registration and docs/ reference tree, which main deliberately left out. Conflict resolutions favored main's versions where the two sides solved the same problem (config location, build deploy target, COM-port release, tab rename) since main's fixes were validated against a real rebuild-wipes-config incident. docs/architecture.md and docs/data-model.md updated to describe the resulting %APPDATA% config path and the note field.
384 lines
23 KiB
Markdown
384 lines
23 KiB
Markdown
# 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 -- <python> 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`.
|
||
- **Board-Serial-Tools schliessen den Link nach jedem Aufruf** (`get_board_status`,
|
||
`load_from_board`, `write_to_board` — `finally: _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. Menschenlesbare Referenzdoku (Architektur, Datenmodell,
|
||
Protokoll) liegt in `docs/`, siehe „Dokumentation und Verifikation" unten.
|
||
|
||
## 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.
|
||
- **Notizen** (freier Text je Button/Encoder-Aktion, seit 2026-08-15) sind
|
||
aus demselben Grund rein lokal: leben im `"note"`-Feld JEDER Action
|
||
(`{"type","data","note"}`), nicht in einer separaten Struktur. `to_binary`/
|
||
`pack_config` ignorieren das Feld beim Schreiben (liest nur type/data),
|
||
`from_binary` liefert frisch vom Board immer `note=""` (Board kennt keine
|
||
Notizen) — `versapad_combined.merge_notes(neu, alt)` kopiert bestehende
|
||
Notizen nach jedem `load_from_board()`/`fetch_from_board()` zurück, sonst
|
||
gingen sie bei jedem Board-Refresh verloren. Action-Typ wechseln
|
||
(`set_button_key` etc.) darf die Notiz NICHT loeschen (baut die neue
|
||
Action ueber `_replace_action()`/`dlg.action["note"]` mit der alten Notiz),
|
||
nur `set_button_note()`/`set_encoder_note()`/das Notiz-Feld im
|
||
Programmiermodus-Dialog aendern sie gezielt.
|
||
- 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".
|
||
- **Config-Pfad darf NIE im Installationsverzeichnis liegen (2026-08-15,
|
||
Datenverlust-Bug):** `build_and_deploy.ps1` räumt sein Zielverzeichnis vor
|
||
jedem Deploy komplett ab (`Remove-Item $localDir -Recurse -Force`). Nachdem
|
||
`app_dir()` am selben Tag auf genau dieses Verzeichnis gezeigt hatte, hat
|
||
**jeder Rebuild die Nutzer-Config mitgelöscht**; die App hat sie danach
|
||
kommentarlos leer vom Board neu aufgebaut (`load_or_fetch()` →
|
||
`fetch_from_board()`), wodurch alle Notizen weg waren. Besonders tückisch:
|
||
Board-Bindings und LEDs sahen danach völlig normal aus, nur die rein
|
||
lokalen Felder (Notizen, Profilnamen) fehlten -- der Schaden ist also
|
||
unsichtbar, wenn man nur aufs Grid schaut. Fix: `app_dir()` liefert jetzt
|
||
`%APPDATA%\VersaPadViewer` (Roaming), getrennt vom Installationsordner in
|
||
`%LOCALAPPDATA%`; zusätzlich rettet `build_and_deploy.ps1` vorgefundene
|
||
`versapad_config*.json` aus dem Zielverzeichnis über den Deploy hinweg
|
||
(für Altinstallationen). **Regel für künftige Änderungen: Programm- und
|
||
Datenverzeichnis nie zusammenlegen, egal wie praktisch "alles an einem
|
||
Ort" klingt.**
|
||
- **Nur EIN Config-Pfad, unabhängig vom Startweg (2026-08-15):** `app_dir()`
|
||
hat bewusst KEINEN `sys.frozen`-Zweig mehr. Vorher sah die gebaute `.exe`
|
||
eine andere Datei als der aus dem Quellcode gestartete MCP-Server, was zu
|
||
zwei auseinanderlaufenden Configs führte (die `.exe` zeigte ein anderes
|
||
Profil 0 als der MCP-Server meldete). Wer hier wieder nach Startweg
|
||
unterscheidet, baut denselben Bug erneut ein.
|
||
- **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. **Diese app_dir()-Variante ist seit
|
||
2026-08-15 überholt und war aktiv schädlich, siehe oben "Config-Pfad
|
||
darf nie im Installationsverzeichnis liegen".**
|
||
`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.
|
||
- **Herkunft der drei Punkte oben + Tab-Umbenennen-Fix:** ursprünglich per
|
||
Cherry-Pick aus Julian Appels `dev/jappel`-Branch auf `main` übernommen
|
||
(der sich zeitgleich mit dem eigenen COM-Port-Fix entwickelt hatte, kein
|
||
Fork-Sync-Automatismus -- manuell gegengelesen). Auf `main` bewusst NICHT
|
||
übernommen wurde damals der `dist/VersaPadViewer`-Build-Zielpfad (statt
|
||
`%LOCALAPPDATA%\VersaPadViewer`) sowie `.mcp.json` und die `docs/*.md`-
|
||
Referenzdateien. **2026-08-15: `main` wurde zurück in `dev/jappel`
|
||
gemerged**, damit landen beide Linien wieder in einem Branch -- inklusive
|
||
`.mcp.json`, `docs/*.md` und dem `%LOCALAPPDATA%`-Zielpfad (der hat sich
|
||
im Merge durchgesetzt, siehe Config-Pfad-Bullets oben). Der Notes-Feature-
|
||
Teil dieser Historie ist der Grund, warum `docs/data-model.md` das
|
||
`"note"`-Feld nachträglich braucht.
|
||
- **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.
|
||
- **Randloses Fenster (seit 2026-08-15):** Die Titelleiste ist per reinem Tk
|
||
`overrideredirect(True)` ausgeblendet -- **niemals** per ctypes/WinAPI
|
||
nachhelfen (kein `SetWindowLongW`/`ShowWindow` auf das eigene Fenster),
|
||
siehe Speicher-Notiz "keine Selbstversteck-Fenstertricks": genau diese
|
||
Kombination hat in einem anderen Projekt Bitdefender-Fehlalarme
|
||
ausgelöst. Konsequenzen, die mitgebaut werden müssen: kein
|
||
Taskleisten-Eintrag (Rückweg nur über das Tray-Icon), kein Ziehen am
|
||
Rahmen (Kopfzeile ist deshalb per `<B1-Motion>` verschiebbar), keine
|
||
System-Buttons (eigene `✕`/`—` in der Kopfzeile, beide legen ins Tray)
|
||
und **keine Resize-Ränder**.
|
||
- **Der Größen-Anfasser hängt per `place()` an der Fensterecke, nicht
|
||
gepackt am Ende des Inhalts.** Gepackt verschwindet er, sobald das Fenster
|
||
kleiner als der Inhalt ist -- also exakt dann, wenn man ihn zum
|
||
Vergrößern bräuchte. `MIN_W/MIN_H` begrenzen das Verkleinern.
|
||
- Fensterhöhe muss zum Karteninhalt passen: seit die Karten eine Notizzeile
|
||
haben (`CARD_H` 84 → 114) braucht das Fenster ~960px, sonst liegen
|
||
Encoder-Bereich und Fußzeile unterhalb des Rands. Wer `CARD_H` ändert,
|
||
muss `geometry()` mit anpassen.
|
||
- 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.
|
||
|
||
**Änderungen an der Config immer aus der Sicht prüfen, die die laufende App
|
||
hat (2026-08-15 gelernt):** Claudes Bash- und PowerShell-Werkzeuge sehen
|
||
unter `C:\Users\chris\AppData\...` teilweise *unterschiedliche* Dateien —
|
||
derselbe Pfad lieferte gleichzeitig 29826 Bytes (Bash/MCP-Server) und
|
||
28455 Bytes (PowerShell/`.exe`). Konkret heißt das: **Config-Schreibvorgänge
|
||
über den versapad-MCP-Server (`save_local()`) erreichen die installierte
|
||
`.exe` nicht zuverlässig.** Das hat eine Fehlersuche über viele Runden
|
||
verschleppt, weil "Notizen sind in der Datei" (Bash) und "App zeigt keine
|
||
Notizen" gleichzeitig stimmten. Vorgehen:
|
||
- Config-Dateien, die die installierte App lesen soll, über **PowerShell**
|
||
schreiben/prüfen (`Get-Item`, dort ausgeführtes `python`), nicht über Bash.
|
||
- Bei "Änderung wirkt nicht"-Symptomen als Erstes Dateigröße/mtime aus
|
||
*beiden* Sichten vergleichen, bevor Code-Ursachen gesucht werden.
|
||
- `Z:\Git\...` (SMB-Share) ist von diesem Effekt nicht betroffen — Quellcode
|
||
und Build verhalten sich normal.
|
||
|
||
Verlässlich zum Ziel führt bei solchen Widersprüchen ein temporäres
|
||
Diagnose-Modul, das im gebauten Bundle mitläuft und `app_dir()`,
|
||
`DEFAULT_PATH`, `os.stat()` und den tatsächlich geladenen JSON-Inhalt in eine
|
||
Datei schreibt — damit war die Ursache in einem Durchlauf sichtbar, nachdem
|
||
Vermutungen mehrfach danebenlagen.
|
||
|
||
## 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:
|
||
```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).
|