sync
This commit is contained in:
303
firmware/Protokoll.md
Normal file
303
firmware/Protokoll.md
Normal file
@@ -0,0 +1,303 @@
|
||||
# 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.
|
||||
|
||||
133
firmware/Tags.md
Normal file
133
firmware/Tags.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# Audio-Tags Format
|
||||
|
||||
Dieses Dokument beschreibt das aktuelle Tag-Format für Audiodateien.
|
||||
|
||||
## 1) Position in der Datei
|
||||
|
||||
Die Tags stehen am Dateiende:
|
||||
|
||||
`[audio_data][metadata][tag_version_u8][footer_len_le16]["TAG!"]`
|
||||
|
||||
- `audio_data`: eigentliche Audiodaten
|
||||
- `metadata`: Folge von Tag-Einträgen
|
||||
- `tag_version_u8`: 1 Byte Versionsnummer des Tag-Formats
|
||||
- `footer_len_le16`: 2 Byte, Little Endian
|
||||
- `"TAG!"`: 4 Byte Magic (`0x54 0x41 0x47 0x21`)
|
||||
|
||||
## 2) Bedeutung von `footer_len_le16`
|
||||
|
||||
`footer_len_le16` ist die **Gesamtlänge des Footers**, also:
|
||||
|
||||
`footer_len = metadata_len + 1 + 2 + 4`
|
||||
|
||||
Damit beginnt `metadata` bei:
|
||||
|
||||
`metadata_start = file_size - footer_len`
|
||||
|
||||
Das passt zur aktuellen Implementierung in der Firmware.
|
||||
|
||||
### Tag-Version
|
||||
|
||||
- `tag_version` ist aktuell `0x01`.
|
||||
- Der Host darf nur bekannte Versionen interpretieren.
|
||||
- Bei unbekannter Version: Tag-Block ignorieren oder als "nicht unterstützt" melden.
|
||||
|
||||
## 3) Endianness und Typen
|
||||
|
||||
- Alle Multi-Byte-Werte sind **Little Endian**.
|
||||
- Tag-Einträge sind TLV-basiert:
|
||||
- `type`: `uint8_t`
|
||||
- `len`: `uint16_t`
|
||||
- `value`: `byte[len]`
|
||||
|
||||
Dadurch können auch unbekannte Typen sauber übersprungen werden.
|
||||
|
||||
## 4) Unterstützte Tag-Typen
|
||||
|
||||
Aktuell definierte Typen:
|
||||
|
||||
- `0x00`: `DESCRIPTION` (Beschreibung des Samples)
|
||||
- `0x01`: `AUTHOR`
|
||||
- `0x10`: `CRC32_RAW`
|
||||
- `0x20`: `FILE_FORMAT` (Info für Host, Player wertet derzeit nicht aus)
|
||||
|
||||
## 5) Value-Format pro Tag
|
||||
|
||||
### 5.1 `0x00` DESCRIPTION
|
||||
|
||||
- `value`: UTF-8-Text
|
||||
- `len`: Anzahl Bytes des UTF-8-Texts
|
||||
|
||||
### 5.2 `0x01` AUTHOR
|
||||
|
||||
- `value`: UTF-8-Text
|
||||
- `len`: Anzahl Bytes des UTF-8-Texts
|
||||
|
||||
### 5.3 `0x10` CRC32_RAW
|
||||
|
||||
- `value`: `uint32_t crc32` (4 Byte, Little Endian)
|
||||
- `len`: **muss 4** sein
|
||||
|
||||
### 5.4 `0x20` FILE_FORMAT
|
||||
|
||||
- `value`:
|
||||
- `bits_per_sample`: `uint8_t`
|
||||
- `sample_rate`: `uint32_t` (Little Endian)
|
||||
- `len`: **muss 5** sein
|
||||
|
||||
Beispielwerte aktuell oft: `bits_per_sample = 16`, `sample_rate = 16000`.
|
||||
|
||||
## 6) Vorkommen je Typ
|
||||
|
||||
Aktueller Stand: **jeder Tag-Typ darf maximal 1x vorkommen**.
|
||||
|
||||
Empfohlene Host-Regel:
|
||||
|
||||
- Falls ein Typ mehrfach vorkommt, letzte Instanz gewinnt (`last-wins`) und ein Warnhinweis wird geloggt.
|
||||
|
||||
## 7) Validierungsregeln (Host)
|
||||
|
||||
Beim Lesen:
|
||||
|
||||
1. Prüfen, ob Datei mindestens 7 Byte hat.
|
||||
2. Letzte 6 Byte prüfen: `footer_len_le16` + `TAG!`.
|
||||
3. `footer_len` gegen Dateigröße validieren (`6 <= footer_len <= file_size`).
|
||||
4. `tag_version` an Position `file_size - 6 - 1` lesen und validieren.
|
||||
5. Im Metadatenbereich TLV-Einträge lesen, bis Ende erreicht.
|
||||
6. Für bekannte Typen feste Längen prüfen (`CRC32_RAW=4`, `FILE_FORMAT=5`).
|
||||
7. Unbekannte Typen über `len` überspringen.
|
||||
|
||||
Beim Schreiben:
|
||||
|
||||
1. Vorhandene Tags entfernen/ersetzen (audio-Ende bestimmen).
|
||||
2. Neue TLV-Metadaten schreiben.
|
||||
3. `tag_version_u8` schreiben (`0x01`).
|
||||
4. `footer_len_le16` schreiben (inkl. 1+2+4).
|
||||
5. `TAG!` schreiben.
|
||||
5. Datei auf neue Länge truncaten.
|
||||
|
||||
## 8) Beispiel (hex)
|
||||
|
||||
Beispiel mit:
|
||||
|
||||
- DESCRIPTION = "Kick"
|
||||
- AUTHOR = "Edi"
|
||||
- CRC32_RAW = `0x12345678`
|
||||
|
||||
TLV-Daten:
|
||||
|
||||
- `00 04 00 4B 69 63 6B`
|
||||
- `01 03 00 45 64 69`
|
||||
- `10 04 00 78 56 34 12`
|
||||
|
||||
`metadata_len = 7 + 6 + 7 = 20 (0x0014)`
|
||||
|
||||
`footer_len = 20 + 1 + 2 + 4 = 27 (0x001B)`
|
||||
|
||||
Footer-Ende:
|
||||
|
||||
- `01 1B 00 54 41 47 21`
|
||||
|
||||
## 9) Hinweis zur aktuellen Firmware
|
||||
|
||||
Die Firmware verarbeitet Tag-Payload direkt binär (Chunk-Streaming über das Protokoll). Das dateiinterne Format entspricht direkt diesem Dokument.
|
||||
@@ -1,5 +1,9 @@
|
||||
VERSION_MAJOR = 0
|
||||
VERSION_MINOR = 1
|
||||
PATCHLEVEL = 14
|
||||
VERSION_MINOR = 2
|
||||
PATCHLEVEL = 19
|
||||
VERSION_TWEAK = 0
|
||||
EXTRAVERSION = debug
|
||||
#if (IS_ENABLED(CONFIG_LOG))
|
||||
EXTRAVERSION = debug
|
||||
#else
|
||||
EXTRAVERSION = 0
|
||||
#endif
|
||||
@@ -29,6 +29,7 @@ CONFIG_USB_DEVICE_PRODUCT="Edi's Buzzer"
|
||||
CONFIG_USB_DEVICE_PID=0x0001
|
||||
CONFIG_USB_DRIVER_LOG_LEVEL_ERR=y
|
||||
CONFIG_USB_DEVICE_LOG_LEVEL_ERR=y
|
||||
CONFIG_USB_DEVICE_LOG_LEVEL_OFF=y
|
||||
CONFIG_USB_DEVICE_INITIALIZE_AT_BOOT=n
|
||||
CONFIG_USB_DEVICE_STACK_NEXT=n
|
||||
|
||||
@@ -47,13 +48,14 @@ CONFIG_FLASH_MAP=y
|
||||
CONFIG_STREAM_FLASH=y
|
||||
CONFIG_IMG_MANAGER=y
|
||||
CONFIG_MCUBOOT_IMG_MANAGER=y
|
||||
CONFIG_MCUBOOT_UTIL_LOG_LEVEL_ERR=y
|
||||
|
||||
# --- HWINFO und CRC ---
|
||||
CONFIG_HWINFO=y
|
||||
CONFIG_CRC=y
|
||||
|
||||
# --- Debugging & Sicherheit ---
|
||||
CONFIG_ASSERT=y
|
||||
CONFIG_ASSERT=n
|
||||
CONFIG_HW_STACK_PROTECTION=y
|
||||
CONFIG_RESET_ON_FATAL_ERROR=y
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
#define AUDIO_WORD_WIDTH 16
|
||||
#define AUDIO_SAMPLE_RATE 16000
|
||||
|
||||
LOG_MODULE_REGISTER(audio, LOG_LEVEL_DBG);
|
||||
LOG_MODULE_REGISTER(audio, LOG_LEVEL_INF);
|
||||
|
||||
/* Dauer eines Blocks in ms (4096 Bytes / (16kHz * 2 Kanäle * 2 Bytes)) = 64 ms */
|
||||
#define BLOCK_DURATION_MS ((AUDIO_BLOCK_SIZE * 1000) / (AUDIO_SAMPLE_RATE * 2 * (AUDIO_WORD_WIDTH / 8)))
|
||||
|
||||
@@ -5,9 +5,15 @@
|
||||
#include <zephyr/pm/device.h>
|
||||
|
||||
#include <fs.h>
|
||||
#include <utils.h>
|
||||
|
||||
LOG_MODULE_REGISTER(buzz_fs, LOG_LEVEL_DBG);
|
||||
LOG_MODULE_REGISTER(buzz_fs, LOG_LEVEL_INF);
|
||||
|
||||
#define TAG_MAGIC "TAG!"
|
||||
#define TAG_MAGIC_LEN 4U
|
||||
#define TAG_LEN_FIELD_LEN 2U
|
||||
#define TAG_VERSION_LEN 1U
|
||||
#define TAG_FOOTER_V1_LEN (TAG_VERSION_LEN + TAG_LEN_FIELD_LEN + TAG_MAGIC_LEN)
|
||||
#define TAG_FORMAT_VERSION 0x01
|
||||
|
||||
#define STORAGE_PARTITION_ID FIXED_PARTITION_ID(littlefs_storage)
|
||||
#define SLOT1_ID FIXED_PARTITION_ID(slot1_partition)
|
||||
@@ -162,6 +168,15 @@ int fs_pm_statvfs(const char *path, struct fs_statvfs *stat)
|
||||
return rc;
|
||||
}
|
||||
|
||||
int fs_pm_stat(const char *path, struct fs_dirent *entry)
|
||||
{
|
||||
LOG_DBG("PM Getting stat for '%s'", path);
|
||||
fs_pm_flash_resume();
|
||||
int rc = fs_stat(path, entry);
|
||||
fs_pm_flash_suspend();
|
||||
return rc;
|
||||
}
|
||||
|
||||
int fs_pm_mkdir(const char *path)
|
||||
{
|
||||
LOG_DBG("PM Creating directory '%s'", path);
|
||||
@@ -171,28 +186,91 @@ int fs_pm_mkdir(const char *path)
|
||||
return rc;
|
||||
}
|
||||
|
||||
ssize_t fs_get_audio_data_len(struct fs_file_t *fp) {
|
||||
int fs_pm_rename(const char *old_path, const char *new_path)
|
||||
{
|
||||
LOG_DBG("PM Renaming '%s' to '%s'", old_path, new_path);
|
||||
fs_pm_flash_resume();
|
||||
int rc = fs_rename(old_path, new_path);
|
||||
fs_pm_flash_suspend();
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int fs_get_tag_bounds(struct fs_file_t *fp, off_t file_size,
|
||||
size_t *audio_limit, size_t *payload_len, bool *has_tag)
|
||||
{
|
||||
uint8_t footer[6];
|
||||
uint16_t tag_len;
|
||||
|
||||
if (audio_limit == NULL || payload_len == NULL || has_tag == NULL) {
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
*has_tag = false;
|
||||
*audio_limit = (size_t)file_size;
|
||||
*payload_len = 0U;
|
||||
|
||||
if (file_size < (off_t)TAG_FOOTER_V1_LEN) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
fs_seek(fp, -(off_t)(TAG_LEN_FIELD_LEN + TAG_MAGIC_LEN), FS_SEEK_END);
|
||||
if (fs_read(fp, footer, sizeof(footer)) != sizeof(footer)) {
|
||||
fs_seek(fp, 0, FS_SEEK_SET);
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
if (memcmp(&footer[2], TAG_MAGIC, TAG_MAGIC_LEN) != 0) {
|
||||
fs_seek(fp, 0, FS_SEEK_SET);
|
||||
return 0;
|
||||
}
|
||||
|
||||
tag_len = (uint16_t)footer[0] | ((uint16_t)footer[1] << 8);
|
||||
if (tag_len > (uint16_t)file_size || tag_len < TAG_FOOTER_V1_LEN) {
|
||||
fs_seek(fp, 0, FS_SEEK_SET);
|
||||
return -EBADMSG;
|
||||
}
|
||||
|
||||
uint8_t tag_version = 0;
|
||||
fs_seek(fp, -(off_t)TAG_FOOTER_V1_LEN, FS_SEEK_END);
|
||||
if (fs_read(fp, &tag_version, 1) != 1) {
|
||||
fs_seek(fp, 0, FS_SEEK_SET);
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
if (tag_version != TAG_FORMAT_VERSION) {
|
||||
fs_seek(fp, 0, FS_SEEK_SET);
|
||||
return -ENOTSUP;
|
||||
}
|
||||
|
||||
*has_tag = true;
|
||||
*audio_limit = (size_t)file_size - tag_len;
|
||||
*payload_len = tag_len - TAG_FOOTER_V1_LEN;
|
||||
|
||||
fs_seek(fp, 0, FS_SEEK_SET);
|
||||
return 0;
|
||||
}
|
||||
|
||||
ssize_t fs_get_audio_data_len(struct fs_file_t *fp) {
|
||||
off_t file_size;
|
||||
size_t audio_limit = 0U;
|
||||
size_t payload_len = 0U;
|
||||
bool has_tag = false;
|
||||
|
||||
fs_seek(fp, 0, FS_SEEK_END);
|
||||
file_size = fs_tell(fp);
|
||||
|
||||
fs_seek(fp, 0, FS_SEEK_SET);
|
||||
if (file_size < 6) return file_size;
|
||||
|
||||
fs_seek(fp, -6, FS_SEEK_END);
|
||||
if (fs_read(fp, footer, 6) == 6) {
|
||||
if (memcmp(&footer[2], "TAG!", 4) == 0) {
|
||||
uint16_t tag_len = footer[0] | (footer[1] << 8);
|
||||
if (tag_len <= file_size) {
|
||||
fs_seek(fp, 0, FS_SEEK_SET);
|
||||
return file_size - tag_len;
|
||||
}
|
||||
}
|
||||
if (file_size < 0) {
|
||||
fs_seek(fp, 0, FS_SEEK_SET);
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
if (fs_get_tag_bounds(fp, file_size, &audio_limit, &payload_len, &has_tag) < 0) {
|
||||
fs_seek(fp, 0, FS_SEEK_SET);
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
fs_seek(fp, 0, FS_SEEK_SET);
|
||||
return file_size;
|
||||
return has_tag ? (ssize_t)audio_limit : file_size;
|
||||
}
|
||||
|
||||
ssize_t fs_read_audio(struct fs_file_t *fp, void *buffer, size_t len, size_t audio_limit) {
|
||||
@@ -206,88 +284,85 @@ ssize_t fs_read_audio(struct fs_file_t *fp, void *buffer, size_t len, size_t aud
|
||||
return fs_read(fp, buffer, to_read);
|
||||
}
|
||||
|
||||
int fs_write_hex_tag(struct fs_file_t *fp, const char *hex_str) {
|
||||
size_t hex_len = strlen(hex_str);
|
||||
|
||||
// Ein Hex-String muss eine gerade Anzahl an Zeichen haben
|
||||
if (hex_len % 2 != 0) return -EINVAL;
|
||||
|
||||
size_t payload_len = hex_len / 2;
|
||||
uint16_t total_footer_len = (uint16_t)(payload_len + 6);
|
||||
|
||||
// 1. Audio-Ende bestimmen und dorthin seeken
|
||||
size_t audio_limit = fs_get_audio_data_len(fp);
|
||||
fs_seek(fp, audio_limit, FS_SEEK_SET);
|
||||
|
||||
// 2. Payload Byte für Byte konvertieren und schreiben
|
||||
for (size_t i = 0; i < hex_len; i += 2) {
|
||||
int high = hex2int(hex_str[i]);
|
||||
int low = hex2int(hex_str[i+1]);
|
||||
|
||||
if (high < 0 || low < 0) return -EINVAL; // Ungültiges Hex-Zeichen
|
||||
|
||||
uint8_t byte = (uint8_t)((high << 4) | low);
|
||||
fs_write(fp, &byte, 1);
|
||||
}
|
||||
|
||||
// 3. Die 2 Bytes Länge schreiben (Little Endian)
|
||||
uint8_t len_bytes[2];
|
||||
len_bytes[0] = (uint8_t)(total_footer_len & 0xFF);
|
||||
len_bytes[1] = (uint8_t)((total_footer_len >> 8) & 0xFF);
|
||||
fs_write(fp, len_bytes, 2);
|
||||
|
||||
// 4. Magic Bytes schreiben
|
||||
fs_write(fp, "TAG!", 4);
|
||||
|
||||
// 5. Datei am aktuellen Punkt abschneiden
|
||||
off_t current_pos = fs_tell(fp);
|
||||
return fs_truncate(fp, current_pos);
|
||||
}
|
||||
|
||||
int fs_read_hex_tag(struct fs_file_t *fp, char *hex_str, size_t hex_str_size) {
|
||||
if (hex_str == NULL || hex_str_size == 0) {
|
||||
int fs_tag_open_read(struct fs_file_t *fp, uint8_t *version, size_t *payload_len)
|
||||
{
|
||||
if (fp == NULL || version == NULL || payload_len == NULL) {
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
hex_str[0] = '\0';
|
||||
|
||||
// Dateigröße ermitteln
|
||||
fs_seek(fp, 0, FS_SEEK_END);
|
||||
off_t file_size = fs_tell(fp);
|
||||
size_t audio_limit = 0U;
|
||||
size_t payload_size = 0U;
|
||||
bool has_tag = false;
|
||||
|
||||
// Audio-Limit finden (Anfang des Payloads)
|
||||
size_t audio_limit = fs_get_audio_data_len(fp);
|
||||
|
||||
// Prüfen, ob überhaupt ein Tag existiert (audio_limit < file_size)
|
||||
if (audio_limit >= file_size) {
|
||||
// Kein Tag vorhanden -> leerer String
|
||||
return 0;
|
||||
if (file_size < 0) {
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
// Die Payload-Länge ist: Gesamtgröße - Audio - 6 Bytes (Länge + Magic)
|
||||
size_t payload_len = file_size - audio_limit - 6;
|
||||
|
||||
if ((payload_len * 2U) + 1U > hex_str_size) {
|
||||
return -ENOMEM; // Nicht genug Platz im Zielpuffer
|
||||
int rc = fs_get_tag_bounds(fp, file_size, &audio_limit, &payload_size, &has_tag);
|
||||
if (rc < 0) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
if (!has_tag) {
|
||||
return -ENOENT;
|
||||
}
|
||||
|
||||
// Zum Anfang des Payloads springen
|
||||
fs_seek(fp, audio_limit, FS_SEEK_SET);
|
||||
*version = TAG_FORMAT_VERSION;
|
||||
*payload_len = payload_size;
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint8_t byte;
|
||||
for (size_t i = 0; i < payload_len; i++) {
|
||||
if (fs_read(fp, &byte, 1) != 1) {
|
||||
return -EIO;
|
||||
}
|
||||
ssize_t fs_tag_read_chunk(struct fs_file_t *fp, void *buffer, size_t len)
|
||||
{
|
||||
return fs_read(fp, buffer, len);
|
||||
}
|
||||
|
||||
// Jedes Byte als zwei Hex-Zeichen in den Zielpuffer schreiben
|
||||
hex_str[i * 2] = int2hex(byte >> 4);
|
||||
hex_str[i * 2 + 1] = int2hex(byte & 0x0F);
|
||||
int fs_tag_open_write(struct fs_file_t *fp)
|
||||
{
|
||||
ssize_t audio_limit = fs_get_audio_data_len(fp);
|
||||
if (audio_limit < 0) {
|
||||
return (int)audio_limit;
|
||||
}
|
||||
fs_seek(fp, audio_limit, FS_SEEK_SET);
|
||||
return 0;
|
||||
}
|
||||
|
||||
ssize_t fs_tag_write_chunk(struct fs_file_t *fp, const void *buffer, size_t len)
|
||||
{
|
||||
return fs_write(fp, buffer, len);
|
||||
}
|
||||
|
||||
int fs_tag_finish_write(struct fs_file_t *fp, uint8_t version, size_t payload_len)
|
||||
{
|
||||
if (version != TAG_FORMAT_VERSION) {
|
||||
return -ENOTSUP;
|
||||
}
|
||||
|
||||
hex_str[payload_len * 2] = '\0';
|
||||
size_t total_footer_len = payload_len + TAG_FOOTER_V1_LEN;
|
||||
if (total_footer_len > UINT16_MAX) {
|
||||
return -EFBIG;
|
||||
}
|
||||
|
||||
return 0;
|
||||
if (fs_write(fp, &version, 1) != 1) {
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
uint8_t len_bytes[2];
|
||||
len_bytes[0] = (uint8_t)(total_footer_len & 0xFFU);
|
||||
len_bytes[1] = (uint8_t)((total_footer_len >> 8) & 0xFFU);
|
||||
if (fs_write(fp, len_bytes, sizeof(len_bytes)) != sizeof(len_bytes)) {
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
if (fs_write(fp, TAG_MAGIC, TAG_MAGIC_LEN) != TAG_MAGIC_LEN) {
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
off_t current_pos = fs_tell(fp);
|
||||
return fs_truncate(fp, current_pos);
|
||||
}
|
||||
|
||||
int flash_get_slot_info(slot_info_t *info) {
|
||||
|
||||
@@ -74,6 +74,15 @@ int fs_pm_unlink(const char *path);
|
||||
*/
|
||||
int fs_pm_statvfs(const char *path, struct fs_statvfs *stat);
|
||||
|
||||
/**
|
||||
* @brief Wrapper around fs_stat that handles power management for the flash
|
||||
* Resumes the flash before stat and suspends it afterwards
|
||||
* @param path Path to file or directory
|
||||
* @param entry Pointer to fs_dirent structure to receive metadata
|
||||
* @return 0 on success, negative error code on failure
|
||||
*/
|
||||
int fs_pm_stat(const char *path, struct fs_dirent *entry);
|
||||
|
||||
/**
|
||||
* @brief Wrapper around fs_mkdir that handles power management for the flash
|
||||
* Resumes the flash before creating the directory and suspends it afterwards
|
||||
@@ -82,6 +91,15 @@ int fs_pm_statvfs(const char *path, struct fs_statvfs *stat);
|
||||
*/
|
||||
int fs_pm_mkdir(const char *path);
|
||||
|
||||
/**
|
||||
* @brief Wrapper around fs_rename that handles power management for the flash
|
||||
* Resumes the flash before renaming and suspends it afterwards
|
||||
* @param old_path Current path of the file or directory
|
||||
* @param new_path New path for the file or directory
|
||||
* @return 0 on success, negative error code on failure
|
||||
*/
|
||||
int fs_pm_rename(const char *old_path, const char *new_path);
|
||||
|
||||
/**
|
||||
* @brief Gets the length of the audio data in a file, accounting for any metadata tags
|
||||
* @param fp Pointer to an open fs_file_t structure representing the audio file
|
||||
@@ -100,21 +118,47 @@ int fs_get_audio_data_len(struct fs_file_t *fp);
|
||||
int fs_read_audio(struct fs_file_t *fp, void *buffer, size_t len, size_t audio_limit);
|
||||
|
||||
/**
|
||||
* @brief Writes a hexadecimal string as a metadata tag at the end of an audio file
|
||||
* @brief Positions file pointer at start of tag payload if tags exist.
|
||||
* @param fp Pointer to an open fs_file_t structure representing the audio file
|
||||
* @param hex_str Null-terminated string containing hexadecimal characters (0-9, a-f, A-F)
|
||||
* @return 0 on success, negative error code on failure
|
||||
* @param version Pointer to receive tag format version
|
||||
* @param payload_len Pointer to receive tag payload length in bytes
|
||||
* @return 0 on success, -ENOENT if no tags exist, negative error code on failure
|
||||
*/
|
||||
int fs_write_hex_tag(struct fs_file_t *fp, const char *hex_str);
|
||||
int fs_tag_open_read(struct fs_file_t *fp, uint8_t *version, size_t *payload_len);
|
||||
|
||||
/**
|
||||
* @brief Reads a hexadecimal string from a metadata tag at the end of an audio file
|
||||
* @brief Reads a chunk from current tag payload position.
|
||||
* @param fp Pointer to an open fs_file_t positioned in tag payload
|
||||
* @param buffer Destination buffer
|
||||
* @param len Maximum bytes to read
|
||||
* @return Number of bytes read, 0 at payload end, negative error code on failure
|
||||
*/
|
||||
ssize_t fs_tag_read_chunk(struct fs_file_t *fp, void *buffer, size_t len);
|
||||
|
||||
/**
|
||||
* @brief Positions file pointer for tag payload overwrite at end of audio data.
|
||||
* @param fp Pointer to an open fs_file_t structure representing the audio file
|
||||
* @param hex_str Buffer to be filled with the hexadecimal string (must be large enough to hold the data)
|
||||
* @param hex_str_size Size of the hex_str buffer
|
||||
* @return 0 on success, negative error code on failure
|
||||
*/
|
||||
int fs_read_hex_tag(struct fs_file_t *fp, char *hex_str, size_t hex_str_size);
|
||||
int fs_tag_open_write(struct fs_file_t *fp);
|
||||
|
||||
/**
|
||||
* @brief Writes a raw tag payload chunk.
|
||||
* @param fp Pointer to an open fs_file_t positioned for tag payload write
|
||||
* @param buffer Source buffer
|
||||
* @param len Number of bytes to write
|
||||
* @return Number of bytes written, negative error code on failure
|
||||
*/
|
||||
ssize_t fs_tag_write_chunk(struct fs_file_t *fp, const void *buffer, size_t len);
|
||||
|
||||
/**
|
||||
* @brief Finalizes tags by appending version + footer and truncating file.
|
||||
* @param fp Pointer to an open fs_file_t structure representing the audio file
|
||||
* @param version Tag format version to write
|
||||
* @param payload_len Tag payload length in bytes
|
||||
* @return 0 on success, negative error code on failure
|
||||
*/
|
||||
int fs_tag_finish_write(struct fs_file_t *fp, uint8_t version, size_t payload_len);
|
||||
|
||||
/**
|
||||
* @brief Retrieves information about the firmware slot, such as start address and size
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,29 +1,53 @@
|
||||
#ifndef PROTOCOL_H
|
||||
#define PROTOCOL_H
|
||||
|
||||
#include <zephyr/kernel.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define PROTOCOL_MAX_PATH_LEN 32U
|
||||
|
||||
typedef enum {
|
||||
PS_WAITING_FOR_COMMAND,
|
||||
PS_READING_COMMAND,
|
||||
PS_READING_PARAMETERS,
|
||||
PS_WAITING_FOR_END_OF_LINE,
|
||||
PS_WAIT_SYNC = 0,
|
||||
PS_READ_HEADER,
|
||||
PS_READ_PAYLOAD,
|
||||
PS_READ_PAYLOAD_CRC,
|
||||
} protocol_state_t;
|
||||
|
||||
typedef enum {
|
||||
CMD_INVALID = 0,
|
||||
CMD_INFO,
|
||||
CMD_LS,
|
||||
CMD_PUT_BINARY_FILE,
|
||||
CMD_MKDIR,
|
||||
CMD_RM,
|
||||
CMD_CONFIRM,
|
||||
CMD_REBOOT,
|
||||
CMD_PLAY,
|
||||
CMD_SET_TAG,
|
||||
CMD_GET_TAG,
|
||||
CMD_CHECK,
|
||||
/* Weitere Kommandos folgen hier */
|
||||
CMD_GET_PROTOCOL_VERSION = 0x00,
|
||||
CMD_GET_FIRMWARE_STATUS = 0x01,
|
||||
CMD_GET_FLASH_STATUS = 0x02,
|
||||
CMD_CONFIRM_FIRMWARE = 0x03,
|
||||
CMD_REBOOT = 0x04,
|
||||
CMD_LIST_DIR = 0x10,
|
||||
CMD_CHECK_FILE_CRC = 0x11,
|
||||
CMD_MKDIR = 0x12,
|
||||
CMD_RM = 0x13,
|
||||
CMD_PUT_FILE_START = 0x14,
|
||||
CMD_PUT_FILE_CHUNK = 0x15,
|
||||
CMD_PUT_FILE_END = 0x16,
|
||||
CMD_PUT_FW_START = 0x17,
|
||||
CMD_STAT = 0x18,
|
||||
CMD_RENAME = 0x19,
|
||||
CMD_RM_R = 0x1A,
|
||||
CMD_GET_FILE = 0x1B,
|
||||
CMD_GET_TAG_BLOB = 0x20,
|
||||
CMD_SET_TAG_BLOB_START = 0x21,
|
||||
CMD_SET_TAG_BLOB_CHUNK = 0x22,
|
||||
CMD_SET_TAG_BLOB_END = 0x23,
|
||||
} protocol_cmd_t;
|
||||
|
||||
typedef enum {
|
||||
FRAME_REQ = 0x01,
|
||||
FRAME_RESP_ACK = 0x10,
|
||||
FRAME_RESP_DATA = 0x11,
|
||||
FRAME_RESP_STREAM_START = 0x12,
|
||||
FRAME_RESP_STREAM_CHUNK = 0x13,
|
||||
FRAME_RESP_STREAM_END = 0x14,
|
||||
FRAME_RESP_ERROR = 0x7F,
|
||||
} protocol_frame_type_t;
|
||||
|
||||
typedef enum {
|
||||
P_ERR_NONE = 0x00,
|
||||
P_ERR_INVALID_COMMAND = 0x01,
|
||||
@@ -47,4 +71,7 @@ typedef enum {
|
||||
P_ERR_BUSY = 0x31,
|
||||
P_ERR_INTERNAL = 0x32,
|
||||
} protocol_error_t;
|
||||
|
||||
void protocol_thread_entry(void *p1, void *p2, void *p3);
|
||||
|
||||
#endif // PROTOCOL_H
|
||||
@@ -51,15 +51,3 @@ uint8_t get_reboot_status(void)
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
int hex2int(char c) {
|
||||
if (c >= '0' && c <= '9') return c - '0';
|
||||
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
||||
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
||||
return -1; // Fehlerhaftes Zeichen
|
||||
}
|
||||
|
||||
char int2hex(uint8_t i) {
|
||||
if (i < 10) return '0' + i;
|
||||
return 'a' + (i - 10);
|
||||
}
|
||||
@@ -22,18 +22,4 @@ void reboot_with_status(uint8_t status_code);
|
||||
*/
|
||||
uint8_t get_reboot_status();
|
||||
|
||||
/**
|
||||
* @brief Converts a hexadecimal character to its integer value.
|
||||
* @param c The hexadecimal character (0-9, a-f, A-F) to convert.
|
||||
* @return The integer value of the hexadecimal character, or -1 if the character is not a valid hexadecimal digit.
|
||||
*/
|
||||
int hex2int(char c);
|
||||
|
||||
/**
|
||||
* @brief Converts an integer value to its hexadecimal character representation.
|
||||
* @param i The integer value to convert (0-15).
|
||||
* @return The hexadecimal character representation of the integer value.
|
||||
*/
|
||||
char int2hex(uint8_t i);
|
||||
|
||||
#endif // UTILS_H
|
||||
|
||||
Reference in New Issue
Block a user