VersaMCU/doc/07_serial_protocol.md
cjjohn 13c3e41c91 Add lightweight READ_STATUS command to avoid blocking LED updates during polling
CONFIG_READ was being (ab)used by the Windows viewer's Live-Sync feature
to poll just the active profile every 1.5s, but the handler sends the
full 740-byte config as ~124 blocking chunk packets from inside
poll_vendor() -- which runs before updateLEDs() in the same loop
iteration (see the loop-order comment at the top of
CMainController.cpp). Every poll cycle stalled updateLEDs() long enough
that running Pulse/Blink animations visibly stuttered, since their
brightness is computed from an absolute millis() timestamp and jumps
forward once the stall clears instead of catching up smoothly.

Added USB_CMD_READ_STATUS (0x06) / USB_EVT_STATUS (0x86): a single NVM
read (no serial I/O) and one 8-byte reply packet with the active
profile in Data[1], no chunking. Documented in
doc/07_serial_protocol.md alongside why CONFIG_READ is unsuitable for
polling. CONFIG_READ stays as-is for actual full-dump use (e.g. "Vom
Board laden").

Verified on hardware after flashing via env:versapad_usb: READ_STATUS
returns the correct profile in ~well under CONFIG_READ's dump time,
Live-Sync no longer visibly disturbs LED animations.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 15:37:21 +02:00

168 lines
5.6 KiB
Markdown
Raw Permalink 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.

# Serial-Protokoll (CDC USB)
Dateien:
- `hal/usb_serial.h`
- `hal/usb_serial.cpp`
- `CMainController.cpp`
## Paketformat
Alle Pakete sind exakt 8 Byte lang:
```text
Byte 0: command / event id
Byte 1: key_id oder chunk-index oder chunk-count
Byte 2..7: kommandospezifische Daten
```
LED-Kommandos verwenden Byte 2..4 für RGB. Config- und Makro-DATA-Pakete
verwenden alle sechs Bytes 2..7 als Nutzlast. Einfache Events aus
`usb_serial_send()` nutzen höchstens Byte 2..3 und füllen den Rest mit null.
Es gibt kein Framing, keinen Längenheader und keine Prüfsumme auf Paketebene.
## Richtungen
| Richtung | IDs | Verarbeitung |
|---|---|---|
| PC -> Board | `0x01..0x7F` | `poll_vendor()` |
| Board -> PC | `0x81..0xFF` | `usb_serial_send()` |
## Commands
| ID | Name | Zweck |
|---|---|---|
| `0x01` | `SET_LED_OVERRIDE` | temporaere LED-Override setzen |
| `0x02` | `CLEAR_LED_OVERRIDE` | Override entfernen |
| `0x03` | `SET_LED_BASE` | Base-Farbe im RAM setzen |
| `0x05` | `PING` | Antwort: `PONG` |
| `0x06` | `READ_STATUS` | Antwort: `STATUS` (Byte 1 = aktives Profil 0-2) |
| `0x10` | `CONFIG_BEGIN` | Config-Transfer starten |
| `0x11` | `CONFIG_DATA` | 6 Byte Config-Nutzdaten |
| `0x12` | `CONFIG_COMMIT` | Config pruefen und speichern |
| `0x13` | `CONFIG_READ` | Config-Dump an Host senden |
| `0x20` | `MACRO_BEGIN` | Makro-Transfer starten |
| `0x21` | `MACRO_DATA` | 6 Byte Makro-Nutzdaten |
| `0x22` | `MACRO_COMMIT` | Makros speichern |
| `0x23` | `MACRO_READ` | Makro-Dump an Host senden |
## Events
| ID | Name | Zweck |
|---|---|---|
| `0x81` | `KEY_DOWN` | Host-Button gedrückt |
| `0x82` | `KEY_UP` | Host-Button losgelassen |
| `0x83` | `ENC_CW` | Encoder-Host-Action im Uhrzeigersinn |
| `0x84` | `ENC_CCW` | Encoder-Host-Action gegen Uhrzeigersinn |
| `0x85` | `PONG` | Antwort auf Ping |
| `0x86` | `STATUS` | Antwort auf `READ_STATUS`, Byte 1 = aktives Profil 0-2 |
| `0x90` | `CONFIG_ACK` | Config erfolgreich gespeichert |
| `0x91` | `CONFIG_NACK` | Config ungueltig oder NVM-Timeout |
| `0x92` | `CONFIG_BEGIN` | Config-Dump beginnt |
| `0x93` | `CONFIG_DATA` | 6 Byte Config-Dump |
| `0x94` | `CONFIG_END` | Config-Dump fertig |
| `0x95` | `MACRO_ACK` | Makros erfolgreich gespeichert |
| `0x96` | `MACRO_BEGIN` | Makro-Dump beginnt |
| `0x97` | `MACRO_DATA` | 6 Byte Makro-Dump |
| `0x98` | `MACRO_END` | Makro-Dump fertig |
| `0x99` | `MACRO_NACK` | Makro-Speichern fehlgeschlagen |
Bei `ActionType::HOST_COMMAND` enthält Byte 1 die Matrix-Key-ID oder
Encoder-ID. Die 16-Bit-Command-ID aus `SAction.data` steht little-endian in
Byte 2 und 3. Encoder-Actions verwenden die richtungsspezifischen IDs
`0x83/0x84`.
## Chunk-Zahlen
Aktuelle Blob-Groessen:
- Config: `740` Byte
- Makros: `512` Byte
Bei 6 Nutzbytes pro Paket ergibt das:
```text
Config: ceil(740 / 6) = 124 Chunks
Makros: ceil(512 / 6) = 86 Chunks
```
Wichtig fuer Implementierungen: Der Byte-Offset eines Chunks muss mindestens 16 Bit breit sein. Bei der Config liegt der Offset ab Chunk 43 ueber 255 Byte; ein 8-Bit-Offset wuerde ueberlaufen und spaetere Profilbereiche falsch dumpen.
## Transferablauf
### PC -> Board
```text
BEGIN(chunk_count)
DATA 0
DATA 1
...
COMMIT
```
### Board -> PC
```text
BEGIN(chunk_count)
DATA 0
DATA 1
...
END
```
## Validierung
`CONFIG_COMMIT` prueft:
- Magic
- Version
- CRC
Nur bei erfolgreicher Pruefung wird in NVM geschrieben.
`MACRO_COMMIT` schreibt ohne CRC direkt nach NVM und signalisiert nur Erfolg oder Fehler.
Beide Empfangspfade erwarten exakt die berechnete Chunkzahl, markieren jeden
Index einmalig und akzeptieren `COMMIT` nur nach einem vollständigen Transfer.
Doppelte oder außerhalb des Bereichs liegende Chunks machen den Transfer
ungültig und führen beim Commit zu NACK.
Config-Commit prüft zusätzlich Magic, Version, CRC, Profilindex,
Actiontypen/-daten, LED-Enums und kritische Animationsperioden.
Makro-Commit prüft die Vollständigkeit und alle HID-Keycodes. Die Makrotabelle
besitzt weiterhin keine eigene persistente CRC.
## READ_STATUS vs. CONFIG_READ fuer Polling
`CONFIG_READ` sendet den kompletten 740-Byte-Dump synchron und blockierend
aus `poll_vendor()`, bevor `updateLEDs()` im selben Loop-Durchlauf drankommt
(siehe Kommentar am Dateianfang von `CMainController.cpp`) bei
wiederholtem Polling (z.B. Live-Sync in der Windows-App, die nur das aktive
Profil braucht) fuehrt das sichtbar zu ins Stocken geratenden LED-Pulse-
Animationen, weil `updateLEDs()` bei jedem Poll um die Dump-Dauer verzoegert
wird. `READ_STATUS` liest nur `active_profile` aus dem NVM-Config und
schickt ein einzelnes Paket zurueck kein Chunking, keine mehrfachen
`SerialUSB.write()`-Aufrufe, kein spuerbarer Einfluss auf die
LED-Animation. Für periodisches Profil-Polling immer `READ_STATUS`
verwenden, `CONFIG_READ` nur fuer den tatsaechlichen vollen Dump (z.B.
"Vom Board laden" im Programmiermodus).
## Praktische Hinweise fuer die GUI
- nach `CONFIG_COMMIT` auf `CONFIG_ACK` oder `CONFIG_NACK` warten
- danach erst `MACRO_*` senden
- Dumps besser sequenziell lesen: zuerst Config, danach Makros
- `DtrEnable` muss aktiv sein, sonst verwirft das Board CDC-Ausgaben
- ausschließlich vollständige 8-Byte-Pakete schreiben; schon ein verlorenes
Byte verschiebt die Paketgrenzen für alle folgenden Daten
- Host-Command-ID little-endian aus Byte 2/3 lesen
- Config- und Makro-Dumps ebenfalls auf Chunkzahl, eindeutige Indizes und
Vollständigkeit prüfen
## Implementierungsdetails
- RX-Ringbuffer: 256 Byte = 32 volle Pakete; er ist ein laufender
Zwischenpuffer und fasst keinen kompletten Configtransfer
- feste 8-Byte-Pakete vereinfachen Firmware und GUI
- nach einem reinen SWD-Reflash kann ein physischer USB-Reconnect noetig sein