VersaGUI-py/AGENTS.md
Julian Appel 189ec01e68 Merge branch 'main' into dev/jappel
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.
2026-08-15 18:16:00 +02:00

23 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. 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:

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