VersaGUI-py/AGENTS.md
cjjohn 4ac29a3e5c Add custom icon and tray-minimize behavior (like VersaGUI's TrayApp)
Minimizing or closing hides the window instead of leaving a taskbar
entry; only "Beenden" in the tray right-click menu really exits. Tray
callbacks route through a queue instead of touching Tk directly from
pystray's thread (same pattern as the earlier serial-thread fix).

Also fixes the build script: PyInstaller failed on the network share
while copying Tcl/Tk tzdata (path-length issue), so building now
happens in a local temp copy instead of directly on Z:.

Adds an info button that explains the MCP server's available tools.
2026-08-05 08:30:50 +02:00

201 lines
12 KiB
Markdown

# versapad-viewer
Eigenes kleines Tool (Grovy311-Kontext, nicht Teil der fremden jappel-Repos
VersaGUI/VersaMCU). Zeigt die Steuermatrix (4x5 Button-Grid + 4 Encoder) als
Browser-Live-Seite oder natives Tkinter-Fenster. Inzwischen mehr als ein
reiner Viewer: der Tkinter-Desktop-Viewer hat einen vollen Programmiermodus
(Action-Editor, Serial-Schreibzugriff aufs Board, Makro-Editor) bekommen —
faktisch ein schlankes Python-Pendant zu VersaGUI. Der Browser-Server
(`server.py`) blieb bewusst read-only/einfach.
## Architektur
**Read-only-Schicht (JSON, beide Frontends):**
- `versapad_data.py` — Decoding fuer Anzeige: JSON laden, HID-Keycode/
Consumer-Usage/Modifier → lesbarer Text, Grid-Geometrie (index =
spalte*5+reihe), `hid_key_choices()`/`consumer_choices()` fuer Dropdowns.
- `server.py` — stdlib `http.server`, generiert HTML bei jedem Request neu,
Profil-Wechsel über `?profile=0|1|2`, Auto-Reload alle 4s. Rein lesend,
kein Programmiermodus (bewusst einfach gehalten).
**MCP-Server (Config per KI-Tool-Aufruf statt Hand-JSON):**
- `versapad_mcp_server.py` — registriert als User-Scope-MCP-Server "versapad"
(`claude mcp add -s user versapad -- <Python313> versapad_mcp_server.py`,
landet in `~/.claude.json`). Tools: `list_profiles`/`get_profile`/
`get_macro`/`get_board_status` (lesen), `set_button_key`/`_consumer`/
`_macro`/`_profile_switch`/`_none`/`_led`, dieselben `set_encoder_*`,
`set_macro`, `rename_profile` (mutieren nur In-Memory-State), `save_local`/
`load_local` (↔ `versapad_config_all.json`), `load_from_board`/
`write_to_board` (↔ echtes Board per Serial). Nutzt das MCP-SDK v2 (Paket
`mcp`, Klasse `mcp.server.mcpserver.MCPServer`**nicht** `FastMCP` aus
`mcp.server.fastmcp`, das gibt's in dieser SDK-Version nicht mehr, wurde
umbenannt). `@mcp.tool()`-dekorierte Funktionen bleiben direkt aufrufbar
(kein `.fn`-Unterschied wie bei alten FastMCP-Versionen).
**Binaer-/Serial-Schicht (fuer den Programmiermodus):**
- `versapad_protocol.py` — pack/unpack fuer `SDeviceConfig`(740B, alle 3
Profile) und `SMacroTable`(512B, 32 Slots) + CRC16 (Poly 0x1021, Init
0xFFFF), 1:1 aus den Firmware-Structs (`nvm_config.h`, `action.h`,
`macro_config.h`, `CButton.h`-LEDAnim-Enum, `nvm_config.cpp`-CRC).
**Gegen echtes Board validiert:** read → unpack → pack → byte-identisch
zum Original, inkl. CRC — siehe Git-Historie dieser Datei / Sessionlog.
- `versapad_serial.py``VersaPadLink`: liest/schreibt komplette Config +
Makros per 8-Byte-Paket-Protokoll (`CmdConfigRead/Begin/Data/Commit`,
`CmdMacroRead/Begin/Data/Commit`), Board-Identifikation per VID/PID
`239A:0042`. Schreiben ist sicher im Sinne von "kann NVM nicht zerlegen"
— Firmware prueft Magic/CRC/Keycode-Bereich vor jedem Save und antwortet
sonst nur mit NACK (kein Datenmuell moeglich).
- `versapad_combined.py` — kombiniertes Ein-Datei-Format (alle 3 Profile +
Makros + **nur lokal gespeicherte** Profilnamen) statt der 3 Einzel-JSONs.
Passt besser zum Wire-Protokoll: `CONFIG_BEGIN/COMMIT` ueberträgt ohnehin
immer den kompletten 740B-Block auf einmal, nie nur ein Profil.
Default-Pfad: `~\OneDrive\Desktop\versapad_config_all.json`.
**UI:**
- `desktop_viewer.py` — Tkinter, flaches Design (Label-Flächen, keine
Canvas-Formen/Anti-Aliasing). Zellen nutzen `.place()` mit festen
Pixel-Positionen statt `.pack()` (mit pack sah das Grid je nach leerer/
belegter Zelle ungleichmäßig aus). Drei unabhängige Modi:
- **Nur lesen** (Default): pollt die 3 JSONs alle 1.5s.
- **Live-Sync**: pollt per Serial das aktive Profil vom Board, schaltet
Tabs automatisch mit. Laeuft in einem Hintergrundthread, der NUR ueber
eine `queue.Queue` mit dem Main-Thread kommuniziert (`self.after()`
direkt aus einem Fremdthread aufrufen ist in Tkinter nicht threadsicher
und hat den Prozess in einer frueheren Version lautlos abstuerzen
lassen — `pyw` hat kein Konsolenfenster, der Crash war unsichtbar).
- **Programmiermodus**: Zellen anklicken → `action_dialog.py`
(`ActionEditDialog`/`MacroStepsDialog`) bearbeitet Action-Typ, HID-Taste
(Dropdown statt Tastendruck-Capture — kein WinAPI-Hook, um nicht wieder
einen AV-Fehlalarm wie bei den Fensterverstecktricks zu riskieren),
Consumer, Makro-Slot+Schritte, ProfileSwitch, LED-Farbe/Animation.
"Vom Board laden"/"Zum Board übertragen" nutzen die Binaer-Schicht.
Schaltet Live-Sync automatisch aus (ein `VersaPadLink` kann nicht von
zwei Konsumenten gleichzeitig genutzt werden).
- `action_dialog.py` — die beiden Bearbeiten-Dialoge, importiert nur
`versapad_data` (fuer Dropdown-Inhalte).
## Quelle der Wahrheit — nicht raten
Bei Aenderungen am Schema/Decoding/Binaerformat immer gegen die Firmware-
und GUI-Quellen abgleichen, nie aus dem Gedaechtnis rekonstruieren:
- `VersaGUI/src/ActionDialog.cs` (HidKeyName, s_consumer, Modifier-Bits)
- `VersaGUI/src/Protocol.cs` (Paket-IDs, Chunk-Groessen)
- `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
tatsaechlich behandelt — Protocol.cs definiert mehr Konstanten als die
Firmware zwingend implementiert, immer hier gegenchecken)
Zeichentasten (A-Z, Ziffern, Satzzeichen) zeigen US-Layout-Näherung, da die
echte GUI das über `GetKeyNameText()` layoutabhängig auflöst — das bilden
wir ohne WinAPI-Call nicht nach.
**Korrektur einer frueheren Annahme:** Anders als zuerst notiert, kann die
Firmware Makros sehr wohl per normaler USB-Verbindung schreiben
(`USB_CMD_MACRO_BEGIN/DATA/COMMIT` in `CMainController.cpp` sind
implementiert) — kein SWD/JTAG noetig. Der VersaPad-Skill
(`~/.claude/skills/versapad/SKILL.md`) behauptet noch das Gegenteil und
sollte bei Gelegenheit korrigiert werden.
## Start
```
py server.py # Browser, http://127.0.0.1:8765
pyw desktop_viewer.py # natives Fenster, kein Konsolenfenster
```
Oder `run_browser.bat` / `run_desktop.bat` per Doppelklick, oder die
Desktop-Verknuepfungen "VersaPad Viewer (Browser)" / "VersaPad Viewer
(Fenster)".
**Wichtig:** `pyw`/`py` benutzen, nicht bare `pythonw`/`python` — auf hal9001
liegt in PATH zuerst eine Hermes-Agent-venv (`...\hermes\hermes-agent\venv\
Scripts\pythonw.exe`) ohne pyserial. `pyw`/`py` (Python Launcher) lösen
zuverlässig zur echten Python313-Installation auf.
## Gepackte .exe (PyInstaller) — kein Terminal-Flash, kein Python-Setup nötig
`build_and_deploy.ps1` kopiert den Quellcode zuerst nach `%TEMP%`, baut dort
mit PyInstaller (`--windowed --onedir --icon icon.ico`), und kopiert das
Ergebnis nach `%LOCALAPPDATA%\VersaPadViewer\VersaPadViewer.exe`.
Desktop-Verknüpfung "VersaPad Viewer (Fenster)" zeigt dorthin (inkl. eigenem
Icon, `IconLocation` zeigt auf die `.exe` selbst statt auf `pythonw.exe`).
**`--onedir`, nicht `--onefile`** — Single-File-PyInstaller-Bundles lösen
öfter AV-Fehlalarme aus (sehen strukturell wie ein Packer aus); `--onedir`
(Ordner mit `.exe` + `_internal`) ist unauffälliger. Trotzdem unsigniert,
also grundsätzlich nie über Smart App Control/AV in Stein gemeißelt sicher.
**Zwei Netzlaufwerk-Stolpersteine, deshalb baut UND deployt alles lokal:**
1. Laufzeit: die gebaute `.exe` läuft NICHT vom Netzlaufwerk aus (`Z:\Git\...`,
SMB-Share auf cj-ki) — Windows blockiert das Nachladen der
`_internal`-DLLs von einem Netzwerkpfad, der Prozess startet dann einfach
nie, ganz ohne Fehlermeldung/Crash-Log/AV-Benachrichtigung (sah zuerst wie
ein AV-Block aus, war aber keiner — verifiziert indem dieselbe `.exe` 1:1
lokal kopiert sofort lief).
2. Buildzeit: PyInstaller scheitert beim Kopieren der Tcl/Tk-`tzdata`
(viele tief verschachtelte, lang benannte Zeitzonen-Ordner) auf dem
SMB-Share mit `FileNotFoundError: [WinError 3]` — Pfadlängen-Problem,
kombiniert mit dem eh schon langen Projektpfad (`Z:\Git\Versa Board\
versapad-viewer\dist\...`).
Deshalb kopiert `build_and_deploy.ps1` den Quellcode zuerst nach `%TEMP%`
und baut NUR dort — Z: wird nur zum Lesen der `.py`-Dateien angefasst.
Bei erneutem Bauen: `.\build_and_deploy.ps1` in PowerShell ausführen
(lokal, nicht über die sandboxed Bash-Tool-Umgebung — die hat eine eigene
Dateisystem-Sicht, die nicht zuverlässig mit dem echten Windows-Dateisystem
übereinstimmt; siehe Sessionlog: ein `cp -r` über Bash landete an einem Ort,
den PowerShell/der reale Explorer nicht sah. Kopieroperationen für Dinge,
die der User/andere Prozesse sehen sollen, gehören in PowerShell, nicht in
Bash).
## Icon + Tray (kein Taskleisten-Eintrag beim Minimieren, wie VersaGUI)
- `icon.png`/`icon.ico` — generiert via Pillow (dunkles abgerundetes Quadrat,
blauer Block, 2x2-Punktraster; flaches Design, keine Farbverläufe/
Anti-Aliasing-Details, siehe Speicher-Notiz zu Tkinter-Flat-Design).
`.ico` fürs Fenster-/Taskleisten-/Exe-Icon, `.png` fürs Tray-Icon (pystray
braucht ein PIL-Image, kein Multi-Res-`.ico`).
- `resource_path()` in `desktop_viewer.py` findet Assets sowohl im
Quellordner als auch im PyInstaller-Bundle (`sys._MEIPASS`).
- Minimieren (`<Unmap>` mit `state()=='iconic'`) ruft `withdraw()` statt
normal zu minimieren → kein Taskleisten-Eintrag, nur noch Tray-Icon.
Schließen (X) macht dasselbe (`_hide_to_tray`, nicht `_quit`) — echtes
Beenden nur über "Beenden" im Tray-Rechtsklickmenü (mirrort VersaGUIs
`TrayApp`-Menü: Öffnen/Beenden). Tray-Icon läuft über `pystray.Icon.
run_detached()` in eigenem Thread — Klicks im Tray-Menü legen nur ein
Ereignis in `self._tray_queue`, das der Main-Thread per `after()`-Polling
abholt (dieselbe Queue-statt-Direktzugriff-Regel wie beim Serial-Thread,
siehe oben — Tk-Aufrufe aus einem Fremdthread haben schon mal einen
stillen Absturz verursacht).
- Neue Abhängigkeiten: `pystray`, `pillow` (zusätzlich zu `pyserial`).
- Info-Button (ⓘ oben rechts im Header) öffnet einen Dialog mit der
MCP-Server-Doku (`MCP_INFO_TEXT`-Konstante) — rein informativ, keine Logik.
## Dateien/Pfade (hardcoded, hal9001-spezifisch)
- Einzel-JSONs (Read-only-Modus): `~\OneDrive\Desktop\versapad_config{1,2,3}.json`
— Profil 0 Windows / 1 Fusion 360 / 2 BricsCAD, siehe `versapad_data.CONFIG_PATHS`.
- Kombinierte Datei (Programmiermodus-Export): `~\OneDrive\Desktop\
versapad_config_all.json` (Default, siehe `versapad_combined.DEFAULT_PATH`).
## Offene Punkte
- Kein automatischer Windows-Start/Tray-Icon — bewusst einfach gehalten,
bei Bedarf nachrüstbar.
- Live-Sync und VersaGUI (bzw. Programmiermodus) können den COM-Port nicht
gleichzeitig halten (exklusiver Zugriff) — zeigt dann "Port belegt", kein
Crash. VersaGUI läuft als Tray-App dauerhaft im Hintergrund weiter, auch
wenn nur das Konfigurationsfenster geschlossen wird — für Programmiermodus/
Live-Sync muss es über das Tray-Icon → "Beenden" wirklich beendet werden.
Live-Sync wird beim Aktivieren des Programmiermodus automatisch ausgeschaltet.
Innerhalb des Programmiermodus selbst laufen "Vom Board laden"/"Zum Board
übertragen" synchron im UI-Thread (kurzzeitiges Einfrieren möglich, v.a.
bei Timeout) — bislang nicht als Hintergrund-Thread ausgelagert.
Der Browser-Server (`server.py`) hat keinen Programmiermodus.
- HID-Tasten-Auswahl im Programmiermodus ist ein Dropdown, kein
Tastendruck-Capture (bewusst, um WinAPI-Hooks/AV-Fehlalarme zu vermeiden).
- Macro-Editor erlaubt nur Strg/Shift/Alt pro Schritt (kein Win), passend
zum Original-`ActionDialog.cs`-Verhalten fuer Makro-Steps.