VersaGUI-py/docs/protocol.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

5.8 KiB

Serial-Wire-Protokoll

Beschreibt, wie versapad_serial.VersaPadLink mit dem Board spricht. 1:1 aus VersaGUI/src/Protocol.cs und VersaMCU/doc/07_serial_protocol.md übernommen (siehe „Existing-Codebase-Regel“ in AGENTS.md — bei Änderungen gegen die Firmware-/ C#-Quellen abgleichen, nicht aus dem Gedächtnis rekonstruieren). Für die Bedeutung der übertragenen Bytes (Config/Makro-Layout) siehe data-model.md.

Transport

  • USB-CDC (virtueller COM-Port), 115200 Baud.
  • Board-Erkennung über USB VID:PID 239A:0042 (versapad_serial.find_port() durchsucht serial.tools.list_ports.comports()).
  • Der Port ist exklusiv — siehe „Nebenläufigkeit“ in architecture.md.
  • Jedes Paket ist exakt 8 Byte:
    Byte Bedeutung
    [0] Command-/Event-ID
    [1] je nach Befehl: Chunk-Index, Chunk-Anzahl, oder ungenutzt
    [2..7] Payload, 6 Byte (PAYLOAD_SIZE)

Befehle (Host → Board)

Konstante Wert Zweck
CMD_READ_STATUS 0x06 Leichtgewichtiger Status-Poll (aktives Profil)
CMD_CONFIG_BEGIN 0x10 Start eines Config-Schreibvorgangs
CMD_CONFIG_DATA 0x11 Ein Config-Datenpaket
CMD_CONFIG_COMMIT 0x12 Config committen (NVM-Schreiben nach Validierung)
CMD_CONFIG_READ 0x13 Komplette Config vom Board anfordern
CMD_MACRO_BEGIN 0x20 Start eines Makro-Schreibvorgangs
CMD_MACRO_DATA 0x21 Ein Makro-Datenpaket
CMD_MACRO_COMMIT 0x22 Makros committen
CMD_MACRO_READ 0x23 Komplette Makro-Tabelle vom Board anfordern

Events (Board → Host)

Konstante Wert Zweck
EVT_STATUS 0x86 Antwort auf CMD_READ_STATUS
EVT_CONFIG_ACK 0x90 Config-Commit erfolgreich
EVT_CONFIG_NACK 0x91 Config-Commit abgelehnt (Validierung fehlgeschlagen)
EVT_CONFIG_BEGIN 0x92 Board beginnt, Config-Dump zu senden
EVT_CONFIG_DATA 0x93 Ein Config-Datenpaket (Antwort auf CMD_CONFIG_READ)
EVT_CONFIG_END 0x94 Config-Dump vollständig
EVT_MACRO_ACK 0x95 Makro-Commit erfolgreich
EVT_MACRO_BEGIN 0x96 Board beginnt, Makro-Dump zu senden
EVT_MACRO_DATA 0x97 Ein Makro-Datenpaket
EVT_MACRO_END 0x98 Makro-Dump vollständig
EVT_MACRO_NACK 0x99 Makro-Commit abgelehnt

Ablauf: Lesen (CONFIG_READ / MACRO_READ)

Genutzt von read_full_config() (740B, ⌈740/6⌉ = 124 Datenpakete) und read_macros() (512B, ⌈512/6⌉ = 86 Datenpakete). Deadline 3s.

Host  → [CMD_CONFIG_READ, 0, 0,0,0,0,0,0]
Board → [EVT_CONFIG_BEGIN, ...]
Board → [EVT_CONFIG_DATA, chunk_idx=0, <=6B Payload]
Board → [EVT_CONFIG_DATA, chunk_idx=1, <=6B Payload]
        ... (124 Pakete insgesamt für Config, 86 für Makros)
Board → [EVT_CONFIG_END, ...]

chunk_idx (Byte [1]) bestimmt den Ziel-Offset im Empfangspuffer: offset = chunk_idx * 6. Kommt EVT_END, ohne dass zuvor EVT_BEGIN gesehen wurde, gilt das als Timeout (unvollständige/verpasste Antwort).

Ablauf: Schreiben (CONFIG_BEGIN/DATA/COMMIT bzw. MACRO_*)

Genutzt von write_full_config()/write_macros(). Deadline 5s. Anzahl Chunks maximal 255 (ein Byte) — bei größeren Blobs schlägt der Aufruf mit "too_large" fehl, bevor überhaupt gesendet wird.

Host  → [CMD_CONFIG_BEGIN, chunk_count, 0,0,0,0,0,0]
Host  → [CMD_CONFIG_DATA, 0, <=6B Payload (zero-padded)]
Host  → [CMD_CONFIG_DATA, 1, <=6B Payload]
        ... (ein Paket je Chunk)
Host  → [CMD_CONFIG_COMMIT, 0, 0,0,0,0,0,0]
Board → [EVT_CONFIG_ACK, ...]   -- oder EVT_CONFIG_NACK bei fehlgeschlagener Validierung

Sicherheitsnetz: Die Firmware prüft Magic/Version/CRC (Config) bzw. Keycode-Bereich (Makros) vor jedem NVM-Schreiben und antwortet sonst nur mit NACK — ein fehlerhafter Schreibversuch kann das Board laut Firmware-Design nicht in einen inkonsistenten Zustand bringen, siehe nvm_config_validate() in den Firmware-Quellen.

Ablauf: Leichtgewichtiger Status-Poll (READ_STATUS)

Genutzt von read_active_profile() für kontinuierliches Live-Sync-Polling (alle 1,5s). Deadline 1s.

Host  → [CMD_READ_STATUS, 0, 0,0,0,0,0,0]
Board → [EVT_STATUS, active_profile (0-2), ...]

Ein einzelnes Antwortpaket statt eines vollen CONFIG_READ-Dumps (124 Pakete) — Letzterer blockiert die Firmware in poll_vendor() lang genug, dass laufende LED-Pulse-Animationen sichtbar stottern. Ältere Firmware ohne CMD_READ_STATUS antwortet einfach gar nicht → sauberer Timeout, kein Absturz (siehe AGENTS.md, Eintrag „4215323“ in der Historie).

  1. Board per VID/PID finden (find_port()).
  2. serial.Serial(port, 115200, timeout=0.5) öffnen.
  3. DTR auf True setzen, 0,2s warten (Board braucht kurz, bis es nach dem Öffnen/DTR-Toggle wieder reagiert).
  4. Input-Buffer leeren.

Wichtig: VersaPadLink schließt die Verbindung nicht automatisch nach einem Befehl — nur bei serial.SerialException (Fehlerfall) oder explizitem .close()-Aufruf. Aufrufer, die den exklusiven Port nicht dauerhaft blockieren wollen, müssen selbst schließen (siehe versapad_mcp_server.py, das nach jedem Board-Tool-Aufruf _link.close() in einem finally-Block aufruft).

Wert Bedeutung
None kein Fehler
"no_pyserial" Paket pyserial nicht installiert
"not_found" kein Gerät mit passender VID/PID gefunden
"busy" Port gefunden, aber Öffnen fehlgeschlagen (von woanders gehalten — VersaGUI, Live-Sync, ein anderer Prozess)
"timeout" keine (gültige) Antwort innerhalb der Deadline
"nack" Board hat den Commit explizit abgelehnt (Validierung fehlgeschlagen)
"too_large" zu sendender Blob braucht mehr als 255 Chunks