Files
buzzer/firmware/Protokoll.md
2026-03-02 00:25:40 +01:00

304 lines
8.9 KiB
Markdown

# Buzzer-Kommunikationsprotokoll (vNext Entwurf)
Diese Datei beschreibt den geplanten Neuentwurf des Protokolls. Ziel ist ein klar strukturiertes Binärprotokoll mit stabilen Typen, expliziter Endianness und sauberem Streaming für große Daten.
## 1) Grundregeln
- Transport: USB CDC (Byte-Stream)
- Multi-Byte-Integer: **Little Endian**
- Integer-Typen im Protokoll: nur feste Breiten (`uint8_t`, `uint16_t`, `uint32_t`, `uint64_t`)
- `bool`: als `uint8_t` (`0` = false, `1` = true)
- Strings: immer `len + bytes`, ohne implizites `\0`
- `unsigned long` wird im Protokoll **nicht** verwendet (plattformabhängig)
> Hinweis: Obwohl nRF52 und STM32G0 beide Little Endian sind, bleibt Endianness explizit Teil der Spezifikation.
## 2) Frame-Struktur
Alle Requests und Responses nutzen denselben Header.
### 2.1 Header
| Feld | Typ | Beschreibung |
|---|---|---|
| `sync` | 4 Byte | Immer `BUZZ` (`0x42 0x55 0x5A 0x5A`) |
| `frame_type` | `uint8_t` | Typ des Frames (siehe unten) |
| `command_id` | `uint8_t` | Kommando-ID |
| `sequence` | `uint16_t` | Anfrage-/Antwort-Korrelation |
| `payload_len` | `uint32_t` | Länge von `payload` in Bytes |
| `header_crc16` | `uint16_t` | CRC16 über Header ohne `sync` und ohne `header_crc16` |
Danach folgen:
| Feld | Typ | Beschreibung |
|---|---|---|
| `payload` | `byte[payload_len]` | Kommandoabhängige Nutzdaten |
| `payload_crc32` | `uint32_t` | CRC32 über `payload` |
### 2.2 `frame_type`
| Wert | Name | Bedeutung |
|---:|---|---|
| `0x01` | `REQ` | Host → Device Anfrage |
| `0x10` | `RESP_ACK` | Erfolgreich, ohne Nutzdaten |
| `0x11` | `RESP_DATA` | Erfolgreich, mit Nutzdaten |
| `0x12` | `RESP_STREAM_START` | Start eines Datenstroms |
| `0x13` | `RESP_STREAM_CHUNK` | Chunk eines Datenstroms |
| `0x14` | `RESP_STREAM_END` | Ende eines Datenstroms |
| `0x7F` | `RESP_ERROR` | Fehlerantwort |
## 3) Antwortmodell (ACK vs DATA)
Ja, wir unterscheiden explizit:
- **`RESP_ACK`**: „OK ohne Payload“ (Host muss nur Erfolg verbuchen)
- **`RESP_DATA`**: „OK mit Payload“
- **`RESP_ERROR`**: Fehlercode + optionaler Detailtext
Damit ist Parsing eindeutig und ohne Sonderfälle wie „OK aber vielleicht mit Daten“.
## 4) Fehlercodes
Fehlercodes bleiben wie bisher inhaltlich erhalten (`P_ERR_*`), werden aber als Binärwert in `RESP_ERROR` übertragen.
### 4.1 Fehler-Payload (`RESP_ERROR`)
| Feld | Typ | Beschreibung |
|---|---|---|
| `error_code` | `uint8_t` | Protokollfehlercode |
| `detail_len` | `uint8_t` | Länge von `detail` |
| `detail` | `byte[detail_len]` | Optionaler Kurztext (ASCII/UTF-8) |
## 5) Kommandos (erste Aufteilung)
## 5.1 `0x00` `GET_PROTOCOL_VERSION`
**Request-Payload:** keine
**Response:** `RESP_DATA`
| Feld | Typ | Beschreibung |
|---|---|---|
| `protocol_version` | `uint16_t` | Versionsnummer des Protokolls |
## 5.2 `0x01` `GET_FIRMWARE_STATUS`
**Request-Payload:** keine
**Response:** `RESP_DATA`
| Feld | Typ | Beschreibung |
|---|---|---|
| `status` | `uint8_t` | `0` = pending, `1` = confirmed |
| `version_len` | `uint8_t` | Länge des Versionsstrings |
| `version` | `byte[version_len]` | Firmware-Version |
## 5.3 `0x02` `GET_FLASH_STATUS`
**Request-Payload:** keine
**Response:** `RESP_DATA`
| Feld | Typ | Beschreibung |
|---|---|---|
| `block_size` | `uint32_t` | Bytes pro Block |
| `total_blocks` | `uint32_t` | Gesamtblöcke |
| `free_blocks` | `uint32_t` | Freie Blöcke |
| `path_max_len` | `uint32_t` | Maximale erlaubte Pfadlänge in Bytes |
## 5.4 `0x10` `LIST_DIR`
**Request-Payload:**
| Feld | Typ | Beschreibung |
|---|---|---|
| `path_len` | `uint8_t` | Länge von `path` |
| `path` | `byte[path_len]` | Zielverzeichnis, z. B. `/` oder `/lfs/a` |
`path_len` darf den in `GET_FLASH_STATUS.path_max_len` gemeldeten Wert nicht überschreiten.
**Response:** `RESP_DATA`
`LIST_DIR` liefert Einträge als Stream:
1. `RESP_STREAM_START`
2. `RESP_STREAM_CHUNK` pro Verzeichniseintrag
3. `RESP_STREAM_END`
### `RESP_STREAM_START` Payload
| Feld | Typ | Beschreibung |
|---|---|---|
| `entry_count` | `uint32_t` | Anzahl Einträge, `0xFFFFFFFF` = unbekannt |
### `RESP_STREAM_CHUNK` Payload (ein Eintrag)
| Feld | Typ | Beschreibung |
|---|---|---|
| `entry_type` | `uint8_t` | `0` = Datei, `1` = Verzeichnis |
| `name_len` | `uint8_t` | Länge von `name` |
| `size` | `uint32_t` | Dateigröße in Bytes (`0` für Verzeichnisse) |
| `name` | `byte[name_len]` | Name ohne führenden Pfad |
### `RESP_STREAM_END` Payload
- leer
**Hinweis:** Rekursion bleibt Host-seitig. Der Host setzt aus angefragtem Basispfad + `name` den vollständigen Pfad zusammen.
## 5.5 `0x11` `CHECK_FILE_CRC`
**Request-Payload:**
| Feld | Typ | Beschreibung |
|---|---|---|
| `path_len` | `uint8_t` | Länge von `path` |
| `path` | `byte[path_len]` | Pfad zur Datei |
`path_len` darf den in `GET_FLASH_STATUS.path_max_len` gemeldeten Wert nicht überschreiten.
**Response:** `RESP_DATA`
| Feld | Typ | Beschreibung |
|---|---|---|
| `crc32` | `uint32_t` | CRC32 (Little Endian) über den Audio-Inhalt |
**Hinweis:** Die CRC-Berechnung folgt der aktuellen Firmware-Logik für Audio-Dateien (ohne angehängte Tag-Daten).
## 5.6 `0x12` `MKDIR`
**Request-Payload:**
| Feld | Typ | Beschreibung |
|---|---|---|
| `path_len` | `uint8_t` | Länge von `path` |
| `path` | `byte[path_len]` | Zielpfad für neues Verzeichnis |
`path_len` darf den in `GET_FLASH_STATUS.path_max_len` gemeldeten Wert nicht überschreiten.
**Response:** `RESP_ACK`
Bei Erfolg wird eine leere ACK-Antwort gesendet. Bei Fehlern `RESP_ERROR` mit passendem `P_ERR_*`.
## 5.7 `0x13` `RM`
**Request-Payload:**
| Feld | Typ | Beschreibung |
|---|---|---|
| `path_len` | `uint8_t` | Länge von `path` |
| `path` | `byte[path_len]` | Pfad zu Datei oder leerem Verzeichnis |
`path_len` darf den in `GET_FLASH_STATUS.path_max_len` gemeldeten Wert nicht überschreiten.
**Response:** `RESP_ACK`
Bei Erfolg wird eine leere ACK-Antwort gesendet. Bei Fehlern `RESP_ERROR` mit passendem `P_ERR_*`.
**Hinweis:** Rekursives Löschen bleibt Host-seitig (mehrere `LIST_DIR` + `RM` Aufrufe).
## 5.8 `0x20` `GET_TAG_BLOB`
**Request-Payload:**
| Feld | Typ | Beschreibung |
|---|---|---|
| `path_len` | `uint8_t` | Länge von `path` |
| `path` | `byte[path_len]` | Pfad zur Datei |
`path_len` darf den in `GET_FLASH_STATUS.path_max_len` gemeldeten Wert nicht überschreiten.
**Response:** Stream
1. `RESP_STREAM_START` mit `total_len:uint32_t`
2. `RESP_STREAM_CHUNK` mit rohen Tag-Blob-Bytes
3. `RESP_STREAM_END` (leer)
Der Blob-Inhalt ist die TLV-Metadatenstruktur aus `Tags.md` (ohne Footer `version+len+TAG!`).
## 5.9 `0x21` `SET_TAG_BLOB_START`
Startet eine Tag-Blob-Übertragung.
**Request-Payload:**
| Feld | Typ | Beschreibung |
|---|---|---|
| `path_len` | `uint8_t` | Länge von `path` |
| `path` | `byte[path_len]` | Pfad zur Datei |
| `total_len` | `uint16_t` | Gesamtlänge des folgenden Blobs |
**Response:** `RESP_ACK`
## 5.10 `0x22` `SET_TAG_BLOB_CHUNK`
Sendet einen weiteren Chunk des Blobs.
**Request-Payload:**
| Feld | Typ | Beschreibung |
|---|---|---|
| `data` | `byte[]` | Chunk-Daten |
**Response:** `RESP_ACK`
## 5.11 `0x23` `SET_TAG_BLOB_END`
Schließt den Upload ab und schreibt den Blob in die Datei (ersetzt bestehende Tags).
**Request-Payload:** leer
**Response:** `RESP_ACK`
Hinweis: Ein leerer Blob (`total_len=0`) ist erlaubt und entspricht "Tags leeren".
## 6) Große Daten (Dateien, Audio, Firmware)
Für mehrere MB nicht als einzelnes `RESP_DATA` senden, sondern streamen.
### 6.1 Warum nicht ein riesiges `payload_len`?
- Höheres Risiko bei Paketverlust/Timeout
- Mehr RAM-/Pufferdruck auf MCU
- Schlechtere Wiederanlaufstrategie bei Fehlern
### 6.2 Streaming-Ablauf
1. `RESP_STREAM_START` mit Metadaten
2. `RESP_STREAM_CHUNK` (mehrfach)
3. `RESP_STREAM_END` mit Gesamt-CRC/Abschlussstatus
### 6.3 Stream-Metadaten (`RESP_STREAM_START`)
| Feld | Typ | Beschreibung |
|---|---|---|
| `stream_id` | `uint16_t` | ID des Streams |
| `total_len` | `uint32_t` | Gesamtlänge in Bytes |
| `chunk_size` | `uint16_t` | Ziel-Chunkgröße |
### 6.4 Chunk-Payload (`RESP_STREAM_CHUNK`)
| Feld | Typ | Beschreibung |
|---|---|---|
| `stream_id` | `uint16_t` | Zuordnung zum Stream |
| `offset` | `uint32_t` | Byte-Offset im Gesamtstrom |
| `data_len` | `uint16_t` | Länge von `data` |
| `data` | `byte[data_len]` | Nutzdaten |
`total_len` ist `uint32_t`, damit sind bis 4 GiB abbildbar. Für euer Gerät ist das mehr als ausreichend und trotzdem standardisiert.
## 7) Offene Punkte für die Implementierung
- Fixe maximale `chunk_size` (z. B. 256/512/1024 Bytes)
- ACK/NACK auf Chunk-Ebene nötig oder „best effort + Retry auf Kommando-Ebene“?
- Timeout-/Retry-Policy pro Kommando
- Welche Kommandos zuerst in `protocol.c` migriert werden
## 8) Kurzfazit für den nächsten Schritt
- Ja, getrennte Kommandos sind sinnvoll.
- Ja, Endianness muss explizit definiert sein (Little Endian).
- Ja, `ACK` und `DATA` sollten als unterschiedliche Frame-Typen geführt werden.
- Für große Dateien: `uint32_t total_len` + Chunk-Streaming statt Einmal-Payload.