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.
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— stdlibhttp.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, dieselbenset_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 (Paketmcp, Klassemcp.server.mcpserver.MCPServer— nichtFastMCPausmcp.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 fuerSDeviceConfig(740B, alle 3 Profile) undSMacroTable(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/PID239A: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/COMMITueberträ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.Queuemit 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 —pywhat 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 (einVersaPadLinkkann nicht von zwei Konsumenten gleichzeitig genutzt werden).
action_dialog.py— die beiden Bearbeiten-Dialoge, importiert nurversapad_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:
- Laufzeit: die gebaute
.exelä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.exe1:1 lokal kopiert sofort lief). - Buildzeit: PyInstaller scheitert beim Kopieren der Tcl/Tk-
tzdata(viele tief verschachtelte, lang benannte Zeitzonen-Ordner) auf dem SMB-Share mitFileNotFoundError: [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)..icofürs Fenster-/Taskleisten-/Exe-Icon,.pngfürs Tray-Icon (pystray braucht ein PIL-Image, kein Multi-Res-.ico).resource_path()indesktop_viewer.pyfindet Assets sowohl im Quellordner als auch im PyInstaller-Bundle (sys._MEIPASS).- Minimieren (
<Unmap>mitstate()=='iconic') ruftwithdraw()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 VersaGUIsTrayApp-Menü: Öffnen/Beenden). Tray-Icon läuft überpystray.Icon. run_detached()in eigenem Thread — Klicks im Tray-Menü legen nur ein Ereignis inself._tray_queue, das der Main-Thread perafter()-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 zupyserial). - 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, sieheversapad_data.CONFIG_PATHS. - Kombinierte Datei (Programmiermodus-Export):
~\OneDrive\Desktop\ versapad_config_all.json(Default, sieheversapad_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.