# 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`](../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`](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`](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). ## Verbindungsaufbau (`VersaPadLink._ensure_open()`) 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). ## Fehlerzustände (`VersaPadLink.last_error`) | 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 |