VersaGUI-py/AGENTS.md
Julian Appel 78e9640dec Document the layout fix and the return of the title bar
AGENTS.md: die WinAPI-Regel praeziser gefasst -- verboten bleiben
Fenstermanipulation und globale Hooks, passive Layout-Abfragen sind es
nicht (und die offizielle VersaGUI macht dasselbe). Dazu die neue
Aufloesungsreihenfolge samt Warnung, nicht auf "Zeichen auswerten"
zurueckzubauen, die Kollisionsregel bei Tastennamen, der Mehrmonitor-Fix
und die Folgen der Titelleiste (welche Eigenbauten dadurch entfallen sind
und warum randlos nicht ohne Ruecksprache zurueckkommt).

README.md und docs/architecture.md entsprechend: Position statt Zeichen,
layoutrichtige Beschriftungen inkl. der Anzeigeaenderung an bestehenden
Belegungen, Taskleisten-Eintrag statt randlosem Fenster.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 01:02:18 +02:00

512 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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()`.
Seit 2026-08-28 zusätzlich `tk_event_to_hid()` (Tk-Tastendruck →
HID-Keycode+Modifier, siehe Tastendruck-Erkennung unten) und
`macro_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 `.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).
- `versapad_keylayout.py` (seit 2026-08-29) — passive Abfragen ans aktive
Windows-Tastaturlayout: `hid_for_vk()` (Virtual-Key → Scan-Code → HID über
`MapVirtualKeyW`) und `key_name()` (Beschriftung über `GetKeyNameTextW`).
Optional: auf Nicht-Windows/ohne ctypes bleibt `AVAILABLE` False und
`versapad_data` faellt auf seine US-Naeherung zurueck. Zur Abgrenzung
gegen die WinAPI-Verbote siehe Domaenenregeln.
**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.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.
- **WinAPI: was verboten bleibt und was nicht.** Verboten sind
**Fenstermanipulation am eigenen Fenster** (`SetWindowLongW`,
`ShowWindow`) und **globale Eingabehooks** (`SetWindowsHookEx`) — genau
diese Kombination hat laut Speicher-Notiz „keine Selbstversteck-
Fenstertricks" in einem anderen Projekt Bitdefender-Fehlalarme ausgeloest.
Erlaubt und seit 2026-08-29 in Benutzung sind **passive Layout-Abfragen**
(`MapVirtualKeyW`, `GetKeyNameTextW` in `versapad_keylayout.py`): sie
lesen nur die Tastaturbelegung, fassen weder Fenster noch Prozesse noch
den Eingabestrom an; die offizielle VersaGUI (C#) benutzt
`GetKeyNameText()` fuer denselben Zweck. Die Grenze verlaeuft also nicht
bei „ctypes", sondern bei „greift ins System ein".
- **Normales Fenster mit Titelleiste (seit 2026-08-29).** Von 2026-08-15
bis dahin lief das Fenster randlos (`overrideredirect(True)`) — das kostet
unter Windows zwingend den Taskleisten-Eintrag. Als der ausdruecklich
gebraucht wurde, gab es nur drei Wege: Titelleiste zurueck,
`SetWindowLongW`+WS_EX_APPWINDOW (siehe Verbot oben) oder ein
unsichtbares Proxy-Fenster. Der User hat sich fuer die Titelleiste
entschieden. Damit sind ersatzlos entfallen: ziehbare Kopfzeile
(`_start_move`/`_on_move`), Groessen-Anfasser unten rechts
(`_start_resize`/`_on_resize`, ersetzt durch `self.minsize()`), die
eigenen `✕`/`—`-Knoepfe und der `<Unmap>`-Handler, der Minimieren ins Tray
umgeleitet hat. **Wer das Fenster wieder randlos machen will, nimmt dem
User den Taskleisten-Eintrag weg** — nicht ohne Ruecksprache.
- Schliessen (`✕`) legt weiterhin ins Tray statt zu beenden (wie die
offizielle VersaGUI, das Programm laeuft im Hintergrund weiter),
Minimieren geht jetzt aber ganz normal in die Taskleiste. `Escape`
minimiert ebenfalls, statt wie frueher ins Tray zu legen — mit
Taskleisten-Eintrag waere „verschwindet spurlos" die unangenehmere
Ueberraschung.
- 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.
- **Dialogposition wird gegen das ELTERNFENSTER begrenzt, nie gegen
`winfo_screenwidth()` (2026-08-29):** Tk meldet dort nur den
Hauptbildschirm. Die erste Fassung hat damit geklemmt — lag das
Hauptfenster auf einem zweiten Monitor, zog genau diese Begrenzung den
Dialog zurueck an den Rand des ersten. Passt der Dialog nicht ins
Elternfenster (der Makro-Schritte-Dialog ist breiter als der
Action-Dialog), wird er an dessen linker oberer Ecke ausgerichtet statt
zentriert.
- **Dialoge öffnen über dem Hauptfenster, nicht in der Bildschirmecke
(2026-08-28):** `_ModalDialog` baut sich `withdraw()`n auf, positioniert
sich in `run()` per `_center_on_parent()` und wird erst dann
`deiconify()`t. Ohne das legt Tk jeden Toplevel bei `+0+0` an — bei 20
Tasten hintereinander wandert der Blick jedes Mal in die linke obere Ecke.
Das `withdraw()` gehört zwingend dazu, sonst blitzt der Dialog dort auf,
bevor er springt.
- **Verschachtelte Dialoge geben den Grab zurück:** `MacroStepsDialog` läuft
im `ActionEditDialog`. `_finish()` ruft `grab_release()`, was den Grab des
*aufrufenden* Dialogs mit wegnimmt — `run()` setzt ihn deshalb am Ende
wieder, wenn der Parent ein `_ModalDialog` ist.
- **Panel-Höhe im `ActionEditDialog` ist 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. Immer `copy.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 in
`versapad_data.tk_event_to_hid()`) — **weiterhin kein WinAPI-Hook**
(`SetWindowsHookEx` o.ä.), 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.
- **Welche Taste gemeint ist, wird ueber die PHYSISCHE POSITION
aufgeloest, nie ueber das erzeugte Zeichen** (2026-08-29, nach
Fehlermeldung aus der Praxis). HID-Keycodes *sind* Positionen: das Board
sendet eine Position, erst Windows macht daraus ein Zeichen. Die erste
Fassung ging ueber keysym/Zeichen und war auf deutschem Layout
entsprechend kaputt — Y und Z landeten vertauscht auf dem Board, ÄÖÜ
und #/+ waren gar nicht erfassbar. Reihenfolge jetzt: (1) benannte
Tasten ueber den keysym, (2) Zeichentasten ueber
`versapad_keylayout.hid_for_vk()`, (3) die alte Naeherung nur noch als
Fallback ohne WinAPI. **Nicht auf "Zeichen auswerten" zurueckbauen**
das ist genau der Fehler, der hier behoben wurde.
- Schritt (1) ist keine Bequemlichkeit, sondern noetig: `MapVirtualKeyW`
liefert fuer die Pfeiltasten denselben Scan-Code wie fuer ihre
Numpad-Zwillinge (gemessen: VK_LEFT und VK_NUMPAD4 beide 0x4B, das
E0-Praefix von `MAPVK_VK_TO_VSC_EX` bleibt dort aus). Ueber den
Scan-Code allein waeren Pfeiltasten nicht von Numpad-Tasten zu
unterscheiden.
- 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.
- `_KeyCapture` hängt an einem `tk.Label`, nicht an einem `tk.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.
- **Tastenbeschriftungen kommen vom aktiven Layout** (seit 2026-08-29).
`versapad_data.hid_key_name()` fragt fuer Zeichentasten
`GetKeyNameTextW` (deutsch: HID 0x1C → „Z", 0x34 → „ä"); fuer alles
andere bleiben die gepflegten deutschen Namen aus `_SPECIAL_KEYS`
(„Enter", „Bild↑", „Num5") — die lesen sich besser als das, was Windows
liefert („EINGABE", „4 (ZEHNERTASTATUR)"). Ohne Layout-Abfrage
(Nicht-Windows) faellt alles auf die alte US-Naeherung zurueck.
Konsequenzen, die man kennen muss:
- Bestehende Belegungen aendern ihre **Anzeige**, nicht ihre Daten. Wer
frueher im Dropdown „Z" gewaehlt hat, bekam 0x1D — das steht so in der
Config und zeigt jetzt wahrheitsgemaess „Y", weil es auf dieser Tastatur
ein Y tippt. Das ist keine Regression, sondern der sichtbar gewordene
Altfehler.
- Namen sind Schluessel (Dropdown, `hid_key_code_for_name()`) und muessen
eindeutig bleiben. Es gibt echte Kollisionen: auf deutschem Layout heisst
HID 0x31 schlicht „#" — den Namen trug bisher HID 0x32. Der Layoutname
gewinnt, der verdraengte US-Name wird als „# (US-Layout)" gekennzeichnet
statt verworfen, damit die Taste ansprechbar bleibt.
- `hid_key_code_for_name()` akzeptiert weiterhin beide Schreibweisen, bei
Kollision gewinnt das Layout. Fuer den MCP-Server heisst das:
`set_button_key(key="Z")` trifft die Taste, die auf dieser Tastatur ein
Z tippt (0x1C), nicht mehr die US-Position 0x1D.
- Ein Layoutwechsel zur Laufzeit wird nicht bemerkt (Namen werden einmal
ermittelt und behalten).
- 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
- 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
```
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).