Reconciles two independent lines of work: main cherry-picked and then extended dev/jappel's build-script/config-portability/rename-tab/ COM-port fixes, additionally fixing a config-loss bug (rebuild wiped the config when it lived in the install dir -- moved to roaming %APPDATA% instead) and adding free-text notes per action plus a frameless resizable window. dev/jappel keeps its .mcp.json registration and docs/ reference tree, which main deliberately left out. Conflict resolutions favored main's versions where the two sides solved the same problem (config location, build deploy target, COM-port release, tab rename) since main's fixes were validated against a real rebuild-wipes-config incident. docs/architecture.md and docs/data-model.md updated to describe the resulting %APPDATA% config path and the note field.
11 KiB
Architektur
Überblick über die Schichten, Prozesse und Datenflüsse von VersaPad Viewer.
Für Domänenregeln, bekannte Bugs und Implementierungsdisziplin siehe
AGENTS.md; für Installation/Nutzung siehe
README.md. Für die genauen Datenformate siehe
data-model.md, für das Serial-Wire-Protokoll
protocol.md.
Ziel und Kontext
VersaPad Viewer ist ein eigenständiges Python-Tool für das VersaPad-Makropad (4×5-Button-Grid + 4 Encoder, SAMD21-Firmware). Es ergänzt die offizielle VersaGUI (C#/.NET) um eine schlankere, plattformunabhängigere Alternative zum Anzeigen, Live-Synchronisieren und Neuprogrammieren der Belegung, plus einen MCP-Server, der dieselbe Programmierung KI-gesteuert per Tool-Aufruf erlaubt. Beide GUIs (die offizielle VersaGUI und dieses Tool) konkurrieren um denselben exklusiven USB-CDC-Port — siehe „Nebenläufigkeit" unten.
Schichtenmodell
┌─────────────────────────────────────────────────────────────────┐
│ Frontends │
│ ┌────────────┐ ┌──────────────────┐ ┌────────────────────┐ │
│ │ server.py │ │ desktop_viewer.py │ │ versapad_mcp_ │ │
│ │ (Browser, │ │ (Tkinter, Tray, │ │ server.py │ │
│ │ read-only)│ │ Live-Sync, Edit) │ │ (KI-Tool-Aufrufe) │ │
│ └─────┬──────┘ └────────┬──────────┘ └─────────┬──────────┘ │
│ │ │ │ │
│ └───────────┬───────┴──────────────┬───────────┘ │
│ ▼ ▼ │
│ versapad_combined.py versapad_data.py │
│ (Ein-Datei-Format, (Decoding fürs Anzeigen, │
│ Board-Sync, Auto- Legacy-Einzel-JSONs) │
│ Create) │
│ │ │
│ ▼ │
│ versapad_protocol.py (pack/unpack, CRC16) │
│ │ │
│ ▼ │
│ versapad_serial.py (VersaPadLink, 8-Byte-Pakete) │
│ │ │
└─────────────────────┼──────────────────────────────────────────────┘
▼
VersaPad-Board (USB-CDC, VID:PID 239A:0042)
action_dialog.py ist ein UI-Hilfsmodul von desktop_viewer.py
(Bearbeiten-Dialoge für den Programmiermodus) und taucht oben nicht separat
auf.
Read-only-Schicht
versapad_data.py— decodiert JSON-Rohdaten (Keycodes, Consumer-IDs, Modifier-Bits) zu lesbarem Text, kennt die Grid-Geometrie (index = spalte*5 + reihe). Liest wahlweise:- die klassischen
versapad_config1/2/3.json(CONFIG_PATHS) — Export der offiziellen VersaGUI, kein von diesem Tool geschriebenes Format; - oder (über
versapad_combined.py) die kombinierte Datei. app_dir()liefert das Basisverzeichnis für die eigene Config, siehe „Config-Speicherort" unten.
- die klassischen
server.py— generiert bei jedem HTTP-Request frisch HTML aus dem aktuellen Zustand (vcomb.load_or_fetch()), Auto-Reload alle 4s per<meta http-equiv="refresh">. Rein lesend, kein eigener Zustand zwischen Requests, daher nie „veraltet" im Sinne von In-Memory-Staleness.
Binär-/Serial-Schicht
versapad_protocol.py— pack/unpack fürSDeviceConfig(740B, alle 3 Profile) undSMacroTable(512B, 32 Slots), plus CRC16. 1:1 aus den Firmware-Structs übernommen, siehedata-model.md.versapad_serial.py—VersaPadLink: öffnet bei Bedarf den COM-Port (per VID/PID-Erkennung), spricht das 8-Byte-Paket-Protokoll, sieheprotocol.md. Schließt die Verbindung nicht von selbst nach einem Befehl — Aufrufer müssen das selbst tun, wenn sie den Port nicht dauerhaft blockieren wollen (siehe „Nebenläufigkeit" unten).versapad_combined.py— das Ein-Datei-Format: alle 3 Profile + Makro-Tabelle + lokale Profilnamen in einer JSON (versapad_config_all.json). Bindeglied zwischen den JSON-Strukturen und den Binärblobs ausversapad_protocol.py. Zentrale Funktionen:load_or_fetch()— bevorzugt die lokale Datei, baut sie bei Bedarf automatisch neu auf (erst Board-Versuch, sonst leere Default-Config).save_file()/load_file()— reines Lesen/Schreiben der JSON.fetch_from_board()/to_binary()— Konvertierung zu/von den Binärblobs für Board-Lese-/Schreibvorgänge.read_profile_names()— liest nur die Profilnamen, ohne Board-Zugriff (für Tab-Beschriftungen im Nur-Lese-Modus).
UI
desktop_viewer.py— Tkinter-Fenster mit drei unabhängig umschaltbaren Modi (siehe „Modi" unten), Tray-Icon, Info-Dialog mit MCP-Doku.action_dialog.py— modale Bearbeiten-Dialoge (ActionEditDialog,MacroStepsDialog) für den Programmiermodus.
MCP-Server
versapad_mcp_server.py— registriert als projektgebundener MCP-Server "versapad" (.mcp.json) oder wahlweise user-scope (claude mcp add -s user versapad -- <python> versapad_mcp_server.py). Hält einen eigenen In-Memory-Zustand (_state["combined"], unabhängig von jeder laufenden GUI), der explizit persave_local()/load_local()mit der Datei bzw.load_from_board()/write_to_board()mit dem Board synchronisiert wird. Board-Serial-Tools schließen die Verbindung nach jedem Aufruf wieder (siehe unten).
Config-Speicherort
versapad_data.app_dir() bestimmt das Basisverzeichnis für die eigene
Config-Datei (versapad_combined.DEFAULT_PATH =
app_dir()/versapad_config_all.json): %APPDATA%\VersaPadViewer
(Roaming-AppData), unabhängig davon, ob die gebaute .exe oder der
Quellcode gestartet wurde (kein sys.frozen-Zweig).
Bewusst nicht das Installationsverzeichnis (%LOCALAPPDATA%\ VersaPadViewer, wo die .exe liegt): build_and_deploy.ps1 räumt das
Zielverzeichnis vor jedem Deploy komplett ab, läge die Config dort, würde
jeder Rebuild sie mitlöschen (genau das ist am 2026-08-15 passiert, siehe
AGENTS.md). Ebenso bewusst nicht vom Startweg abhängig — sonst sähe
die .exe eine andere Datei als ein aus dem Quellcode gestarteter
versapad_mcp_server.py, und Änderungen aus dem einen Weg wären im
anderen unsichtbar (ebenfalls am 2026-08-15 beobachtet).
Fehlt die Datei, legt load_or_fetch() sie automatisch an — zuerst per
Serial-Versuch vom Board (das ist die eigentliche Quelle der Wahrheit, die
Datei nur ein Lesecache dafür), sonst als leere Default-Config
(default_combined()). Das Tool ist damit auch ganz ohne vorhandene
Config oder angeschlossenes Board sofort benutzbar.
Die klassischen versapad_config1/2/3.json (versapad_data.CONFIG_PATHS)
bleiben bewusst getrennt hartkodiert auf ~\OneDrive\Desktop — das ist
optionale Lese-Interop mit einem JSON-Export der offiziellen VersaGUI,
kein von diesem Tool selbst gepflegtes Format, und daher nicht Teil der
„portablen Installation".
Modi in desktop_viewer.py
Drei Checkboxen, unabhängig voneinander:
| Modus | Zweck | Zustand |
|---|---|---|
| Nur-Lesen (Default) | Zeigt das aktuelle Profil an | Liest bei jedem Poll (alle 1,5s) frisch über _current_profile_view() → vcomb.load_or_fetch(). Kein eigener In-Memory-Snapshot, daher nie veraltet. |
| Live-Sync | Fragt per Serial das aktuell aktive Profil ab, schaltet die Ansicht mit | Hintergrund-Thread pollt read_active_profile(), hält dafür den COM-Port dauerhaft offen, solange die Checkbox an ist. Schließt sich mit VersaGUI/Programmiermodus/MCP-Board-Zugriff gegenseitig aus (exklusiver Port). |
| Programmiermodus | Zellen anklicken zum Bearbeiten | Lädt self.combined einmalig pro Prozesslauf beim ersten Aktivieren (bevorzugt DEFAULT_PATH, sonst default_combined()). Jede Bearbeitung speichert sofort automatisch (_autosave_combined()). Achtung: Da der Snapshot nur einmal geladen wird, sieht der Programmiermodus externe Änderungen (z.B. per MCP) erst nach einem Neustart der exe oder einem expliziten „Datei laden…“. |
Tk-Aufrufe passieren nie direkt aus dem Serial- oder Tray-Hintergrundthread
— Ergebnisse landen in einer queue.Queue, der Main-Thread holt sie per
after()-Polling ab (Absturzrisiko bei Cross-Thread-Tk-Zugriff, siehe
AGENTS.md).
Nebenläufigkeit / Prozessmodell
Der USB-CDC-Port ist exklusiv — nur eine Verbindung gleichzeitig. Drei potenzielle Halter existieren parallel und wissen nichts voneinander:
- Die offizielle VersaGUI (C#/.NET), läuft dauerhaft als Tray-App.
desktop_viewer.py, wenn Live-Sync an ist (hält den Port dauerhaft) oder während eines Board-Lese-/Schreibvorgangs im Programmiermodus (hält ihn nur kurz).versapad_mcp_server.py, während eines Board-Tool-Aufrufs — schließt die Verbindung danach explizit wieder (finally: _link.close()inget_board_status(),load_from_board(),write_to_board()), damit ein einzelner MCP-Aufruf nicht dauerhaft blockiert, was Live-Sync/VersaGUI sonst mit „busy“ aussperren würde.
Mehrere MCP-Server-Prozesse: Je nach Host-Umgebung können mehrere
unabhängige versapad_mcp_server.py-Prozesse gleichzeitig laufen (z.B.
durch wiederholte Tool-Ladevorgänge/Reconnects), jeder mit eigenem,
nicht geteiltem In-Memory-Zustand. Ein write_to_board()-Aufruf kann daher
auf einem anderen Prozess landen als vorherige set_*-Aufrufe und einen
veralteten/leeren Zustand schreiben, obwohl die Antwort {"ok": true}
meldet. Empfohlenes Muster: vor write_to_board() immer load_local()
aufrufen (liest die Datei prozessunabhängig frisch von der Platte) und
nach dem Schreiben mit load_from_board() + get_profile() gegenlesen,
statt dem ACK allein zu vertrauen. Details siehe „Bug beobachtet
2026-08-14“ in AGENTS.md.
Build/Deploy
build_and_deploy.ps1 installiert requirements.txt selbst
(pip install -r), baut mit PyInstaller (--onedir --windowed, nur
desktop_viewer.py wird gebündelt) und kopiert das Ergebnis nach
%LOCALAPPDATA%\VersaPadViewer\. --onedir statt --onefile, um
AV-Fehlalarme zu verringern. Räumt das Zielverzeichnis vor dem Kopieren
komplett ab, rettet dabei aber zuvor gefundene versapad_config*.json
(Altinstallationen, bei denen die Config noch im Installationsordner
liegt) über den Deploy hinweg. Läuft komplett in try/catch mit
Exit-Code-Prüfung und pausiert am Ende (Erfolg wie Fehler) auf
Tastendruck, außer bei -NoPause. Muss lokal laufen, nicht auf einem
Netzlaufwerk (Pfadlängen-/DLL-Ladeprobleme, siehe AGENTS.md). Details:
README.md.