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

12 KiB

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.MCPServernicht 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.pyVersaPadLink: 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.