AGENTS.md: die Regel "Auswahl ist ein Dropdown, kein Tastendruck-Capture" ist ueberholt -- Capture gibt es jetzt, aber weiterhin ohne WinAPI-Hook, und genau diese Grenze muss bleiben. Dazu die Layout-Naeherung (Y/Z vertauscht auf deutschem Layout, minus-Kollision bewusst zugunsten der US-Position aufgeloest), die neuen UI-Regeln (Dialogposition, Grab-Rueckgabe bei verschachtelten Dialogen, feste Panel-Hoehe, In-Memory-Ablage fuers Kopieren) und ein Verifikationsrezept fuer UI-Aenderungen inkl. der DPI-Falle beim Screenshot-Vergleich. Der Deferred-Work-Eintrag zum Capture faellt weg. README.md: neue Bedienung in den Features, Tastenkuerzel-Uebersicht, und die Einschraenkung praezisiert (nicht mehr "Dropdown statt Capture", sondern was die Fenster-basierte Erkennung nicht sehen kann). docs/architecture.md: _ModalDialog/_KeyCapture in der UI-Schicht, plus zwei neue Abschnitte zu den Grenzen der Erkennung und zur Kopier-Ablage. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
28 KiB
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(). Seit 2026-08-28 zusätzlichtk_event_to_hid()(Tk-Tastendruck → HID-Keycode+Modifier, siehe Tastendruck-Erkennung unten) undmacro_slot_choices()/macro_slot_from_choice()für die Slot-Auswahl.action_label()/annotate_profile()nehmen die Makrotabelle optional entgegen und zeigen dann statt „Makro (Slot 7)" die echte Tastenfolge.app_dir()liefert das Verzeichnis für die eigene Config (versapad_combined.DEFAULT_PATH) — bei der.exeder Installations- ordner, sonst der Projektordner, siehe Installierbarkeit-Notiz unten.CONFIG_PATHSbleibt 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— stdlibhttp.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ürSDeviceConfig(740B, alle 3 Profile) undSMacroTable(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/PID239A: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-07CMD_READ_STATUS/EVT_STATUS(0x06/0x86, ein Antwortpaket) statt eines vollenCONFIG_READ-Dumps (124 Pakete) — Letzterer blockiert die Firmware inpoll_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 altemversapad-Firmwarestand liefertREAD_STATUSschlicht Timeout, kein Absturz.versapad_combined.py— Ein-Datei-Format (alle 3 Profile + Makros + nur lokal gespeicherte Profilnamen), Default-Pfadversapad_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 leeredefault_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. Gemeinsame Basis_ModalDialog(Positionierung über dem Elternfenster, Enter/Escape, Verteilung der Tastenevents) und_KeyCapture(Tastendruck-Erkennung).
MCP-Server:
versapad_mcp_server.py— registriert als projektgebundener MCP-Server "versapad" über.mcp.jsonim Projektordner (Alternative:claude mcp add -s user versapad -- <python> versapad_mcp_server.py). Nutzt MCP-SDK v2 (Paketmcp, Klassemcp.server.mcpserver.MCPServer— nichtFastMCPausmcp.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 oderMCP_INFO_TEXTindesktop_viewer.py.- Board-Serial-Tools schliessen den Link nach jedem Aufruf (
get_board_status,load_from_board,write_to_board—finally: _link.close()). Grund:VersaPadLinkschliesst 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ätererwrite_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: vorwrite_to_board()immer erstload_local()(liest die Datei frisch von der Platte, unabhängig davon welcher Prozess antwortet), und nach jedem Schreibvorgang mitload_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
swist 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=0beendet 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_configignorieren das Feld beim Schreiben (liest nur type/data),from_binaryliefert frisch vom Board immernote=""(Board kennt keine Notizen) —versapad_combined.merge_notes(neu, alt)kopiert bestehende Notizen nach jedemload_from_board()/fetch_from_board()zurück, sonst gingen sie bei jedem Board-Refresh verloren. Action-Typ wechseln (set_button_keyetc.) darf die Notiz NICHT loeschen (baut die neue Action ueber_replace_action()/dlg.action["note"]mit der alten Notiz), nurset_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
VersaPadLinkkann 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.jsongespeichert (nicht erst bei explizitem "Datei speichern"). Der Nur-Lese-Modus bevorzugt dieselbe Datei, falls vorhanden, und fällt sonst auf die klassischenversapad_config{1,2,3}.jsonzurü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.ps1räumt sein Zielverzeichnis vor jedem Deploy komplett ab (Remove-Item $localDir -Recurse -Force). Nachdemapp_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 rettetbuild_and_deploy.ps1vorgefundeneversapad_config*.jsonaus 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 KEINENsys.frozen-Zweig mehr. Vorher sah die gebaute.exeeine andere Datei als der aus dem Quellcode gestartete MCP-Server, was zu zwei auseinanderlaufenden Configs führte (die.exezeigte 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.ps1starb 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 perpip install -r, läuft komplett in try/catch, prüft$LASTEXITCODEnach jedem nativen Aufruf, und pausiert am Ende (Erfolg wie Fehler) auf Tastendruck ---NoPausefür CI/Automation. (2)versapad_combined.DEFAULT_PATHwar hartkodiert auf~\OneDrive\Desktopeiner bestimmten Maschine -- für andere Nutzer unbrauchbar. Fix:versapad_data.app_dir()(neue Funktion) liefert bei der gebauten.exederen Installationsordner (sys.executable-Verzeichnis), sonst den Projektordner (__file__- Verzeichnis) --DEFAULT_PATHhä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 leeredefault_combined()an stattRuntimeErrorzu werfen -- Erstbenutzung ganz ohne vorhandene Config oder Board funktioniert jetzt. (4) Profilnamen waren zusätzlich hartkodiert inversapad_data.PROFILE_NAMES(Dict, jetzt entfernt, ersetzt durchNUM_PROFILES = 3) und liefen der JSON-profile_namesparallel -- Programmiermodus zeigte umbenannte Profile, Lesemodus/Browser-Ansicht weiterhin die alten Namen. Fix:server.pyunddesktop_viewer.pylesen 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 aufmainübernommen (der sich zeitgleich mit dem eigenen COM-Port-Fix entwickelt hatte, kein Fork-Sync-Automatismus -- manuell gegengelesen). Aufmainbewusst NICHT übernommen wurde damals derdist/VersaPadViewer-Build-Zielpfad (statt%LOCALAPPDATA%\VersaPadViewer) sowie.mcp.jsonund diedocs/*.md- Referenzdateien. 2026-08-15:mainwurde zurück indev/jappelgemerged, damit landen beide Linien wieder in einem Branch -- inklusive.mcp.json,docs/*.mdund dem%LOCALAPPDATA%-Zielpfad (der hat sich im Merge durchgesetzt, siehe Config-Pfad-Bullets oben). Der Notes-Feature- Teil dieser Historie ist der Grund, warumdocs/data-model.mddas"note"-Feld nachträglich braucht. - Bug behoben 2026-08-08:
server.py(vp.load_profile()) unddesktop_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 ungefangeneFileNotFoundErrorbzw. "Config-Datei fehlt"-Anzeige, obwohl das Board die Config laengst dauerhaft im NVM haelt. Fix: neueversapad_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. Indesktop_viewer.pynur versucht, wenn Live-Sync aus ist (sonst haelt der Serial-Hintergrundthread den COM-Port -- zwei gleichzeitige Zugriffe auf denselbenself.serwaeren eine Race Condition). Die Desktop-JSONs sind damit reiner Lesecache, kein Pflegeaufwand mehr fuers Board-Backup. - Bug behoben 2026-08-07:
_on_toggle_editing()initialisierteself.combinedbeim ersten Aktivieren des Programmiermodus mitvcomb.default_combined()— das seedet ALLE 3 Profile aus den alten Einzel-JSONsversapad_config{1,2,3}.json, nicht aus der aktuellenversapad_config_all.jsonoder 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 zuerstvcomb.DEFAULT_PATH, fällt nur bei fehlender/kaputter Datei aufdefault_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 (keinSetWindowLongW/ShowWindowauf 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_Hbegrenzen das Verkleinern. - Fensterhöhe muss zum Karteninhalt passen: seit die Karten eine Notizzeile
haben (
CARD_H84 → 114) braucht das Fenster ~960px, sonst liegen Encoder-Bereich und Fußzeile unterhalb des Rands. WerCARD_Händert, mussgeometry()mit anpassen. - Dialoge öffnen über dem Hauptfenster, nicht in der Bildschirmecke
(2026-08-28):
_ModalDialogbaut sichwithdraw()n auf, positioniert sich inrun()per_center_on_parent()und wird erst danndeiconify()t. Ohne das legt Tk jeden Toplevel bei+0+0an — bei 20 Tasten hintereinander wandert der Blick jedes Mal in die linke obere Ecke. Daswithdraw()gehört zwingend dazu, sonst blitzt der Dialog dort auf, bevor er springt. - Verschachtelte Dialoge geben den Grab zurück:
MacroStepsDialogläuft imActionEditDialog._finish()ruftgrab_release(), was den Grab des aufrufenden Dialogs mit wegnimmt —run()setzt ihn deshalb am Ende wieder, wenn der Parent ein_ModalDialogist. - Panel-Höhe im
ActionEditDialogist fix (grid_propagate(False)): sonst springt die Fenstergröße bei jedem Typwechsel (Keine/Taste/Makro/…) und OK/Abbrechen wandern unter dem Mauszeiger weg. - Kopieren/Einfügen zwischen Tasten (Rechtsklickmenü bzw. Strg+C/Strg+V
auf der Karte unter dem Mauszeiger) benutzt eine reine In-Memory-Ablage
(
self._clip), nicht die System-Zwischenablage — dort liegen Action-Dicts, kein Text. Immercopy.deepcopy(), sonst teilen sich zwei Tasten dasselbe Dict und eine spätere Bearbeitung ändert beide. „Leeren" behält die Notiz (gleiche Regel wie beim Typwechsel im Dialog, siehe Notizen-Bullet oben). Eine kopierte LED-Farbe auf einen Encoder einzufügen ist wirkungslos (Encoder haben keine eigene LED) — das ist Absicht, kein Fehler. - Die Toolbar des Programmiermodus ist bei der Standardbreite (790px) fast voll — zusätzliche Hinweistexte dort kurz halten, sonst werden sie rechts abgeschnitten.
- Tastendruck-Erkennung (seit 2026-08-28, auf expliziten User-Wunsch):
Tasten lassen sich im Bearbeiten-Dialog per „⌨ Taste drücken" erfassen,
Makro-Schritte zusätzlich als Folge am Stück („⏺ Folge aufnehmen").
Umgesetzt ausschließlich über Tk-Fenster-Events (
<KeyPress>/<KeyRelease>auf dem Dialog-Toplevel, ausgewertet inversapad_data.tk_event_to_hid()) — weiterhin kein WinAPI-Hook (SetWindowsHookExo.ä.), aus demselben AV-Fehlalarm-Grund wie bei den Fensterverstecktricks. Wer hier auf einen globalen Hook „aufrüstet", baut genau dieses Risiko ein. Konsequenzen, die so bleiben müssen:- Erkannt wird nur, was das fokussierte Fenster erreicht — Win+L, Strg+Alt+Entf und andere vom System abgefangene Kombinationen nicht.
- Das Dropdown bleibt daneben stehen (Korrekturmöglichkeit), es ersetzt die Erkennung nicht und wird von ihr nicht ersetzt.
- Modifier werden doppelt ermittelt (selbst mitgeführte Press/Release-Bits
plus
event.state), weil ein KeyRelease bei Fokuswechsel verloren gehen kann. _KeyCapturehängt an einemtk.Label, nicht an einemtk.Button: Buttons reagieren per Klassen-Binding selbst auf Leertaste/Enter und würden die laufende Aufnahme mit ihrem eigenen Klick beantworten.- Makro-Schritte filtern das Win-Bit weg (
allow_win=False), passend zur Firmware-Regel oben.
- 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). Dieselbe Näherung gilt für die Tastendruck-Erkennung: HID-Keycodes sind physische US-Tastenpositionen, Tk liefert aber nur keysym/VK-Code des aktiven Layouts — die Position (Scan-Code) wäre dafür nötig und ist ohne WinAPI nicht zu bekommen. Auf deutschem Layout landen Y und Z deshalb vertauscht auf dem Board, und die einzige echte Keysym-Kollision (minus: US-Position 0x2D vs. deutsche Position 0x38) ist bewusst zugunsten der US-Position aufgelöst, damit Erkennung und Dropdown-Beschriftung dasselbe sagen. Bei gehaltenem Shift zählt zuerst der VK-Code, weil der keysym dann das verschobene Zeichen ist (deutsch: Shift+7 →slash, was sonst fälschlich auf Taste 0x38 zeigen würde). - 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 einequeue.Queue, der Main-Thread holt sie perafter()-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.ps1kopiert den Quellcode deshalb zuerst nach%TEMP%, baut nur dort, deployt nach%LOCALAPPDATA%.--onedir, nicht--onefilebeim 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.csdefiniert 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ührtespython), 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
- Hintergrund-Thread für "Vom Board laden"/"Zum Board übertragen" — laufen aktuell synchron im UI-Thread (kurzzeitiges Einfrieren möglich)
- Vorgefertigte
.exeim Repo/als Release-Asset — bewusst nicht committet (Build-Artefakt), Anleitung zum Selbstbauen steht inREADME.md
Nicht an diesen Punkten arbeiten, ohne dass der User es explizit anfragt.
Dokumentation und Verifikation
README.mdist 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 inAGENTS.md, nicht dupliziert indocs/. Bei Änderungen am Binärformat/Protokoll/Datenmodelldocs/data-model.mdbzw.docs/protocol.mdmitpflegen.
Prüfungen vor einem Commit an Binärformat/Protokoll:
python -m py_compile *.py
Für UI-Änderungen (Dialoge, Tastendruck-Erkennung, Kopieren/Einfügen) hat
sich zusätzlich bewährt, ein Wegwerf-Skript im Scratchpad zu fahren, das die
Dialoge ohne Board aufbaut, Tk-Events als kleine Fake-Event-Objekte
(keysym/keycode/state) durchreicht und das Ergebnis-Dict prüft — die
komplette Capture- und Copy/Paste-Logik ist so ohne Klicken verifizierbar.
Wichtig dabei: versapad_combined.DEFAULT_PATH vorher auf eine Temp-Datei
umbiegen, sonst schreibt _autosave_combined() in die echte Nutzer-Config.
Für Screenshots gilt: Tk rechnet in logischen Pixeln, ImageGrab liefert
physische — bei aktiver Windows-Skalierung (hier 125%) sonst ein zu kleiner
Ausschnitt, der wie ein Layout-Fehler aussieht.
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).