VersaGUI-py/docs/architecture.md
Julian Appel 83429363c1 Add architecture/data-model/protocol reference docs
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.
2026-08-14 23:19:13 +02:00

11 KiB
Raw Blame History

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.
  • 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ür SDeviceConfig (740B, alle 3 Profile) und SMacroTable (512B, 32 Slots), plus CRC16. 1:1 aus den Firmware-Structs übernommen, siehe data-model.md.
  • versapad_serial.pyVersaPadLink: öffnet bei Bedarf den COM-Port (per VID/PID-Erkennung), spricht das 8-Byte-Paket-Protokoll, siehe protocol.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 aus versapad_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 per save_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 neben VersaPadViewer.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:

  1. Die offizielle VersaGUI (C#/.NET), läuft dauerhaft als Tray-App.
  2. 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).
  3. versapad_mcp_server.py, während eines Board-Tool-Aufrufs — schließt die Verbindung danach explizit wieder (finally: _link.close() in get_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.