Harden firmware state and transfer handling

This commit is contained in:
2026-07-24 09:49:21 +02:00
parent 50dbf8fbee
commit ce5db617a1
27 changed files with 558 additions and 231 deletions
+4 -5
View File
@@ -119,8 +119,7 @@ Der Werksreset ist keine PC-Funktion, sondern Teil der Firmware:
## Nebenläufigkeit
Encoder-Callbacks laufen im EIC-Interrupt, Matrixcallbacks im Loop. Beide
schreiben in dieselbe `CEventQueue`; `processEvents()` liest im Loop. Die Queue
besitzt aktuell keinen Interruptschutz für den Matrix-Push und verwirft Events
bei Überlauf. Das ist eine bekannte Einschränkung, keine garantierte
Multi-Producer-Sicherheit; siehe
[09_known_limitations.md](09_known_limitations.md).
schreiben in dieselbe `CEventQueue`; `processEvents()` liest im Loop. Der
Matrixcallback maskiert Interrupts während seines Queue-Pushs, sodass Loop und
ISR den Tail-Index nicht gleichzeitig ändern. Bei Überlauf werden Events
weiterhin verworfen.
+2 -2
View File
@@ -44,5 +44,5 @@ key_id = col * MATRIX_ROWS + row
- Encoder-SW-Tasten gehen durch denselben Matrix-Pfad (`COL_0`)
- `matrix_scan()` wird einmal pro `loop()` aufgerufen
- Der Callback schreibt in dieselbe Queue wie die Encoder-ISRs. Ein
Encoderinterrupt kann den Matrix-Push unterbrechen; die aktuelle Queue
schützt diesen Multi-Producer-Fall nicht ausdrücklich.
kurzer `noInterrupts()`/`interrupts()`-Abschnitt schützt den Matrix-Push
davor, von einem Encoderinterrupt unterbrochen zu werden.
+3 -5
View File
@@ -45,11 +45,9 @@ static void isr_enc0_b() { handle_encoder(0); }
- ISR-Wrapper führen nur Dekodierung und Queue-Push aus; sie rufen kein USB,
NVM, LED-Rendering oder `delay()` auf
Wichtig: Die Queue wird zusätzlich vom Matrixcallback im Loop beschrieben.
Ein Encoderinterrupt kann diesen Push unterbrechen. Die Implementierung ist
damit nicht streng Single-Producer und besitzt für diesen Fall aktuell keinen
Interruptschutz. Details:
[09_known_limitations.md](09_known_limitations.md).
Die Queue wird zusätzlich vom Matrixcallback im Loop beschrieben. Dieser
Loop-Push läuft in einer kurzen Critical Section; gleichpriorisierte
Encoder-ISRs unterbrechen sich auf dem Cortex-M0+ nicht gegenseitig.
## Initialisierung
+18 -14
View File
@@ -25,9 +25,9 @@ Das `packed` ist zwingend, weil Config v3 bytegenau zwischen Firmware und GUI ue
| `NONE` | keine Aktion | - |
| `HID_KEY` | Tastaturtaste ueber USB HID | low byte = keycode, high byte = modifier |
| `HID_CONSUMER` | Media/Consumer-HID | usage id |
| `HOST_COMMAND` | Event an die GUI | aktuell nicht ausgewertet |
| `HOST_COMMAND` | Event an die GUI | 16-Bit-Command-ID |
| `MACRO` | Makro aus `SMacroTable` | slot 0..31 |
| `PROFILE_SWITCH` | Profilwechsel | 0..2 oder `0xFF` fuer naechstes Profil |
| `PROFILE_SWITCH` | Profilwechsel | 0..2, `0x00FF` oder `0xFFFF` für nächstes Profil |
## Verhalten bei `KEY_DOWN`
@@ -35,7 +35,7 @@ Das `packed` ist zwingend, weil Config v3 bytegenau zwischen Firmware und GUI ue
|---|---|
| `HID_KEY` | `usb_hid_send_key()` |
| `HID_CONSUMER` | `usb_hid_send_consumer()` |
| `HOST_COMMAND` | `USB_EVT_KEY_DOWN (0x81)` mit `key_id` senden |
| `HOST_COMMAND` | `USB_EVT_KEY_DOWN (0x81)` mit `key_id` und Command-ID senden |
| `MACRO` | komplette Sequenz sofort abspielen |
| `PROFILE_SWITCH` | Config aus NVM laden, Profil aendern, CRC neu berechnen, speichern, Buttons neu initialisieren |
| `NONE` | nichts |
@@ -46,7 +46,7 @@ Das `packed` ist zwingend, weil Config v3 bytegenau zwischen Firmware und GUI ue
|---|---|
| `HID_KEY` | `usb_hid_release_key()` |
| `HID_CONSUMER` | `usb_hid_release_consumer()` |
| `HOST_COMMAND` | keine Ausgabe; `USB_EVT_KEY_UP` ist nur definiert |
| `HOST_COMMAND` | `USB_EVT_KEY_UP (0x82)` mit `key_id` und Command-ID senden |
| `MACRO` | nichts |
| `PROFILE_SWITCH` | nichts |
| `NONE` | nichts |
@@ -62,25 +62,29 @@ down -> delay(10 ms) -> up
- Makros laufen komplett synchron in der Firmware.
Das Hold-Modell verwaltet keine Menge gleichzeitig gedrückter Tasten. Ein
neuer Keyboard-Down ersetzt den vorherigen Report und jeder Keyboard-Up leert
den gesamten Report. Consumer-HID hat dieselbe Einschränkung mit genau einem
Usage-Wert.
Keyboard-Keys und Modifier werden im HID-HAL referenzgezählt. Bis zu sechs
unterschiedliche Keyboard-Usages können der Report gleichzeitig abbilden;
beim Loslassen einer Action bleiben die übrigen Holds aktiv.
Der Consumer-Descriptor kann jeweils nur ein Usage darstellen. Der HAL
verwaltet mehrere Holds und zeigt das zuletzt gedrückte aktive Usage; nach
dessen Release wird das zuvor aktive Usage wiederhergestellt.
## Host-Commands
Der aktuelle Code sendet für jede `HOST_COMMAND`-Action nur:
```text
Byte 0 = USB_EVT_KEY_DOWN (0x81)
Byte 0 = USB_EVT_KEY_DOWN (0x81) oder USB_EVT_KEY_UP (0x82)
Byte 1 = Matrix-Key-ID oder Encoder-ID
Byte 2 = Command-ID Low-Byte
Byte 3 = Command-ID High-Byte
```
`SAction.data` wird dabei nicht übertragen. Auch Encoder-Actions verwenden
aktuell `0x81`; die definierten Events `ENC_CW (0x83)` und `ENC_CCW (0x84)`
werden nicht emittiert. Ein Release erzeugt kein `KEY_UP`-Paket. Das ist
aktuelles Verhalten und als Einschränkung in
[09_known_limitations.md](09_known_limitations.md) festgehalten.
Encoder-Host-Actions senden genau ein Richtungsereignis:
`ENC_CW (0x83)` beziehungsweise `ENC_CCW (0x84)`, ebenfalls mit Encoder-ID und
Command-ID. Die GUI erhält damit die konfigurierte Action direkt aus dem
Event und muss den aktiven Profilstand nicht rekonstruieren.
## Makro-Ausfuehrung
+8 -4
View File
@@ -52,7 +52,8 @@ sie nicht: `SAction.data` wird direkt als Slotindex `0..31` verwendet.
- kopiert 512 Byte aus NVM in `SMacroTable`
- erkennt komplett geloeschten Flash (`0xFF`) als "noch nie beschrieben"
- setzt dann eine leere Tabelle
- prüft alle belegten Steps gegen den HID-Descriptor (`keycode <= 0x65`)
- setzt bei gelöschtem oder ungültigem Inhalt eine leere Tabelle
Eine leere Tabelle ist also ein gueltiger Default-Zustand.
@@ -60,15 +61,18 @@ Eine leere Tabelle ist also ein gueltiger Default-Zustand.
`macro_config_save()`:
1. Tabelle in einen 4-Byte-aligned Puffer kopieren
2. beide Rows loeschen
3. 8 Pages zu je 64 Byte schreiben
1. HID-Keycodes validieren
2. Tabelle in einen 4-Byte-aligned Puffer kopieren
3. beide Rows loeschen
4. 8 Pages zu je 64 Byte schreiben
Rueckgabewert:
- `true` bei Erfolg
- `false` bei NVM-Timeout
`static_assert` schützt die erwarteten Größen 2 und 512 Byte beim Build.
## Ausfuehrung
Beim Triggern eines Makros:
+5 -5
View File
@@ -73,12 +73,12 @@ Die Firmware verwendet eine eigene ganzzahlige Hue-Umrechnung und ruft
`Adafruit_NeoPixel::ColorHSV()` nicht auf.
Für `PULSE` muss `period_ms >= 2` gelten, da der Code durch die halbe Periode
teilt. Eingehende Configs validieren diesen Grenzwert aktuell nicht.
teilt. Configvalidierung lehnt kleinere Werte ab; `set_anim()` klemmt direkte
interne Aufrufe zusätzlich auf mindestens 2 ms.
`COLOR_FADE` benötigt `set_color_fade(to, period_ms)`, weil dort Start- und
Zielfarbe gesetzt werden. Ein bloßes `set_anim(COLOR_FADE, ...)`, wie es beim
direkten Laden dieses Enum-Werts aus der Config geschieht, initialisiert diese
Farben nicht aus den Configdaten.
`COLOR_FADE` benötigt `set_color_fade(to, period_ms)`. Beim Laden aus der
Config interpretiert der Controller die gespeicherte Base-Farbe als Ziel und
startet einen einmaligen Fade von Schwarz zu dieser Farbe.
### Phasenversatz (Regenbogen-Wellen)
+12 -15
View File
@@ -17,12 +17,10 @@ Dateien:
Makros und Config sind komplett getrennt.
Diese Tabelle beschreibt die Adressen, auf die der Laufzeitcode zugreift. Das
aktive Linker-Skript reserviert davon aktuell nur `0x1FE00..0x1FFFF`
ausdrücklich; Makrobereich und erste Config-Row liegen noch im zulässigen
Firmware-ROM. Das aktuelle Binary ist klein genug, aber die Bereiche sind
nicht vollständig gegen Firmwarewachstum geschützt. Siehe
[09_known_limitations.md](09_known_limitations.md).
Das aktive Linker-Skript reserviert `0x1FB00..0x1FFFF` als eigenen
NVM-Memory-Bereich. Die Boarddefinition meldet entsprechend höchstens
129.792 Byte Firmware-Flash. Damit kann ein erfolgreich gelinktes Image die
fünf NVM-Rows nicht überdecken.
## `SDeviceConfig`
@@ -101,18 +99,17 @@ Praktisch sichtbares Ergebnis:
`nvm_config_load()`:
1. 740 Byte aus NVM kopieren
2. Magic pruefen
3. Version pruefen
4. CRC pruefen
5. bei Fehlern Defaults laden und `false` zurueckgeben
2. Magic, Version und CRC prüfen
3. Profilindex, Actiontypen/-daten und LED-Enums prüfen
4. `PULSE`-Perioden auf mindestens 2 ms prüfen
5. bei Fehlern Defaults laden und `false` zurückgeben
Die Firmware faellt also immer auf einen gueltigen Zustand zurueck.
Die Defaults werden bei diesem Fallback nur in das übergebene RAM-Struct
geschrieben und nicht automatisch in Flash persistiert.
Nach erfolgreicher CRC-Prüfung sichert `load()` einen zu großen
`active_profile` im RAM auf Profil 0 ab. Andere Felder und Enum-Werte werden
nicht auf gültige Bereiche geprüft.
Dieselbe Validierung wird vor einem Config-Commit aus dem CDC-Protokoll
verwendet.
## Speichern
@@ -134,8 +131,8 @@ Wichtig:
- der Schreibpuffer muss 4-Byte-aligned sein
- `packed` allein reicht dafuer nicht
- `nvm_config_save()` berechnet die CRC nicht selbst; der Aufrufer muss
`cfg.crc = nvm_config_crc(cfg)` vorher setzen
- `nvm_config_save()` berechnet die CRC in einer lokalen Kopie immer neu
- `static_assert` schützt die erwarteten Größen 3, 236 und 740 Byte beim Build
## Zusammenhang mit Werksreset
+19 -17
View File
@@ -50,10 +50,10 @@ Es gibt kein Framing, keinen Längenheader und keine Prüfsumme auf Paketebene.
| ID | Name | Zweck |
|---|---|---|
| `0x81` | `KEY_DOWN` | wird für jede aktuelle Host-Action gesendet |
| `0x82` | `KEY_UP` | definiert, aktuell nicht gesendet |
| `0x83` | `ENC_CW` | definiert, aktuell nicht gesendet |
| `0x84` | `ENC_CCW` | definiert, aktuell nicht gesendet |
| `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 |
| `0x90` | `CONFIG_ACK` | Config erfolgreich gespeichert |
| `0x91` | `CONFIG_NACK` | Config ungueltig oder NVM-Timeout |
@@ -66,9 +66,10 @@ Es gibt kein Framing, keinen Längenheader und keine Prüfsumme auf Paketebene.
| `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 die
Encoder-ID. `SAction.data` wird nicht übertragen. Auch CW- und CCW-Actions
senden aktuell `0x81`; die Drehrichtung ist im Paket nicht enthalten.
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
@@ -120,16 +121,15 @@ Nur bei erfolgreicher Pruefung wird in NVM geschrieben.
`MACRO_COMMIT` schreibt ohne CRC direkt nach NVM und signalisiert nur Erfolg oder Fehler.
Die in `BEGIN` angekündigte Chunkzahl wird zwar gespeichert, aber beim Commit
nicht ausgewertet. Die Firmware verfolgt nicht, welche Chunkindizes tatsächlich
eingetroffen sind. Doppelte, fehlende und ungeordnete Chunks werden deshalb
nicht als solche erkannt. Bei Configdaten schlägt ein unvollständiger Transfer
typischerweise an der CRC fehl; bei Makrodaten kann ein unvollständiger
Null-gefüllter Puffer gespeichert werden.
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.
Die Configvalidierung prüft keine Feldwerte oder Enum-Bereiche. Insbesondere
müssen Hostimplementierungen gültige Profilindizes, Actiontypen, LED-Enums und
Animationsperioden liefern.
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.
## Praktische Hinweise fuer die GUI
@@ -139,7 +139,9 @@ Animationsperioden liefern.
- `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
- Encoder-Host-Actions derzeit nicht anhand von `0x83/0x84` erwarten
- 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
+9 -6
View File
@@ -9,7 +9,7 @@ LLM-basierten Coding-Agent zusätzlich die Anweisungen in
- PlatformIO Core oder PlatformIO IDE
- USB-Kabel für Laufzeittests
- Atmel-ICE beziehungsweise kompatibler CMSIS-DAP-Adapter für den Upload
- Zugriff auf die separat gepflegte Windows-GUI, wenn das CDC-Protokoll oder
- das benachbarte Repository `../VersaGUI`, wenn das CDC-Protokoll oder
persistente Formate geändert werden
PlatformIO lädt den Arduino-SAMD-Core, OpenOCD und `Adafruit NeoPixel` über
@@ -66,7 +66,8 @@ und NVM-Schreibvorgängen. Es gibt keinen Scheduler und keine Threads.
## Verifikation
Es gibt derzeit keine automatisierten Tests. Der minimale lokale Check ist:
Für die Firmware gibt es derzeit keine automatisierten Tests. Der minimale
lokale Check ist:
```bash
pio run -e versapad
@@ -82,9 +83,12 @@ Je nach Änderung folgen Hardwaretests:
- NVM: Power-Cycle, ungültige CRC und Werksreset
- LEDs: alle Animationen, Helligkeit und temporäre Overrides
Die GUI ist ein externer Vertrag. Änderungen an `SDeviceConfig`,
`SMacroTable`, Action-Werten oder USB-IDs sind erst vollständig verifiziert,
wenn Firmware und GUI dieselben Bytes senden und interpretieren.
Die GUI ist ein separates Git-Repository im selben Workspace. Änderungen an
`SDeviceConfig`, `SMacroTable`, Action-Werten oder USB-IDs sind erst
vollständig verifiziert, wenn Firmware und `../VersaGUI` dieselben Bytes
senden und interpretieren. `DelphiGUI` gehört nicht zum gepflegten Scope.
Die automatisierten GUI-Vertragstests liegen unter
`../VersaGUI/tests/VersaGUI.ContractTests/`.
## Dokumentation mitpflegen
@@ -92,4 +96,3 @@ Bei jedem Change die betroffene Fachdokumentation aktualisieren. Zahlen wie
Structgrößen, Offsets, Chunk-Anzahlen und Flashgrenzen immer aus dem neuen Code
neu ableiten. Offene oder absichtlich nicht behobene Punkte gehören nach
[`09_known_limitations.md`](09_known_limitations.md).
+49 -64
View File
@@ -1,93 +1,78 @@
# Bekannte Einschränkungen und Risiken
Diese Liste beschreibt den aktuellen Implementierungsstand. Sie ist keine
Liste bereits umgesetzter Features.
Diese Liste beschreibt den aktuellen Implementierungsstand nach den
Robustheitskorrekturen. Sie ist keine Liste bereits umgesetzter Features.
## Flash-Reservierung stimmt nicht vollständig mit dem NVM-Zugriff überein
## Bootloader-Ziel bleibt nicht unterstützt
Die Firmware liest und schreibt:
```text
0x1FB00..0x1FCFF Makros (512 Byte)
0x1FD00..0x1FFFF Config (768 Byte, davon 740 Byte genutzt)
```
Das aktive Linker-Skript `flash_without_bootloader.ld` erlaubt Firmware jedoch
bis einschließlich `0x1FDFF` und reserviert nur `0x1FE00..0x1FFFF`. Damit sind
`0x1FB00..0x1FDFF` nicht gegen ein zukünftig wachsendes Firmware-Image
geschützt. Das aktuelle Image liegt deutlich darunter, aber der Build prüft
diese NVM-Grenze nicht.
Das Bootloader-Linker-Skript reserviert aktuell gar keinen separaten
NVM-Bereich. Das auskommentierte USB-Bootloader-Environment ist daher kein
unterstütztes Ziel.
Das aktive Ziel `versapad_nobl` reserviert den kompletten Bereich
`0x1FB00..0x1FFFF` für Makros und Config. Das auskommentierte
USB-Bootloader-Environment verwendet dagegen weiterhin eine historische
Board-/Linker-Konfiguration und ist nicht als Produktionsziel verifiziert.
Die aktive Boarddatei benennt die MCU als `samd21g17d`, setzt für den
Arduino-Core aber weiterhin das Kompatibilitätsmakro `__SAMD21G18A__`. Der
PlatformIO-Build meldet korrekt 128 KiB Flash und 16 KiB RAM; vor
device-spezifischen Änderungen sollte diese historische Makro-Abweichung
trotzdem geprüft werden.
PlatformIO-Build meldet korrekt 128 KiB physischen Flash, 16 KiB RAM und
129.792 Byte nutzbaren Firmwarebereich. Vor device-spezifischen
Core-Änderungen sollte die historische Makro-Abweichung trotzdem geprüft
werden.
## Event-Queue hat gemischte Producer
## Event-Queue hat eine feste Kapazität
Matrixevents werden im Loop erzeugt, Encoderevents in EIC-Interrupts. Beide
rufen `CEventQueue::push()` auf und verändern denselben Tail-Index ohne
Interruptschutz. Ein Encoderinterrupt kann einen Matrix-Push unterbrechen.
Die bisherige Annahme eines reinen Single-Producer/Single-Consumer-Ringbuffers
ist deshalb nicht vollständig erfüllt; seltene verlorene oder überschriebene
Events sind theoretisch möglich.
Matrix- und Encoder-Producer verändern den Tail-Index nicht mehr gleichzeitig:
Der Matrixcallback maskiert Interrupts während seines Queue-Pushs.
Zusätzlich werden Events bei voller Queue still verworfen.
Die Queue besitzt aber weiterhin nur 16 nutzbare Slots. Bei Überlauf wird ein
neues Event ohne Hostmeldung verworfen. Das kann vor allem während
blockierender Makro-, NVM- oder Feedbackpfade auftreten.
## `HOST_COMMAND` nutzt seine `data` nicht
## Host-Command-Ausführung liegt in der Desktop-App
`SAction.data` ist für `ActionType::HOST_COMMAND` vorhanden, wird in
`execute_action_down()` aber nicht übertragen. Gesendet wird nur
`USB_EVT_KEY_DOWN (0x81)` mit der Matrix-Key-ID beziehungsweise Encoder-ID.
Die Firmware überträgt Command-ID, Key-/Encoder-ID und Eventrichtung
vollständig. VersaGUI validiert und empfängt diese Pakete, führt eine
Command-ID aber noch nicht als Prozess-, URL- oder frei konfigurierbare
Hostaktion aus. Eine spätere Implementierung benötigt ein explizites,
sicheres Mapping; beliebige Command-Strings sollten nicht direkt an eine Shell
weitergegeben werden.
`USB_EVT_KEY_UP (0x82)`, `USB_EVT_ENC_CW (0x83)` und
`USB_EVT_ENC_CCW (0x84)` sind definiert, werden vom aktuellen Controller aber
nicht gesendet. Bei Encoder-Host-Actions geht dadurch die Richtung im
gesendeten Paket verloren, sofern die Host-Anwendung sie nicht anderweitig aus
der konfigurierten Action ableitet.
## Grenzen des HID-Reports
## HID-Holds sind global
Keyboard-Holds und Modifier werden referenzgezählt. Der USB-Descriptor kann
maximal sechs unterschiedliche Keyboard-Usages gleichzeitig darstellen.
Weitere Holds bleiben intern aktiv und rücken nach, sobald ein Report-Slot
frei wird.
Der Keyboard-Report enthält zwar sechs Keycode-Felder, die Implementierung
setzt aber nur das erste. Jeder neue `HID_KEY`-Down ersetzt den vorherigen
Report; jedes Release sendet einen komplett leeren Report. Analog existiert
nur ein globaler Consumer-Usage-Wert. Gleichzeitige unabhängige Holds werden
daher nicht korrekt verwaltet.
Der Consumer-Descriptor enthält genau ein Usage. Mehrere Consumer-Holds werden
intern verwaltet, sichtbar ist jeweils das zuletzt gedrückte aktive Usage.
## CDC-Transfers sind nur schwach validiert
## CDC bleibt ein festes, ungeframtes Paketprotokoll
- `BEGIN` speichert die angekündigte Chunkzahl, `COMMIT` vergleicht sie aber
nicht mit empfangenen Chunks.
- Doppelte, fehlende oder ungeordnete Chunks werden nicht verfolgt.
- Config-Commit prüft Magic, Version und CRC, aber keine Feldwerte oder
Enum-Bereiche.
- Makro-Commit hat weder CRC noch Vollständigkeitsprüfung.
- Das 8-Byte-Protokoll besitzt kein Framing. Ein verlorenes oder zusätzliches
Byte desynchronisiert alle folgenden Pakete.
Config- und Makrotransfers prüfen jetzt Chunkzahl, eindeutige Indizes und
Vollständigkeit. Config besitzt zusätzlich CRC und Feldvalidierung.
Insbesondere kann ein formal CRC-korrektes Profil ungültige Animationswerte
enthalten. `PULSE` benötigt in der aktuellen Arithmetik eine Periode von
mindestens 2 ms; dieser Mindestwert wird nicht validiert.
Weiterhin gilt:
## Farbanimationen umgehen Teile der Helligkeits-/Override-Logik
- Das Protokoll hat kein Byte-Framing. Ein verlorenes oder zusätzliches Byte
verschiebt die 8-Byte-Paketgrenzen bis zum Reconnect.
- Einzelpakete besitzen keine Sequenznummer oder Prüfsumme.
- Die Makrotabelle besitzt im NVM keine persistente CRC; beim Transfer werden
nur Vollständigkeit und HID-Keycode-Bereiche geprüft.
## Farbanimationen und Helligkeit
Globale und LED-spezifische Helligkeit werden beim Laden in die Base-Farbe
eingerechnet. `COLOR_CYCLE` berechnet RGB dagegen direkt mit festen 40 %
Helligkeit und ignoriert Base-Farbe sowie Override. `COLOR_FADE` benötigt den
separaten Aufruf `set_color_fade()`; das reine Laden des Enum-Werts aus einer
Config setzt keine Start- und Zielfarbe.
Helligkeit und ignoriert Base-Farbe sowie Override.
`COLOR_FADE` aus der Config wird als einmaliger Fade von Schwarz zur
gespeicherten Base-Farbe interpretiert.
## Reservierte beziehungsweise noch ungenutzte Hardware und Felder
- Die drei Fader-Pins sind im Variant und in `config/pins.h` definiert, werden
von der Firmware aber nicht eingelesen.
- `enc_sensitivity[4]` wird gespeichert und mit Default `1` befüllt, beeinflusst
die Encoderdekodierung derzeit aber nicht.
absichtlich noch nicht von der Firmware eingelesen.
- `enc_sensitivity[4]` wird gespeichert und mit Default `1` befüllt,
beeinflusst die Encoderdekodierung derzeit aber nicht.
- `SET_LED_BASE` verändert nur den RAM-Zustand und wird nicht in NVM
persistiert.