VersaGUI-py/docs/architecture.md
Julian Appel 189ec01e68 Merge branch 'main' into dev/jappel
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.
2026-08-15 18:16:00 +02:00

193 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Architektur
Überblick über die Schichten, Prozesse und Datenflüsse von VersaPad Viewer.
Für Domänenregeln, bekannte Bugs und Implementierungsdisziplin siehe
[`AGENTS.md`](../AGENTS.md); für Installation/Nutzung siehe
[`README.md`](../README.md). Für die genauen Datenformate siehe
[`data-model.md`](data-model.md), für das Serial-Wire-Protokoll
[`protocol.md`](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`](data-model.md).
- **`versapad_serial.py`** — `VersaPadLink`: öffnet bei Bedarf den COM-Port
(per VID/PID-Erkennung), spricht das 8-Byte-Paket-Protokoll, siehe
[`protocol.md`](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`): `%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:
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
`%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`](../README.md#als-eigenständige-exe-windows).