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>
168 lines
5.6 KiB
Markdown
168 lines
5.6 KiB
Markdown
# 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
|