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.
136 lines
5.8 KiB
Markdown
136 lines
5.8 KiB
Markdown
# 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 |
|