506 lines
12 KiB
Markdown
506 lines
12 KiB
Markdown
# Buzzer Protocol (Wire Specification)
|
|
|
|
Stand: 2026-03-18
|
|
Quelle: aktueller Implementierungsstand aus Firmware (`buzz_proto`, `fs_mgmt`, `ble_mgmt`) und Web-Client (`transport`, `parser`).
|
|
|
|
## Ziel und Scope
|
|
|
|
Das Buzzer Protocol ist ein binäres Frame-Protokoll für Host <-> Device Kommunikation.
|
|
|
|
Abgedeckte Funktionen:
|
|
- Capability-Abfrage (`PROTO_INFO`)
|
|
- Dateisystem-Info (`FS_INFO`)
|
|
- Verzeichnisliste als Stream (`LS`)
|
|
- Datei-/Tag-Download (`FILE_GET`, `TAGS_GET`)
|
|
- Datei-/Tag-Upload (`FILE_PUT`, `TAGS_PUT`)
|
|
- Datei löschen / umbenennen (`RM_FILE`, `RENAME_FILE`)
|
|
|
|
Nicht produktiv implementiert:
|
|
- `DEVICE_INFO` (`0x02`)
|
|
- Firmware-Update (`FW_*`, `FW_UPDATE`)
|
|
|
|
## Transport und Grundregeln
|
|
|
|
- Alle Integer-Felder sind Little Endian.
|
|
- Jedes Frame hat einen 3-Byte Header.
|
|
- `payload_length` enthält nur die Payload-Länge (ohne Header).
|
|
- Aktiver Produktiv-Transport ist BLE GATT (RX Write Without Response, TX Notify).
|
|
- Es darf genau ein Stream gleichzeitig aktiv sein (`LS`, `FILE_GET`, `FILE_PUT`, `TAGS_*`).
|
|
|
|
BLE Service UUIDs:
|
|
- Service: `e517d988-bab5-4574-8479-97c6cb115ca0`
|
|
- RX: `e517d988-bab5-4574-8479-97c6cb115ca1`
|
|
- TX: `e517d988-bab5-4574-8479-97c6cb115ca2`
|
|
|
|
## Frame-Format
|
|
|
|
### Header
|
|
|
|
```c
|
|
uint8_t frame_type;
|
|
uint16_t payload_length; // LE
|
|
```
|
|
|
|
|
|
### Paketstruktur
|
|
|
|
```mermaid
|
|
---
|
|
title: "Basic Packet Structure"
|
|
---
|
|
packet
|
|
+8: "Frame type"
|
|
+16: "Payload length (LE)"
|
|
+40: "Payload (variable length)"
|
|
```
|
|
|
|
### Maximalgröße
|
|
|
|
Firmware-Buffer ist slab-basiert (`CONFIG_BUZZ_PROTO_SLAB_SIZE`).
|
|
Der effektive Chunk für Transfers wird zusätzlich durch den Transport limitiert. Bei Bluetooth sind das zum Beispiel 3 Bytes:
|
|
|
|
`PROTO_INFO.max_chunk_size` wird dynamisch berechnet als:
|
|
|
|
`min(slab_size - 3, transport_max_payload - 3)`
|
|
|
|
Das Protokoll ist so ausgelegt, dass es mit mindestens 100 Bytes auskommen sollte.
|
|
|
|
## Frame-Typen
|
|
|
|
| Wert | Name | Richtung | Bedeutung |
|
|
|---|---|---|---|
|
|
| `0x00` | `REQUEST` | Host -> Device | API-Aufruf per `data_type` |
|
|
| `0x10` | `RESPONSE` | Device -> Host | Antwort auf `REQUEST` |
|
|
| `0x11` | `ACK` | Host <-> Device | Credit-basierte Flusskontrolle |
|
|
| `0x12` | `ERROR` | Device -> Host | Fehler mit errno-Code |
|
|
| `0x13` | `SUCCESS` | Device -> Host | Erfolgreicher Abschluss |
|
|
| `0x20` | `FILE_START` | Device -> Host | Start Download-Stream |
|
|
| `0x21` | `FILE_CHUNK` | Host <-> Device | Datenblock (Upload/Download) |
|
|
| `0x22` | `FILE_END` | Host <-> Device | Streamende mit CRC32 |
|
|
| `0x30` | `FW_START` | reserviert | nicht aktiv |
|
|
| `0x31` | `FW_CHUNK` | reserviert | aktuell `ENOSYS` |
|
|
| `0x32` | `FW_END` | reserviert | nicht aktiv |
|
|
| `0x40` | `LS_START` | Device -> Host | Start Verzeichnis-Stream |
|
|
| `0x41` | `LS_ENTRY` | Device -> Host | Verzeichniseintrag |
|
|
| `0x42` | `LS_END` | Device -> Host | Ende Verzeichnis-Stream |
|
|
|
|
## Data-Typen (`REQUEST`)
|
|
|
|
| Wert | Name | Status | Beschreibung |
|
|
|---|---|---|---|
|
|
| `0x01` | `PROTO_INFO` | aktiv | Protokollversion + max Chunkgröße |
|
|
| `0x02` | `DEVICE_INFO` | aktiv | Device-Infos (Board, Revision, SOC, ID) |
|
|
| `0x03` | `FS_INFO` | aktiv | Dateisystem- und Pfadinfos |
|
|
| `0x04` | `FW_INFO` | aktiv | Info über Firmware-Status und -Version sowie Kernelversion |
|
|
| `0x20` | `FILE_GET` | aktiv | Datei vom Device streamen |
|
|
| `0x21` | `FILE_PUT` | aktiv | Datei zum Device hochladen |
|
|
| `0x22` | `TAGS_GET` | aktiv | nur Tag-Bereich streamen |
|
|
| `0x23` | `TAGS_PUT` | aktiv | nur Tag-Bereich schreiben |
|
|
| `0x24` | `RM_FILE` | aktiv | Datei löschen |
|
|
| `0x25` | `RENAME_FILE` | aktiv | Datei umbenennen |
|
|
| `0x30` | `FW_UPDATE` | reserviert | aktuell nicht bedient |
|
|
| `0x40` | `LS` | aktiv | Verzeichnisliste starten |
|
|
|
|
## Request/Response-Formate
|
|
|
|
### Generischer Request
|
|
|
|
```c
|
|
uint8_t data_type;
|
|
// optional daten_typspezifische Parameter
|
|
```
|
|
|
|
Wire:
|
|
|
|
```text
|
|
[0x00][payload_len LE][data_type][optional...]
|
|
```
|
|
|
|
```mermaid
|
|
---
|
|
title: "Generic Requst Structure"
|
|
---
|
|
packet
|
|
+8: "Frame type REQUEST: 0x00"
|
|
+16: "Payload length (LE)"
|
|
+8: "Data type"
|
|
+32: "Optional payload (variable length)"
|
|
```
|
|
|
|
### `PROTO_INFO` (`0x01`)
|
|
|
|
Request: keine Zusatzdaten.
|
|
|
|
Response-Payload:
|
|
|
|
```c
|
|
uint8_t data_type; // 0x01
|
|
uint16_t version; // LE
|
|
uint16_t max_chunk_size; // LE
|
|
```
|
|
|
|
```mermaid
|
|
---
|
|
title: "PROTO_INFO response structure"
|
|
---
|
|
packet
|
|
+8: "Frame type RESPONSE: 0x10"
|
|
+16: "Payload length (LE): 5"
|
|
+8: "Data type PROTO_INFO: 0x01"
|
|
+16: "Protocol Version (LE)"
|
|
+16: "Max Chunk Size (LE)"
|
|
```
|
|
|
|
### `DEVICE_INFO` (`0x02`)
|
|
|
|
Request: keine Zusatzdaten.
|
|
|
|
Respone-Payload:
|
|
|
|
```c
|
|
uint8_t data_type; // 0x02
|
|
uint8_t[8] device_id;
|
|
uint8_t board_len;
|
|
uint8_t rev_len;
|
|
uint8_t soc_len;
|
|
uint8_t data[]; // board, rev und soc, ohne Nullterminierung
|
|
```
|
|
|
|
***Hinweis:*** In der aktuellen implementierung werden die Strings auf eine maximale Länge von 32 Zeichen beschränkt. Dies sollte für alle Fälle genügen.
|
|
|
|
### `FS_INFO` (`0x03`)
|
|
|
|
Request: keine Zusatzdaten.
|
|
|
|
Response-Payload:
|
|
|
|
```c
|
|
uint8_t data_type; // 0x03
|
|
uint32_t total_size; // LE
|
|
uint32_t free_size; // LE
|
|
uint8_t max_path_length;
|
|
uint8_t sys_path_length;
|
|
uint8_t audio_path_length;
|
|
uint8_t data[]; // sys_path + audio_path ohne Nullterminierung
|
|
```
|
|
|
|
Im `data` folgen sich System- und Audiopfad ohne Abstand, und ohne 0-Terminierung (`\0`). Beispiel für Systempfad `/lfs/sys`und Audiopfad `/lfs/a`:
|
|
|
|
`/lfs/sys/lfs/a`
|
|
|
|
`sys_path_len` ist in diesem Beispiel 8 und `audio_path_len` ist 6.
|
|
|
|
|
|
```mermaid
|
|
---
|
|
title: "FS_INFO response structure"
|
|
---
|
|
packet
|
|
+8: "Frame type RESPONSE: 0x10"
|
|
+16: "Payload length (LE): variable, 12 + sys path length + audio path length"
|
|
+8: "Data type FS_INFO: 0x03"
|
|
+32: "Total size (LE)"
|
|
+32: "Free size (LE)"
|
|
+8: "Max path length"
|
|
+8: "Sys path length"
|
|
+8: "Audio path length"
|
|
+40: "Sys path + Audio Path (variable)"
|
|
```
|
|
|
|
Beispielpaket mit **8 MiB Flash**, wovon **7 MiB frei** sind und den Pfaden `/lfs/sys` und `/lfs/a`. Die **maximale Pfadlänge** sind **32** Zeichen:
|
|
|
|
```mermaid
|
|
---
|
|
title: "FS_INFO response example"
|
|
---
|
|
packet
|
|
+8: "Frame type RESPONSE: 0x10"
|
|
+16: "Payload length (LE): 26 (0x1A 0x00)"
|
|
+8: "Data type FS_INFO: 0x03"
|
|
+32: "Total size (LE): 8388608 (0x00 0x00 0x80 0x00)"
|
|
+32: "Free size (LE): 7340032 (0x00 0x00 0x70 0x00)"
|
|
+8: "Max path length: 32"
|
|
+8: "Sys path length: 8"
|
|
+8: "Audio path length: 6"
|
|
+112: "data: '/lfs/sys/lfs/a'"
|
|
```
|
|
|
|
Das Beispiel schaut in HEX so aus:
|
|
```text
|
|
0x10 0x1A 0x00 0x03 0x00 0x00 0x80 0x00 0x00 0x00 0x70 0x00 0x20 0x08 0x06
|
|
0x2F 0x6C 0x66 0x73 0x2F 0x73 0x79 0x73 0x2F 0x6C 0x66 0x73 0x2F 0x61
|
|
```
|
|
|
|
### `FW_INFO` (`0x04`)
|
|
|
|
Request: keine Zusatzdaten
|
|
|
|
Response:
|
|
|
|
```c
|
|
uint8_t fw_status; /* 0x00: Confirmed, 0x01: Pending, 0x02: Testing, 0xFF: Unbekannt */
|
|
uint32_t slot1_size; /* (LE) Grösse des Firmware Update Slots */
|
|
uint8_t fw_version_len; /* Länge des Firmware-Versionsstring */
|
|
uint8_t kernel_version_len; /* Länge des Kernel-Versionsstrings */
|
|
uint8_t data[]; /* FW-Version und Kernelversion, ohne Nullterminierung */
|
|
```
|
|
|
|
***Hinweis:*** in der Aktuellen implementierung werden die Versionen auf 32 Zeichen limitiert.
|
|
|
|
### `LS` (`0x40`)
|
|
|
|
Request-Payload:
|
|
|
|
```c
|
|
uint8_t data_type; // 0x40
|
|
char path[]; // ohne Nullterminierung
|
|
```
|
|
|
|
### `FILE_GET` (`0x20`) und `TAGS_GET` (`0x22`)
|
|
|
|
Request-Payload:
|
|
|
|
```c
|
|
uint8_t data_type; // 0x20 oder 0x22
|
|
char path[]; // ohne Nullterminierung
|
|
```
|
|
|
|
Antwort ist ein Stream aus `FILE_START` -> `FILE_CHUNK`* -> `FILE_END`.
|
|
|
|
### `FILE_PUT` (`0x21`) und `TAGS_PUT` (`0x23`)
|
|
|
|
Request-Payload:
|
|
|
|
```c
|
|
uint8_t data_type; // 0x21 oder 0x23
|
|
uint32_t total_size; // LE
|
|
char path[]; // ohne Nullterminierung
|
|
```
|
|
|
|
Danach sendet der Host:
|
|
- `FILE_CHUNK` Frames
|
|
- abschließend `FILE_END` mit CRC32
|
|
|
|
### `RM_FILE` (`0x24`)
|
|
|
|
Request-Payload:
|
|
|
|
```c
|
|
uint8_t data_type; // 0x24
|
|
uint8_t path_length;
|
|
char path[]; // ohne Nullterminierung
|
|
```
|
|
|
|
### `RENAME_FILE` (`0x25`)
|
|
|
|
Request-Payload:
|
|
|
|
```c
|
|
uint8_t data_type; // 0x25
|
|
uint8_t old_path_length;
|
|
uint8_t new_path_length;
|
|
char paths[]; // old_path + new_path (jeweils ohne Nullterminierung)
|
|
```
|
|
|
|
## ACK / ERROR / SUCCESS
|
|
|
|
### ACK (`0x11`)
|
|
|
|
Payload:
|
|
|
|
```c
|
|
uint16_t credits; // LE
|
|
```
|
|
|
|
Wichtig: Es gibt zwei Semantiken je nach Richtung.
|
|
|
|
- Download (`LS`, `FILE_GET`, `TAGS_GET`):
|
|
Host -> Device, Credits werden im Device als absoluter neuer Stand gesetzt.
|
|
- Upload (`FILE_PUT`, `TAGS_PUT`):
|
|
Device -> Host, Credits sind zusätzlich gewährte Tokens (Host addiert sie).
|
|
|
|
### ERROR (`0x12`)
|
|
|
|
Payload:
|
|
|
|
```c
|
|
uint16_t error_code; // positiver errno-Wert, LE
|
|
```
|
|
|
|
Häufige Codes:
|
|
|
|
| Code | Name | Bedeutung |
|
|
|---|---|---|
|
|
| `1` | `EPERM` | fehlende Rechte |
|
|
| `2` | `ENOENT` | Datei/Ordner nicht gefunden |
|
|
| `5` | `EIO` | Flash-I/O-Fehler |
|
|
| `16` | `EBUSY` | Stream/Ressource belegt |
|
|
| `22` | `EINVAL` | ungültige Nutzdaten |
|
|
| `36` | `ENAMETOOLONG` | Pfad zu lang |
|
|
| `71` | `EPROTO` | unzulässiger Frame-Typ |
|
|
| `74` | `EBADMSG` | ungültiger Stream-Frame/CRC Fehler |
|
|
| `88` | `ENOSYS` | nicht implementiert |
|
|
| `90` | `EMSGSIZE` | max Payload ungültig |
|
|
| `116` | `ETIMEDOUT` | Credit-/Stream-Timeout |
|
|
| `134` | `ENOTSUP` | nicht unterstützt |
|
|
|
|
### SUCCESS (`0x13`)
|
|
|
|
Payload:
|
|
|
|
```c
|
|
uint8_t data_type; // erfolgreich abgeschlossener Befehl
|
|
```
|
|
|
|
Wird derzeit u.a. für `FILE_PUT`, `TAGS_PUT`, `RM_FILE`, `RENAME_FILE` genutzt.
|
|
|
|
## Verzeichnisliste (`LS`)
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Host
|
|
participant Device
|
|
|
|
Host->>Device: REQUEST(LS, path)
|
|
Device-->>Host: LS_START
|
|
Host->>Device: ACK(credits=64)
|
|
|
|
loop solange credits > 0
|
|
Device-->>Host: LS_ENTRY
|
|
end
|
|
|
|
alt Verzeichnis vollständig
|
|
Device-->>Host: LS_END(total_entries)
|
|
else keine Credits/Timeout
|
|
Device-->>Host: ERROR(ETIMEDOUT)
|
|
end
|
|
```
|
|
|
|
Hinweis zum aktuellen Web-Client (März 2026): Für `LS` wird initial `ACK(64)` gesendet, ein dynamisches Nachfüllen ist noch nicht implementiert. Große Verzeichnisse können deshalb in `ETIMEDOUT` laufen.
|
|
|
|
## Datei-/Tag-Download (`FILE_GET`, `TAGS_GET`)
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Host
|
|
participant Device
|
|
|
|
Host->>Device: REQUEST(FILE_GET|TAGS_GET, path)
|
|
Device-->>Host: FILE_START(total_size)
|
|
Host->>Device: ACK(credits=128)
|
|
|
|
loop chunks
|
|
Device-->>Host: FILE_CHUNK(data)
|
|
Note over Host: Credits dekrementieren
|
|
alt Credits <= 64
|
|
Host->>Device: ACK(credits=128)
|
|
end
|
|
end
|
|
|
|
Device-->>Host: FILE_END(crc32)
|
|
Note over Host: CRC prüfen
|
|
```
|
|
|
|
## Datei-/Tag-Upload (`FILE_PUT`, `TAGS_PUT`)
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Host
|
|
participant Device
|
|
|
|
Host->>Device: REQUEST(FILE_PUT|TAGS_PUT, total_size, path)
|
|
Device-->>Host: ACK(initial credits)
|
|
|
|
loop solange Host-Credits > 0
|
|
Host->>Device: FILE_CHUNK(data)
|
|
Note over Device: schreibt Flash, zählt unacked_chunks
|
|
alt ACK_WATERMARK erreicht
|
|
Device-->>Host: ACK(additional credits)
|
|
end
|
|
end
|
|
|
|
Host->>Device: FILE_END(crc32)
|
|
alt CRC korrekt
|
|
Device-->>Host: SUCCESS(FILE_PUT|TAGS_PUT)
|
|
else CRC/Write Fehler
|
|
Device-->>Host: ERROR(EBADMSG/EIO/...)
|
|
end
|
|
```
|
|
|
|
## Payload-Frames im Detail
|
|
|
|
### `LS_ENTRY` (`0x41`)
|
|
|
|
```c
|
|
uint8_t type; // 0x00 file, 0x01 dir
|
|
uint32_t size; // LE
|
|
uint8_t name_length;
|
|
char name[]; // ohne Nullterminierung
|
|
```
|
|
|
|
### `LS_END` (`0x42`)
|
|
|
|
```c
|
|
uint32_t total_entries; // LE
|
|
```
|
|
|
|
### `FILE_START` (`0x20`)
|
|
|
|
```c
|
|
uint32_t total_size; // LE
|
|
```
|
|
|
|
`FILE_GET`: komplette Dateigröße.
|
|
`TAGS_GET`: nur Tag-Teil der Datei.
|
|
|
|
### `FILE_CHUNK` (`0x21`)
|
|
|
|
Payload sind rohe Nutzdatenbytes.
|
|
|
|
### `FILE_END` (`0x22`)
|
|
|
|
```c
|
|
uint32_t crc32; // LE, IEEE CRC32
|
|
```
|
|
|
|
## Beispiel-Frames (Hex)
|
|
|
|
### `PROTO_INFO` Request/Response
|
|
|
|
Request:
|
|
|
|
```text
|
|
00 01 00 01
|
|
```
|
|
|
|
Beispiel-Response:
|
|
|
|
```text
|
|
10 05 00 01 01 00 FD 00
|
|
```
|
|
|
|
Interpretation:
|
|
- `10`: RESPONSE
|
|
- `05 00`: Payload 5 Byte
|
|
- `01`: PROTO_INFO
|
|
- `01 00`: Version 1
|
|
- `FD 00`: max_chunk_size = 253
|
|
|
|
### `LS` Request für `/a`
|
|
|
|
```text
|
|
00 03 00 40 2F 61
|
|
```
|
|
|
|
Interpretation:
|
|
- `00`: REQUEST
|
|
- `03 00`: Payload 3 Byte
|
|
- `40`: data_type LS
|
|
- `2F 61`: `/a`
|
|
|
|
### Implementierungsnotizen
|
|
|
|
- Unknown `REQUEST.data_type` wird aktuell mit `ERROR(EINVAL)` beantwortet.
|
|
- Unbekannte/unerwartete `frame_type` im aktiven Protokollthread führen zu `ERROR(EPROTO)`.
|
|
- Stream-Timeout in Firmware erzeugt aktiv `ERROR(ETIMEDOUT)`.
|
|
- Upload-Timeout im FS-Thread (2 s Inaktivität) bricht intern ab; die Host-Seite sollte eigene Watchdogs haben. |