Human-facing reference documentation, split from AGENTS.md's agent-facing domain rules and bug history (which stays there, not duplicated here): - docs/architecture.md: layer diagram, module responsibilities, config storage location, the three GUI modes, and the port-exclusivity / multi-process caveats around concurrent access - docs/data-model.md: the combined and legacy JSON formats, the binary SDeviceConfig/SDeviceProfile/SMacroTable NVM layout byte-for-byte, action types, LED fields, macro-slot conventions, button grid geometry - docs/protocol.md: the 8-byte serial packet format, command/event tables, the read/write/status-poll flows, connection lifecycle, and error states README.md now links to all three from a new "Dokumentation" section, and AGENTS.md's outdated "docs/ tree isn't warranted yet" note is removed now that it exists on explicit user request.
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):
- Gebaute
.exe(PyInstaller--onedir):sys.executables Ordner — die Config liegt also nebenVersaPadViewer.exe, in welchem Installationsverzeichnis sie auch liegt. - Start aus dem Quellcode (
py desktop_viewer.py,py server.py,py versapad_mcp_server.py): der Projektordner (__file__-Verzeichnis).
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
dist\VersaPadViewer\ im Projektordner. --onedir statt --onefile, um
AV-Fehlalarme zu verringern. 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.