diff --git a/firmware/CMakeLists.txt b/firmware/CMakeLists.txt index 15fe1ba..d2ef485 100644 --- a/firmware/CMakeLists.txt +++ b/firmware/CMakeLists.txt @@ -9,9 +9,12 @@ target_sources(app PRIVATE src/io.c src/audio.c src/usb.c + src/uart.c src/protocol.c src/utils.c + src/settings.c ) -zephyr_include_directories(src) + +zephyr_include_directories(include) diff --git a/firmware/Tags.md b/firmware/Tags.md index d445b21..61f2e84 100644 --- a/firmware/Tags.md +++ b/firmware/Tags.md @@ -1,133 +1,122 @@ -# Audio-Tags Format +# Edi's Buzzer - Metadata Tags Format -Dieses Dokument beschreibt das aktuelle Tag-Format für Audiodateien. +## Architektur-Übersicht +Die Metadaten werden transparent an das Ende der rohen Audio-Daten angehängt. Das Format basiert auf einer strikten **Little-Endian** Byte-Reihenfolge und nutzt eine erweiterbare **TLV-Struktur** (Type-Length-Value) für die eigentlichen Datenblöcke. -## 1) Position in der Datei +Das physische Layout einer Datei im Flash-Speicher sieht wie folgt aus: +`[Audio-Rohdaten] [TLV-Block 1] ... [TLV-Block N] [Footer (8 Bytes)]` -Die Tags stehen am Dateiende: +--- -`[audio_data][metadata][tag_version_u8][footer_len_le16]["TAG!"]` +## 1. Footer-Struktur +Der Footer liegt exakt auf den letzten 8 Bytes der Datei (EOF - 8). Er dient als Ankerpunkt für den Parser, um die Metadaten rückwärts aus der Datei zu extrahieren. Das 8-Byte-Alignment stellt speichersichere Casts auf 32-Bit-ARM-Architekturen sicher. -- `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`) +| Offset | Feld | Typ | Beschreibung | +| :--- | :--- | :--- | :--- | +| 0 | `total_size` | `uint16_t` | Gesamtgröße in Bytes (Summe aller TLV-Blöcke + 8 Bytes Footer). | +| 2 | `version` | `uint16_t` | Format-Version. Aktuell `0x0001`. | +| 4 | `magic` | `char[4]` | Fixe Signatur: `"TAG!"` (Hex: `54 41 47 21`). | -## 2) Bedeutung von `footer_len_le16` +--- -`footer_len_le16` ist die **Gesamtlänge des Footers**, also: +## 2. TLV-Header (Type-Length-Value) +Jeder Metadaten-Block beginnt mit einem exakt 4 Bytes großen Header. Unbekannte Typen können vom Controller durch einen relativen Sprung (`fs_seek` um `length` Bytes) übersprungen werden. -`footer_len = metadata_len + 1 + 2 + 4` +| Offset | Feld | Typ | Beschreibung | +| :--- | :--- | :--- | :--- | +| 0 | `type` | `uint8_t` | Definiert den Inhalt des Blocks (siehe Typen-Definitionen). | +| 1 | `index` | `uint8_t` | Erlaubt die Fragmentierung großer Datensätze (z.B. bei JSON > 64 KB). Standard: `0x00`. | +| 2 | `length` | `uint16_t` | Größe der folgenden Payload in Bytes (ohne diesen Header). | -Damit beginnt `metadata` bei: +--- -`metadata_start = file_size - footer_len` +## 3. Typen-Definitionen -Das passt zur aktuellen Implementierung in der Firmware. +### Type `0x00`: Binary System Metadata +Dieser Typ gruppiert maschinenlesbare, binäre Systeminformationen. Die Unterscheidung erfolgt über das `Index`-Feld. -### Tag-Version +#### Index `0x00`: Audio Format +Dieser Block konfiguriert den I2S-Treiber vor der Wiedergabe. +* **Typ:** `0x00` +* **Index:** `0x00` +* **Länge:** `0x0008` (8 Bytes) +* **Payload:** `[codec: 1 Byte] [bit_depth: 1 Byte] [reserved: 2 Bytes] [samplerate: 4 Bytes]` -- `tag_version` ist aktuell `0x01`. -- Der Host darf nur bekannte Versionen interpretieren. -- Bei unbekannter Version: Tag-Block ignorieren oder als "nicht unterstützt" melden. +#### Index `0x01`: Audio CRC32 +Speichert die CRC32-Prüfsumme (IEEE) der reinen Audiodaten (vom Dateianfang bis zum Beginn des ersten TLV-Blocks). Dient Synchronisations-Tools für einen schnellen Integritäts- und Abgleich-Check, ohne die gesamte Datei neu hashen zu müssen. +* **Typ:** `0x00` +* **Index:** `0x01` +* **Länge:** `0x0004` (4 Bytes) +* **Payload:** `uint32_t` (Little-Endian) -## 3) Endianness und Typen +### Type `0x10`: JSON Metadata +Dieser Block enthält Metadaten, die primär für das Host-System (z. B. das Python-Tool) zur Verwaltung, Kategorisierung und Anzeige bestimmt sind. Der Mikrocontroller ignoriert und überspringt diesen Block während der Audiowiedergabe. -- Alle Multi-Byte-Werte sind **Little Endian**. -- Tag-Einträge sind TLV-basiert: - - `type`: `uint8_t` - - `len`: `uint16_t` - - `value`: `byte[len]` +* **Typ:** `0x10` +* **Länge:** Variabel +* **Payload:** UTF-8-kodierter JSON-String (ohne Null-Terminator). -Dadurch können auch unbekannte Typen sauber übersprungen werden. +#### Standardisierte JSON-Schlüssel +Die nachfolgenden Schlüssel (Keys) sind im Basis-Standard definiert. Die Integration weiterer, proprietärer Schlüssel ist technisch möglich. Es wird jedoch empfohlen, dies mit Vorsicht zu handhaben, da zukünftige Standardisierungen diese Schlüsselnamen belegen könnten (Namenskollision). -## 4) Unterstützte Tag-Typen +| Schlüssel | Datentyp | Beschreibung | +| :--- | :--- | :--- | +| `t` | String | Titel der Audiodatei | +| `a` | String | Autor oder Ersteller | +| `r` | String | Bemerkungen (Remarks) oder Beschreibung | +| `c` | Array of Strings | Kategorien zur Gruppierung | +| `dc` | String | Erstellungsdatum (Date Created), idealerweise nach ISO 8601 | +| `ds` | String | Speicher- oder Änderungsdatum (Date Saved), idealerweise nach ISO 8601 | -Aktuell definierte Typen: +**Beispiel-Payload:** +Ein vollständiger JSON-Datensatz gemäß dieser Spezifikation hat folgendes Format: -- `0x00`: `DESCRIPTION` (Beschreibung des Samples) -- `0x01`: `AUTHOR` -- `0x10`: `CRC32_RAW` -- `0x20`: `FILE_FORMAT` (Info für Host, Player wertet derzeit nicht aus) +```json +{ + "t": "Testaufnahme System A", + "a": "Entwickler-Team", + "r": "Überprüfung der Mikrofon-Aussteuerung.", + "c": ["Test", "Audio", "V1"], + "dc": "2026-03-05T13:00:00Z", + "ds": "2026-03-05T13:10:00Z" +} +``` -## 5) Value-Format pro Tag +*(Hinweis zur Skalierbarkeit: Für zukünftige Erweiterungen können dedizierte TLV-Typen definiert werden, wie beispielsweise 0x11 für GZIP-komprimierte JSON-Daten oder 0x20 für binäre Bilddaten wie PNG-Cover).* -### 5.1 `0x00` DESCRIPTION +--- -- `value`: UTF-8-Text -- `len`: Anzahl Bytes des UTF-8-Texts +## 4. Lese-Algorithmus (Parser-Logik) -### 5.2 `0x01` AUTHOR +Der Controller extrahiert die Hardware-Parameter nach folgendem Ablauf: -- `value`: UTF-8-Text -- `len`: Anzahl Bytes des UTF-8-Texts +1. **Footer lokalisieren:** * Gehe zu `EOF - 8`. Lese 8 Bytes in das `tag_footer_t` Struct. + * Validiere `magic == "TAG!"` und `version == 0x0001` (unter Berücksichtigung von Little-Endian Konvertierung via `sys_le16_to_cpu`). +2. **Grenzen berechnen:** + * Lese `total_size`. + * Die reinen Audiodaten enden bei `audio_limit = EOF - total_size`. + * Gehe zur Position `audio_limit`. +3. **TLV-Blöcke iterieren:** + * Solange die aktuelle Leseposition kleiner als `EOF - 8` ist: + * Lese 4 Bytes in den `tlv_header_t`. + * Wenn `type == 0x00`: Lese die nächsten 8 Bytes in das `tlv_audio_format_t` Struct. + * Wenn `type != 0x00`: Führe `fs_seek(header.length, FS_SEEK_CUR)` aus. -### 5.3 `0x10` CRC32_RAW +--- -- `value`: `uint32_t crc32` (4 Byte, Little Endian) -- `len`: **muss 4** sein +## 5. Hex-Beispiel -### 5.4 `0x20` FILE_FORMAT +Eine fiktive Datei enthält Audio-Daten. Es soll ein PCM-Mono Format (16 Bit, 16 kHz) sowie ein kurzes JSON `{"t":"A"}` (9 Bytes) angehängt werden. -- `value`: - - `bits_per_sample`: `uint8_t` - - `sample_rate`: `uint32_t` (Little Endian) -- `len`: **muss 5** sein +**1. TLV 0x00 (Audio Format):** +* Header: `00 00 08 00` (Type 0, Index 0, Length 8) +* Payload: `00 10 00 00 80 3E 00 00` (Mono, 16-Bit, Reserved, 16000 Hz) -Beispielwerte aktuell oft: `bits_per_sample = 16`, `sample_rate = 16000`. +**2. TLV 0x10 (JSON):** +* Header: `10 00 09 00` (Type 16, Index 0, Length 9) +* Payload: `7B 22 74 22 3A 22 41 22 7D` (`{"t":"A"}`) -## 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. +**3. Footer:** +* Total Size: `2D 00` (45 Bytes = 12 Bytes Audio-TLV + 13 Bytes JSON-TLV + 12 Bytes Padding/Zusatz + 8 Bytes Footer) -> *Hinweis: Size ist in diesem Konstrukt abhängig vom genauen Payload.* +* Version: `01 00` +* Magic: `54 41 47 21` (`TAG!`) \ No newline at end of file diff --git a/firmware/VERSION b/firmware/VERSION index 9d7380e..f876bc2 100644 --- a/firmware/VERSION +++ b/firmware/VERSION @@ -1,6 +1,6 @@ VERSION_MAJOR = 0 -VERSION_MINOR = 2 -PATCHLEVEL = 19 +VERSION_MINOR = 3 +PATCHLEVEL = 5 VERSION_TWEAK = 0 #if (IS_ENABLED(CONFIG_LOG)) EXTRAVERSION = debug diff --git a/firmware/src/audio.h b/firmware/include/audio.h similarity index 100% rename from firmware/src/audio.h rename to firmware/include/audio.h diff --git a/firmware/src/fs.h b/firmware/include/fs.h similarity index 79% rename from firmware/src/fs.h rename to firmware/include/fs.h index 281b328..aa1473e 100644 --- a/firmware/src/fs.h +++ b/firmware/include/fs.h @@ -10,6 +10,37 @@ typedef struct slot_info_t { size_t size; } slot_info_t; +typedef enum { + FS_MSG_START, + FS_MSG_CHUNK, + FS_MSG_EOF, + FS_MSG_ABORT +} fs_msg_type_t; + +typedef struct { + fs_msg_type_t type; + + /* Die Union spart RAM, da Start- und Chunk-Parameter + nie gleichzeitig im selben Message-Paket benötigt werden. */ + union { + /* Payload für FS_MSG_START */ + struct { + /* Der String wird sicher in die Queue kopiert */ + char filename[MAX_PATH_LEN]; + uint32_t expected_size; + uint32_t start_position; + } start; + + /* Payload für FS_MSG_CHUNK */ + struct { + void *slab_ptr; + uint32_t chunk_size; + } chunk; + }; +} fs_msg_t; + +extern struct k_msgq fs_msgq; + /** * @brief Initializes the filesystem by mounting it */ @@ -102,6 +133,21 @@ int fs_pm_mkdir(const char *path); */ int fs_pm_rename(const char *old_path, const char *new_path); +/** + * @brief Recursively creates directories for the given path, ensuring the flash is active during the operation + * @param path Path to the directory to create (can include multiple levels, e.g. "/dir1/dir2/dir3") + * @return 0 on success, negative error code on failure + */ +int fs_pm_mkdir_recursive(char *path); + +/** + * @brief Recursively removes a directory and all its contents, ensuring the flash is active during the operation + * @param path Path to the directory to remove + * @param max_len Maximum length of the path buffer + * @return 0 on success, negative error code on failure + */ +int fs_pm_rm_recursive(char *path, size_t max_len); + /** * @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 @@ -138,29 +184,15 @@ int fs_tag_open_read(struct fs_file_t *fp, uint8_t *version, size_t *payload_len 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 - * @return 0 on success, negative error code on failure + * @brief Setzt die Synchronisation für einen neuen Dateitransfer zurück. */ -int fs_tag_open_write(struct fs_file_t *fp); +void fs_reset_transfer_sync(void); /** - * @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 + * @brief Blockiert den aufrufenden Thread, bis der FS-Thread den Transfer + * (EOF oder ABORT) vollständig auf dem Flash abgeschlossen hat. */ -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); +void fs_wait_for_transfer_complete(void); /** * @brief Retrieves information about the firmware slot, such as start address and size diff --git a/firmware/src/io.h b/firmware/include/io.h similarity index 100% rename from firmware/src/io.h rename to firmware/include/io.h diff --git a/firmware/src/protocol.h b/firmware/include/protocol.h similarity index 91% rename from firmware/src/protocol.h rename to firmware/include/protocol.h index f48cd1e..12ae16c 100644 --- a/firmware/src/protocol.h +++ b/firmware/include/protocol.h @@ -30,6 +30,14 @@ typedef enum { CMD_PUT_FILE = 0x20, CMD_PUT_FW = 0x21, CMD_GET_FILE = 0x22, + CMD_PUT_TAGS = 0x24, + CMD_GET_TAGS = 0x25, + + CMD_PLAY = 0x30, + CMD_STOP = 0x31, + + CMD_SET_SETTING = 0x40, + CMD_GET_SETTING = 0x41, } protocol_cmd_t; typedef enum { diff --git a/firmware/include/settings.h b/firmware/include/settings.h new file mode 100644 index 0000000..546cf4e --- /dev/null +++ b/firmware/include/settings.h @@ -0,0 +1,28 @@ +#ifndef BUZZER_SETTINGS_H +#define BUZZER_SETTINGS_H + +#include +#include + +/* Struktur für den direkten Lesezugriff aus dem RAM (Zero-Latency) */ +typedef struct { + uint8_t audio_vol; /* 0..100 */ + bool play_norepeat; /* true = 1, false = 0 */ + uint32_t storage_interval_s; /* 0..7200 Sekunden */ +} app_settings_t; + +/* Globale Instanz für den direkten Lesezugriff */ +extern app_settings_t app_settings; + +/* Initialisiert das Settings-Subsystem, NVS und lädt die gespeicherten Werte */ +int app_settings_init(void); + +/* Setter: Aktualisieren den RAM-Wert und starten/verlängern den Speichern-Timer */ +void app_settings_set_audio_vol(uint8_t vol); +void app_settings_set_play_norepeat(bool norepeat); +void app_settings_set_storage_interval(uint32_t interval_s); + +/* Forciert sofortiges Speichern aller anstehenden Werte (Aufruf z.B. vor CMD_REBOOT) */ +void app_settings_save_pending_now(void); + +#endif /* BUZZER_SETTINGS_H */ \ No newline at end of file diff --git a/firmware/include/uart.h b/firmware/include/uart.h new file mode 100644 index 0000000..6d9718c --- /dev/null +++ b/firmware/include/uart.h @@ -0,0 +1,8 @@ +#ifndef _UART_H_ +#define _UART_H_ + +int uart_init(void); +int uart_write(const uint8_t *data, size_t len, k_timeout_t timeout); +int uart_write_string(const char *str, k_timeout_t timeout); +int uart_read(uint8_t *buffer, size_t max_len, k_timeout_t timeout); +#endif /* _UART_H_ */ \ No newline at end of file diff --git a/firmware/include/usb.h b/firmware/include/usb.h new file mode 100644 index 0000000..c75b909 --- /dev/null +++ b/firmware/include/usb.h @@ -0,0 +1,8 @@ +#ifndef USB_H_ +#define USB_H_ + +int usb_init(void); +void usb_wait_for_dtr(void); +bool usb_dtr_active(void); + +#endif /* USB_H_ */ \ No newline at end of file diff --git a/firmware/src/utils.h b/firmware/include/utils.h similarity index 100% rename from firmware/src/utils.h rename to firmware/include/utils.h diff --git a/firmware/pm_static.yml b/firmware/pm_static.yml index 66455d3..e4a4303 100644 --- a/firmware/pm_static.yml +++ b/firmware/pm_static.yml @@ -3,7 +3,7 @@ mcuboot: size: 0xC000 region: flash_primary -# Primary Slot: Start bleibt 0xC000, Größe jetzt 200KB (0x32000) +# Primary Slot: Start bleibt 0xC000, Größe 200KB (0x32000) mcuboot_primary: address: 0xC000 size: 0x32000 @@ -26,7 +26,13 @@ mcuboot_secondary: size: 0x32000 region: flash_primary -# External Flash bleibt unverändert +# NVS storage am Ende des Flashs, 16KB (0x4000) +settings_storage: + address: 0xFC000 + size: 0x4000 + region: flash_primary + +# External Flash littlefs_storage: address: 0x0 size: 0x800000 diff --git a/firmware/prj.conf b/firmware/prj.conf index 805fd52..40f02a2 100644 --- a/firmware/prj.conf +++ b/firmware/prj.conf @@ -1,7 +1,7 @@ # --- GPIO & Logging --- CONFIG_GPIO=y CONFIG_LOG=y -CONFIG_POLL=y +CONFIG_POLL=n # --- Power Management (Fix für HAS_PM & Policy) --- # CONFIG_PM=y @@ -13,24 +13,25 @@ CONFIG_FLASH_MAP=y CONFIG_FILE_SYSTEM=y CONFIG_FILE_SYSTEM_LITTLEFS=y CONFIG_FILE_SYSTEM_MKFS=y -CONFIG_FS_LITTLEFS_READ_SIZE=64 +CONFIG_FS_LITTLEFS_READ_SIZE=256 CONFIG_FS_LITTLEFS_PROG_SIZE=256 -CONFIG_FS_LITTLEFS_CACHE_SIZE=512 -CONFIG_FS_LITTLEFS_LOOKAHEAD_SIZE=128 +CONFIG_FS_LITTLEFS_CACHE_SIZE=4096 +CONFIG_FS_LITTLEFS_LOOKAHEAD_SIZE=256 CONFIG_FS_LITTLEFS_BLOCK_CYCLES=512 CONFIG_MAIN_STACK_SIZE=2048 +# --- NVS & Settings (für die Speicherung von Konfigurationen) --- +CONFIG_NVS=y +CONFIG_SETTINGS=y +CONFIG_SETTINGS_NVS=y + # --- USB Device & CDC ACM --- -CONFIG_USB_DEVICE_STACK=y -CONFIG_DEPRECATION_TEST=y -CONFIG_USB_DEVICE_MANUFACTURER="Eduard Iten" -CONFIG_USB_DEVICE_PRODUCT="Edis 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 +CONFIG_USB_DEVICE_STACK_NEXT=y +CONFIG_USBD_CDC_ACM_CLASS=y +CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT=n +CONFIG_USBD_LOG_LEVEL_ERR=y +CONFIG_UDC_DRIVER_LOG_LEVEL_ERR=y +CONFIG_USBD_CDC_ACM_LOG_LEVEL_OFF=y # --- UART (für USB-CDC) --- CONFIG_SERIAL=y diff --git a/firmware/src/audio.c b/firmware/src/audio.c index 378f6d1..4d580d4 100644 --- a/firmware/src/audio.c +++ b/firmware/src/audio.c @@ -9,6 +9,7 @@ #include #include #include +#include #define AUDIO_THREAD_STACK_SIZE 2048 #define AUDIO_THREAD_PRIORITY 5 @@ -47,7 +48,6 @@ K_SEM_DEFINE(audio_ready_sem, 0, 1); static const struct device *const i2s_dev = DEVICE_DT_GET(I2S_NODE); static const struct gpio_dt_spec amp_en_dev = GPIO_DT_SPEC_GET(AUDIO_AMP_ENABLE_NODE, gpios); -static volatile int current_volume = 8; static volatile bool abort_playback = false; static char next_random_filename[64] = {0}; @@ -57,6 +57,8 @@ static char cached_404_path[] = "/lfs/sys/404"; static struct k_mutex i2s_lock; static struct k_work audio_stop_work; +static uint32_t last_played_index = 0xFFFFFFFF; + static void audio_stop_work_handler(struct k_work *work) { ARG_UNUSED(work); @@ -89,6 +91,52 @@ void i2s_resume(void) k_mutex_unlock(&i2s_lock); } +int get_random_file(char *out_filename, size_t max_len) +{ + if (audio_file_count == 0) + { + /* Fallback auf System-Sound, wenn Ordner leer */ + strncpy(out_filename, cached_404_path, max_len); + return 0; + } + + uint32_t target_index; + + /* Random-Index generieren mit optionalem No-Repeat-Schutz */ + if (app_settings.play_norepeat && audio_file_count > 1) { + do { + target_index = k_cycle_get_32() % audio_file_count; + } while (target_index == last_played_index); + } else { + target_index = k_cycle_get_32() % audio_file_count; + } + + last_played_index = target_index; + + struct fs_dir_t dirp; + struct fs_dirent entry; + uint32_t current_index = 0; + + fs_dir_t_init(&dirp); + if (fs_pm_opendir(&dirp, AUDIO_PATH) < 0) + return -ENOENT; + + while (fs_readdir(&dirp, &entry) == 0 && entry.name[0] != '\0') + { + if (entry.type == FS_DIR_ENTRY_FILE) + { + if (current_index == target_index) + { + snprintf(out_filename, max_len, "%s/%s", AUDIO_PATH, entry.name); + break; + } + current_index++; + } + } + fs_pm_closedir(&dirp); + return 0; +} + void audio_refresh_file_count(void) { static struct fs_dir_t dirp; @@ -112,6 +160,7 @@ void audio_refresh_file_count(void) fs_pm_closedir(&dirp); audio_file_count = count; LOG_INF("Audio cache refreshed: %u files found in %s", count, AUDIO_PATH); + get_random_file(next_random_filename, sizeof(next_random_filename)); } static void wait_for_i2s_drain(void) @@ -130,40 +179,6 @@ static void wait_for_i2s_drain(void) } } -int get_random_file(char *out_filename, size_t max_len) -{ - if (audio_file_count == 0) - { - /* Fallback auf System-Sound, wenn Ordner leer */ - strncpy(out_filename, cached_404_path, max_len); - return 0; - } - - struct fs_dir_t dirp; - struct fs_dirent entry; - uint32_t target_index = k_cycle_get_32() % audio_file_count; - uint32_t current_index = 0; - - fs_dir_t_init(&dirp); - if (fs_pm_opendir(&dirp, AUDIO_PATH) < 0) - return -ENOENT; - - while (fs_readdir(&dirp, &entry) == 0 && entry.name[0] != '\0') - { - if (entry.type == FS_DIR_ENTRY_FILE) - { - if (current_index == target_index) - { - snprintf(out_filename, max_len, "%s/%s", AUDIO_PATH, entry.name); - break; - } - current_index++; - } - } - fs_pm_closedir(&dirp); - return 0; -} - void audio_system_ready(void) { k_sem_give(&audio_ready_sem); @@ -250,8 +265,8 @@ void audio_thread(void *arg1, void *arg2, void *arg3) bool trigger_started = false; int queued_blocks = 0; - uint8_t factor = MIN(255, current_volume * 0xFF / 100); - LOG_INF("Volume factor: %u (for volume %d%%)", factor, current_volume); + uint8_t factor = MIN(255, app_settings.audio_vol * 0xFF / 100); + LOG_INF("Volume factor: %u (for volume %d%%)", factor, app_settings.audio_vol); while (!abort_playback) { diff --git a/firmware/src/fs.c b/firmware/src/fs.c index 19fd7f7..59b18b3 100644 --- a/firmware/src/fs.c +++ b/firmware/src/fs.c @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -9,18 +10,23 @@ LOG_MODULE_REGISTER(buzz_fs, LOG_LEVEL_INF); +#define FS_THREAD_STACK_SIZE 2048 +#define FS_THREAD_PRIORITY 6 + +#define FS_MSGQ_MAX_ITEMS 4 +#define FS_SLAB_BUF_SIZE 4096 +#define TAG_FORMAT_VERSION 0x0001 #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) FS_LITTLEFS_DECLARE_DEFAULT_CONFIG(fs_storage_data); +K_MEM_SLAB_DEFINE(file_buffer_slab, FS_SLAB_BUF_SIZE, FS_MSGQ_MAX_ITEMS, 4); +K_MSGQ_DEFINE(fs_msgq, sizeof(fs_msg_t), FS_MSGQ_MAX_ITEMS, 4); +K_SEM_DEFINE(fs_transfer_done_sem, 0, 1); + #define QSPI_FLASH_NODE DT_ALIAS(qspi_flash) #if !DT_NODE_EXISTS(QSPI_FLASH_NODE) #error "QSPI Flash alias not defined in devicetree" @@ -33,6 +39,8 @@ static struct k_mutex flash_pm_lock; static struct slot_info_t slot1_info; static struct flash_img_context flash_ctx; +extern struct k_mem_slab file_buffer_slab; + static struct fs_mount_t fs_storage_mnt = { .type = FS_LITTLEFS, .fs_data = &fs_storage_data, @@ -40,6 +48,17 @@ static struct fs_mount_t fs_storage_mnt = { .mnt_point = "/lfs", }; +typedef enum { + FS_STATE_IDLE, + FS_STATE_RECEIVING +} fs_thread_state_t; + +typedef struct __attribute__((packed)) { + uint16_t total_size; + uint16_t version; + uint8_t magic[4]; +} tag_footer_t; + int fs_init(void) { int rc = fs_mount(&fs_storage_mnt); if (rc < 0) { @@ -121,10 +140,7 @@ int fs_pm_close(struct fs_file_t *file) { LOG_DBG("PM Closing file"); int rc = fs_close(file); - if (rc == 0) - { - fs_pm_flash_suspend(); - } + fs_pm_flash_suspend(); return rc; } @@ -144,10 +160,7 @@ int fs_pm_closedir(struct fs_dir_t *dirp) { LOG_DBG("PM Closing directory"); int rc = fs_closedir(dirp); - if (rc == 0) - { - fs_pm_flash_suspend(); - } + fs_pm_flash_suspend(); return rc; } @@ -196,11 +209,159 @@ int fs_pm_rename(const char *old_path, const char *new_path) return rc; } +int fs_pm_rm_recursive(char *path_buf, size_t max_len) +{ + struct fs_dirent entry; + struct fs_dir_t dir; + int rc; + + fs_pm_flash_resume(); + + /* 1. Stat prüfen: Ist es eine Datei? */ + rc = fs_stat(path_buf, &entry); + if (rc != 0) { + fs_pm_flash_suspend(); + return rc; + } + + /* Wenn es eine Datei ist, direkt löschen und beenden */ + if (entry.type == FS_DIR_ENTRY_FILE) { + rc = fs_unlink(path_buf); + fs_pm_flash_suspend(); + return rc; + } + + /* 2. Es ist ein Verzeichnis. Schleife bis es leer ist. */ + size_t orig_len = strlen(path_buf); + + while (1) { + fs_dir_t_init(&dir); + rc = fs_opendir(&dir, path_buf); + if (rc != 0) { + break; + } + + bool found_something = false; + + /* Genau EINEN löschbaren Eintrag suchen */ + while (1) { + rc = fs_readdir(&dir, &entry); + if (rc != 0 || entry.name[0] == '\0') { + break; /* Ende oder Fehler */ + } + if (strcmp(entry.name, ".") == 0 || strcmp(entry.name, "..") == 0) { + continue; /* Ignorieren */ + } + + found_something = true; + break; /* Treffer! Schleife abbrechen. */ + } + + /* WICHTIG: Das Verzeichnis SOFORT schließen, BEVOR wir rekurieren! + * Damit geben wir das File-Handle (NUM_DIRS) an Zephyr zurück. */ + fs_closedir(&dir); + + if (!found_something || rc != 0) { + break; /* Verzeichnis ist nun restlos leer */ + } + + size_t name_len = strlen(entry.name); + if (orig_len + 1 + name_len >= max_len) { + rc = -ENAMETOOLONG; + break; + } + + /* Pfad für das gefundene Kindelement bauen */ + path_buf[orig_len] = '/'; + strcpy(&path_buf[orig_len + 1], entry.name); + + /* Rekursiver Aufruf für das Kind */ + rc = fs_pm_rm_recursive(path_buf, max_len); + + /* Puffer sofort wieder auf unser Verzeichnis zurückschneiden */ + path_buf[orig_len] = '\0'; + + if (rc != 0) { + break; /* Abbruch, falls beim Löschen des Kindes ein Fehler auftrat */ + } + } + + /* 3. Das nun restlos leere Verzeichnis selbst löschen */ + if (rc == 0) { + rc = fs_unlink(path_buf); + } + + fs_pm_flash_suspend(); + return rc; +} + +int fs_pm_mkdir_recursive(char *path) +{ + int rc = 0; + struct fs_dirent entry; + char *p = path; + + /* Führenden Slash überspringen, falls vorhanden (z. B. bei "/lfs") */ + if (*p == '/') { + p++; + } + + /* Flash für den gesamten Durchlauf aktivieren */ + fs_pm_flash_resume(); + + while (*p != '\0') { + if (*p == '/') { + *p = '\0'; /* String temporär am aktuellen Slash terminieren */ + + /* Prüfen, ob dieser Pfadabschnitt bereits existiert */ + rc = fs_stat(path, &entry); + + if (rc == -ENOENT) { + /* Existiert nicht -> anlegen */ + rc = fs_mkdir(path); + if (rc != 0) { + *p = '/'; /* Bei Fehler Slash wiederherstellen und abbrechen */ + break; + } + } else if (rc == 0) { + /* Existiert -> prüfen, ob es ein Verzeichnis ist */ + if (entry.type != FS_DIR_ENTRY_DIR) { + rc = -ENOTDIR; + *p = '/'; + break; + } + } else { + /* Anderer Dateisystemfehler */ + *p = '/'; + break; + } + + *p = '/'; /* Slash für den nächsten Schleifendurchlauf wiederherstellen */ + } + p++; + } + + /* Letztes Element verarbeiten, falls der Pfad nicht mit '/' endet */ + if (rc == 0 && p > path && *(p - 1) != '/') { + rc = fs_stat(path, &entry); + if (rc == -ENOENT) { + rc = fs_mkdir(path); + } else if (rc == 0) { + if (entry.type != FS_DIR_ENTRY_DIR) { + rc = -ENOTDIR; + } + } + } + + /* Flash am Ende wieder in den Suspend schicken */ + 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; + tag_footer_t footer; if (audio_limit == NULL || payload_len == NULL || has_tag == NULL) { return -EINVAL; @@ -210,42 +371,41 @@ static int fs_get_tag_bounds(struct fs_file_t *fp, off_t file_size, *audio_limit = (size_t)file_size; *payload_len = 0U; - if (file_size < (off_t)TAG_FOOTER_V1_LEN) { + if (file_size < (off_t)sizeof(tag_footer_t)) { 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)) { + /* Den 8-Byte-Footer direkt in das Struct einlesen */ + fs_seek(fp, -(off_t)sizeof(tag_footer_t), FS_SEEK_END); + if (fs_read(fp, &footer, sizeof(tag_footer_t)) != sizeof(tag_footer_t)) { fs_seek(fp, 0, FS_SEEK_SET); return -EIO; } - if (memcmp(&footer[2], TAG_MAGIC, TAG_MAGIC_LEN) != 0) { + /* 1. Signatur prüfen */ + if (memcmp(footer.magic, TAG_MAGIC, 4) != 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; - } + /* 2. Endianness konvertieren */ + uint16_t tag_version = sys_le16_to_cpu(footer.version); + uint16_t tag_len = sys_le16_to_cpu(footer.total_size); + /* 3. Version und Größe validieren */ if (tag_version != TAG_FORMAT_VERSION) { fs_seek(fp, 0, FS_SEEK_SET); return -ENOTSUP; } + if (tag_len > (uint16_t)file_size || tag_len < sizeof(tag_footer_t)) { + fs_seek(fp, 0, FS_SEEK_SET); + return -EBADMSG; + } + *has_tag = true; *audio_limit = (size_t)file_size - tag_len; - *payload_len = tag_len - TAG_FOOTER_V1_LEN; + *payload_len = tag_len - sizeof(tag_footer_t); fs_seek(fp, 0, FS_SEEK_SET); return 0; @@ -321,51 +481,6 @@ ssize_t fs_tag_read_chunk(struct fs_file_t *fp, void *buffer, size_t len) return fs_read(fp, buffer, len); } -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; - } - - size_t total_footer_len = payload_len + TAG_FOOTER_V1_LEN; - if (total_footer_len > UINT16_MAX) { - return -EFBIG; - } - - 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) { if (slot1_info.size != 0) { *info = slot1_info; @@ -492,4 +607,113 @@ size_t fs_get_internal_flash_page_size(void) { } return info.size; -} \ No newline at end of file +} + +void fs_reset_transfer_sync(void) +{ + k_sem_reset(&fs_transfer_done_sem); +} + +void fs_wait_for_transfer_complete(void) +{ + k_sem_take(&fs_transfer_done_sem, K_FOREVER); +} + +static void fs_thread_entry(void *p1, void *p2, void *p3) +{ + ARG_UNUSED(p1); + ARG_UNUSED(p2); + ARG_UNUSED(p3); + + LOG_INF("Filesystem thread started"); + fs_thread_state_t state = FS_STATE_IDLE; + fs_msg_t msg; + struct fs_file_t current_file; + fs_file_t_init(¤t_file); + char current_filename[MAX_PATH_LEN] = {0}; + + while (1) + { + k_timeout_t wait_time = (state == FS_STATE_IDLE) ? K_FOREVER : K_SECONDS(1); + int rc = k_msgq_get(&fs_msgq, &msg, wait_time); + + if (rc == -EAGAIN) + { + if (state == FS_STATE_RECEIVING) + { + LOG_WRN("FS Transfer Timeout. Aborting and dropping file."); + fs_pm_close(¤t_file); + fs_pm_unlink(current_filename); + state = FS_STATE_IDLE; + k_sem_give(&fs_transfer_done_sem); + } + continue; + } + + switch (state) + { + case FS_STATE_IDLE: + if (msg.type == FS_MSG_START) + { + strncpy(current_filename, msg.start.filename, MAX_PATH_LEN - 1); + current_filename[MAX_PATH_LEN - 1] = '\0'; + + /* Bei Position 0 (Neuer Datei-Upload) die alte Datei restlos löschen */ + if (msg.start.start_position == 0) { + fs_pm_unlink(current_filename); + } + + rc = fs_pm_open(¤t_file, current_filename, FS_O_CREATE | FS_O_WRITE); + if (rc == 0) { + if (msg.start.start_position > 0) { + fs_seek(¤t_file, msg.start.start_position, FS_SEEK_SET); + } + state = FS_STATE_RECEIVING; + } else { + LOG_ERR("Failed to open %s: %d", current_filename, rc); + } + } + else if (msg.type == FS_MSG_CHUNK) + { + /* Chunks im IDLE-Status (z.B. nach Fehler) direkt verwerfen */ + if (msg.chunk.slab_ptr != NULL) + { + k_mem_slab_free(&file_buffer_slab, msg.chunk.slab_ptr); + } + } + else if (msg.type == FS_MSG_EOF || msg.type == FS_MSG_ABORT) + { + /* Verhindert Deadlocks, falls das Öffnen fehlgeschlagen war */ + k_sem_give(&fs_transfer_done_sem); + } + break; + + case FS_STATE_RECEIVING: + if (msg.type == FS_MSG_CHUNK) + { + if (msg.chunk.slab_ptr != NULL) + { + fs_write(¤t_file, msg.chunk.slab_ptr, msg.chunk.chunk_size); + k_mem_slab_free(&file_buffer_slab, msg.chunk.slab_ptr); + } + } + else if (msg.type == FS_MSG_EOF) + { + fs_pm_close(¤t_file); + state = FS_STATE_IDLE; + k_sem_give(&fs_transfer_done_sem); + } + else if (msg.type == FS_MSG_ABORT) + { + fs_pm_close(¤t_file); + fs_pm_unlink(current_filename); + state = FS_STATE_IDLE; + k_sem_give(&fs_transfer_done_sem); + } + break; + } + } +} + +K_THREAD_DEFINE(fs, FS_THREAD_STACK_SIZE, fs_thread_entry, + NULL, NULL, NULL, FS_THREAD_PRIORITY, 0, 0); \ No newline at end of file diff --git a/firmware/src/main.c b/firmware/src/main.c index 1d80614..796f81a 100644 --- a/firmware/src/main.c +++ b/firmware/src/main.c @@ -15,6 +15,8 @@ #include #include #include +#include +#include LOG_MODULE_REGISTER(main, LOG_LEVEL_INF); @@ -38,6 +40,13 @@ int main(void) int rc; + rc = app_settings_init(); + if (rc < 0) + { + LOG_ERR("Settings initialization failed: %d", rc); + return rc; + } + rc = fs_init(); if (rc < 0) { @@ -52,13 +61,21 @@ int main(void) return rc; } - rc = usb_cdc_acm_init(); + rc = usb_init(); if (rc < 0) { LOG_ERR("USB initialization failed: %d", rc); return rc; } + rc = uart_init(); + if (rc < 0) + { + LOG_ERR("UART initialization failed: %d", rc); + return rc; + } + + rc = io_init(); if (rc < 0) { @@ -81,6 +98,7 @@ int main(void) { LOG_INF("Firmware image already confirmed. No need to confirm again."); } + while (1) { k_sleep(K_FOREVER); diff --git a/firmware/src/protocol.c b/firmware/src/protocol.c index 04ad166..9da3647 100644 --- a/firmware/src/protocol.c +++ b/firmware/src/protocol.c @@ -1,8 +1,8 @@ +// protocol.c - Implementation of the custom protocol for file management and firmware info #include #include #include #include -#include #include #include #include @@ -10,35 +10,30 @@ #include #include +#include #include +#include #include #include #include +#include #define PROTOCOL_VERSION 1U LOG_MODULE_REGISTER(protocol, LOG_LEVEL_DBG); -#define PROTOCOL_STACK_SIZE 3072 -#define PROTOCOL_PRIORITY 6 +#define PROTOCOL_STACK_SIZE 4096 +#define PROTOCOL_PRIORITY 7 #define PROTOCOL_TIMEOUT_MS 10 #define PROTOCOL_TIMEOUT K_MSEC(PROTOCOL_TIMEOUT_MS) static const uint8_t sync_pattern[4] = {'B', 'U', 'Z', 'Z'}; #define SEND_SYNC() \ - usb_write_buffer(sync_pattern, sizeof(sync_pattern)); + uart_write(sync_pattern, sizeof(sync_pattern), PROTOCOL_TIMEOUT); -typedef struct { - char path[PROTOCOL_MAX_PATH_LEN]; - size_t index; - size_t len; -} path_context_t; - -path_context_t path1_ctx; -path_context_t path2_ctx; - -K_MEM_SLAB_DEFINE(file_buffer_slab, 4096, 4, 4); +extern struct k_msgq fs_msgq; +extern struct k_mem_slab file_buffer_slab; static protocol_error_t protocol_map_error(int32_t rc) { @@ -94,59 +89,34 @@ static protocol_error_t protocol_map_error(int32_t rc) static void send_ack_response() { uint8_t resp[5] = {0x42, 0x55, 0x5A, 0x5A, FRAME_RESP_ACK}; - usb_write_buffer(resp, sizeof(resp)); + uart_write(resp, sizeof(resp), PROTOCOL_TIMEOUT); } static void send_error_response(int32_t err) { uint8_t resp[6] = {0x42, 0x55, 0x5A, 0x5A, FRAME_RESP_ERROR, protocol_map_error(err)}; - usb_write_buffer(resp, sizeof(resp)); + uart_write(resp, sizeof(resp), PROTOCOL_TIMEOUT); } -/** - * Fills the provided buffer with data read from the USB interface, waiting up to the specified timeout for data to become available. - * @param buffer Buffer to fill with received data - * @param len Number of bytes to read into the buffer - * @param timeout Maximum time to wait for data before giving up - * @return Number of bytes read into the buffer, or a negative error code on failure - */ - -static int fill_buffer(uint8_t *buffer, size_t len, k_timeout_t timeout) +static int uart_read_exact(uint8_t *data, size_t len, k_timeout_t timeout) { - size_t offset = 0; - while (offset < len) { - if (!usb_wait_for_data(timeout)) { - return -ETIMEDOUT; + size_t total_read = 0; + while (total_read < len) { + int rc = uart_read(&data[total_read], len - total_read, timeout); + if (rc > 0) { + total_read += rc; + } else { + /* 0 = Timeout, <0 = Hardwarefehler */ + return (rc == 0) ? -ETIMEDOUT : rc; + } } - - size_t read_now = usb_read_buffer(buffer + offset, len - offset); - if (read_now == 0) { - return -EIO; - } - - offset += read_now; - } - return (int)offset; + return total_read; } -/** - * Reads a single byte from the USB interface, waiting up to the specified timeout. - * @param byte Pointer to store the read byte - * @return 1 if a byte was read, 0 if no data was available, or a negative error code on failure - */ -static int get_byte(uint8_t *byte, k_timeout_t timeout) +static bool get_path(uint8_t *buffer, size_t len) { - if (!usb_wait_for_data(timeout)) { - return -ETIMEDOUT; - } - - int ret = usb_read_byte(byte); - return (ret ? 1 : -ENODATA); -} - -static bool get_path(uint8_t *buffer, size_t len) { uint8_t path_len; - int rc = get_byte(&path_len, PROTOCOL_TIMEOUT); + int rc = uart_read_exact(&path_len, 1, PROTOCOL_TIMEOUT); if (rc <= 0) { send_error_response((rc < 0) ? rc : -EIO); @@ -160,12 +130,15 @@ static bool get_path(uint8_t *buffer, size_t len) { return false; } - rc = fill_buffer(buffer, path_len, PROTOCOL_TIMEOUT); + rc = uart_read_exact(buffer, path_len, PROTOCOL_TIMEOUT); if (rc != (int)path_len) { - if (rc >= 0) { + if (rc >= 0) + { send_error_response(-EIO); - } else { + } + else + { send_error_response(rc); } return false; @@ -180,7 +153,7 @@ static void proc_protocol_version() resp[0] = FRAME_RESP_DATA; sys_put_le16(PROTOCOL_VERSION, &resp[1]); SEND_SYNC(); - usb_write_buffer(resp, sizeof(resp)); + uart_write(resp, sizeof(resp), PROTOCOL_TIMEOUT); LOG_DBG("RESP: Protocol version %u", PROTOCOL_VERSION); return; } @@ -212,8 +185,8 @@ static void proc_firmware_status() sys_put_le32(KERNELVERSION, &resp[6]); resp[10] = strlen(APP_VERSION_STRING); SEND_SYNC(); - usb_write_buffer(resp, sizeof(resp)); - usb_write_buffer((const uint8_t *)APP_VERSION_STRING, strlen(APP_VERSION_STRING)); + uart_write(resp, sizeof(resp), PROTOCOL_TIMEOUT); + uart_write((const uint8_t *)APP_VERSION_STRING, strlen(APP_VERSION_STRING), PROTOCOL_TIMEOUT); LOG_DBG("RESP: Firmware status"); return; } @@ -238,20 +211,21 @@ static void proc_flash_info() sys_put_le32((uint32_t)fs_get_fw_slot_size(), &resp[13]); sys_put_le16((uint16_t)fs_get_external_flash_page_size(), &resp[17]); sys_put_le16((uint16_t)fs_get_internal_flash_page_size(), &resp[19]); - resp[21] = MAX_PATH_LEN; + resp[21] = MAX_PATH_LEN-1; SEND_SYNC(); - usb_write_buffer(resp, sizeof(resp)); + uart_write(resp, sizeof(resp), PROTOCOL_TIMEOUT); + LOG_DBG("RESP: Flash info"); return; } static void proc_stat() { - uint8_t buffer[MAX_PATH_LEN+1]; - if (!get_path(buffer, MAX_PATH_LEN)) + uint8_t buffer[MAX_PATH_LEN]; + if (!get_path(buffer, MAX_PATH_LEN-1)) { return; } - + char *path = (char *)buffer; struct fs_dirent entry; if (strcmp(path, "/") == 0) @@ -275,19 +249,19 @@ static void proc_stat() resp[1] = (entry.type == FS_DIR_ENTRY_DIR) ? 1 : 0; sys_put_le32((uint32_t)entry.size, &resp[2]); SEND_SYNC(); - usb_write_buffer(resp, sizeof(resp)); + uart_write(resp, sizeof(resp), PROTOCOL_TIMEOUT); LOG_DBG("RESP: Stat path '%s'", path); return; } static void proc_ls() { - uint8_t buffer[MAX_PATH_LEN+1]; - if (!get_path(buffer, MAX_PATH_LEN)) + uint8_t buffer[MAX_PATH_LEN]; + if (!get_path(buffer, MAX_PATH_LEN-1)) { return; } - + char *path = (char *)buffer; if (strlen(path) == 0) { @@ -307,10 +281,11 @@ static void proc_ls() } SEND_SYNC(); - usb_write_byte(FRAME_RESP_LIST_START); + uint8_t resp[8]; + resp[0] = FRAME_RESP_LIST_START; + uart_write(resp, 1, PROTOCOL_TIMEOUT); int num_entries = 0; - uint8_t resp[8]; resp[0] = FRAME_RESP_LIST_CHUNK; while ((rc = fs_readdir(&dir, &entry)) == 0) @@ -321,33 +296,34 @@ static void proc_ls() } resp[3] = (entry.type == FS_DIR_ENTRY_DIR) ? 1 : 0; sys_put_le32(entry.size, &resp[4]); - sys_put_le16(strlen(entry.name)+5, &resp[1]); + sys_put_le16(strlen(entry.name) + 5, &resp[1]); SEND_SYNC(); - usb_write_buffer(resp, sizeof(resp)); - usb_write_buffer(entry.name, strlen(entry.name) ); + uart_write(resp, sizeof(resp), PROTOCOL_TIMEOUT); + uart_write((const uint8_t *)entry.name, strlen(entry.name), PROTOCOL_TIMEOUT); num_entries++; } fs_pm_closedir(&dir); - resp[0]= FRAME_RESP_LIST_END; + resp[0] = FRAME_RESP_LIST_END; sys_put_le16(num_entries, &resp[1]); SEND_SYNC(); - usb_write_buffer(resp, 3); + uart_write(resp, 3, PROTOCOL_TIMEOUT); LOG_DBG("LIST: List directory '%s', %d entries", path, num_entries); return; } -static void proc_rm() { - uint8_t buffer[MAX_PATH_LEN+1]; - if (!get_path(buffer, MAX_PATH_LEN)) +static void proc_rm() +{ + uint8_t buffer[MAX_PATH_LEN]; + if (!get_path(buffer, MAX_PATH_LEN-1)) { return; } - + char *path = (char *)buffer; - int rc = fs_pm_unlink(path); + int rc = fs_pm_rm_recursive(path, MAX_PATH_LEN); if (rc != 0) { send_error_response(rc); @@ -356,21 +332,23 @@ static void proc_rm() { } send_ack_response(); LOG_DBG("RESP: Removed path '%s'", path); + audio_refresh_file_count(); return; } -static void proc_rename() { - uint8_t src_buffer[MAX_PATH_LEN+1]; - uint8_t dst_buffer[MAX_PATH_LEN+1]; - if (!get_path(src_buffer, MAX_PATH_LEN)) +static void proc_rename() +{ + uint8_t src_buffer[MAX_PATH_LEN]; + uint8_t dst_buffer[MAX_PATH_LEN]; + if (!get_path(src_buffer, MAX_PATH_LEN-1)) { return; } - if (!get_path(dst_buffer, MAX_PATH_LEN)) + if (!get_path(dst_buffer, MAX_PATH_LEN-1)) { return; } - + char *source_path = (char *)src_buffer; char *dest_path = (char *)dst_buffer; @@ -383,24 +361,29 @@ static void proc_rename() { } send_ack_response(); LOG_DBG("RESP: Renamed path '%s' to '%s'", source_path, dest_path); + audio_refresh_file_count(); return; } -static void proc_get_file() { - uint8_t path_buffer[MAX_PATH_LEN + 1]; - if (!get_path(path_buffer, MAX_PATH_LEN)) { +static void proc_get_file() +{ + uint8_t path_buffer[MAX_PATH_LEN]; + if (!get_path(path_buffer, MAX_PATH_LEN-1)) + { return; } - + char *path = (char *)path_buffer; struct fs_dirent entry; int rc = fs_pm_stat(path, &entry); - - if (rc != 0) { + + if (rc != 0) + { send_error_response(rc); return; } - if (entry.type == FS_DIR_ENTRY_DIR) { + if (entry.type == FS_DIR_ENTRY_DIR) + { send_error_response(-EISDIR); return; } @@ -408,7 +391,8 @@ static void proc_get_file() { struct fs_file_t file; fs_file_t_init(&file); rc = fs_pm_open(&file, path, FS_O_READ); - if (rc != 0) { + if (rc != 0) + { send_error_response(rc); LOG_ERR("Failed to open file '%s': %d", path, rc); return; @@ -418,13 +402,14 @@ static void proc_get_file() { resp[0] = FRAME_RESP_STREAM_START; uint32_t file_size = (uint32_t)entry.size; sys_put_le32(file_size, &resp[1]); - + SEND_SYNC(); - usb_write_buffer(resp, sizeof(resp)); + uart_write(resp, sizeof(resp), PROTOCOL_TIMEOUT); uint8_t *slab_ptr = NULL; rc = k_mem_slab_alloc(&file_buffer_slab, (void **)&slab_ptr, K_NO_WAIT); - if (rc != 0) { + if (rc != 0) + { send_error_response(-ENOMEM); LOG_ERR("Slab allocation failed: %d", rc); fs_pm_close(&file); @@ -435,39 +420,40 @@ static void proc_get_file() { uint32_t running_crc32 = 0U; bool stream_ok = true; - while (bytes_remaining > 0) { + while (bytes_remaining > 0) + { size_t to_read = MIN(bytes_remaining, 1024); ssize_t bytes_read = fs_read(&file, slab_ptr, to_read); - if (bytes_read < 0) { + if (bytes_read < 0) + { send_error_response((int)bytes_read); stream_ok = false; break; } - if (bytes_read == 0) { + if (bytes_read == 0) + { send_error_response(-EIO); stream_ok = false; break; } - LOG_DBG("1"); running_crc32 = crc32_ieee_update(running_crc32, slab_ptr, (size_t)bytes_read); - LOG_DBG("2"); - rc = usb_write_buffer(slab_ptr, (size_t)bytes_read); - LOG_DBG("3"); - if (rc < 0) { + rc = uart_write(slab_ptr, (size_t)bytes_read, PROTOCOL_TIMEOUT); + if (rc < 0) + { send_error_response(rc); - LOG_ERR("USB write failed: %d", rc); + LOG_ERR("UART write failed: %d", rc); stream_ok = false; break; } - LOG_DBG("4"); bytes_remaining -= bytes_read; } - k_mem_slab_free(&file_buffer_slab, (void **)&slab_ptr); + k_mem_slab_free(&file_buffer_slab, slab_ptr); fs_pm_close(&file); - if (!stream_ok || bytes_remaining != 0U) { + if (!stream_ok || bytes_remaining != 0U) + { LOG_ERR("Aborting stream for '%s' (remaining=%u, ok=%d)", path, bytes_remaining, stream_ok); return; } @@ -476,35 +462,199 @@ static void proc_get_file() { resp_end[0] = FRAME_RESP_STREAM_END; sys_put_le32(running_crc32, &resp_end[1]); SEND_SYNC(); - usb_write_buffer(resp_end, sizeof(resp_end)); + uart_write(resp_end, sizeof(resp_end), PROTOCOL_TIMEOUT); LOG_INF("File '%s' sent. CRC32: 0x%08X", path, running_crc32); } -static void proc_check_file_crc() +static void process_incoming_stream(const char *path, uint32_t expected_size, uint32_t start_position) { - uint8_t path_buffer[MAX_PATH_LEN + 1]; - if (!get_path(path_buffer, MAX_PATH_LEN)) { + fs_reset_transfer_sync(); + fs_msg_t start_msg; + start_msg.type = FS_MSG_START; + strncpy(start_msg.start.filename, path, MAX_PATH_LEN); + start_msg.start.expected_size = expected_size; + start_msg.start.start_position = start_position; + + k_msgq_put(&fs_msgq, &start_msg, K_FOREVER); + + send_ack_response(); + + uint32_t bytes_remaining = expected_size; + uint32_t running_crc32 = 0U; + bool stream_ok = true; + + k_timeout_t stream_timeout = K_MSEC(500); + while (bytes_remaining > 0) + { + uint8_t *slab_ptr = NULL; + + int rc = k_mem_slab_alloc(&file_buffer_slab, (void **)&slab_ptr, K_FOREVER); + if (rc != 0) { + LOG_ERR("Stream error: Slab allocation failed (%d)", rc); + stream_ok = false; + break; + } + + size_t to_read = MIN(bytes_remaining, 4096); + + rc = uart_read_exact(slab_ptr, to_read, stream_timeout); + + if (rc <= 0) { + if (rc == -ETIMEDOUT) { + LOG_ERR("Stream RX Timeout: Expected %zu bytes, got timeout. Remaining: %u", to_read, bytes_remaining); + } else { + LOG_ERR("Stream RX Error: Hardware or UART failure (%d)", rc); + } + + k_mem_slab_free(&file_buffer_slab, slab_ptr); + stream_ok = false; + break; + } + + running_crc32 = crc32_ieee_update(running_crc32, slab_ptr, to_read); + + fs_msg_t chunk_msg; + chunk_msg.type = FS_MSG_CHUNK; + chunk_msg.chunk.slab_ptr = slab_ptr; + chunk_msg.chunk.chunk_size = to_read; + k_msgq_put(&fs_msgq, &chunk_msg, K_FOREVER); + + bytes_remaining -= to_read; + } + + if (!stream_ok) { + fs_msg_t abort_msg; + abort_msg.type = FS_MSG_ABORT; + k_msgq_put(&fs_msgq, &abort_msg, K_FOREVER); + + /* Auf den FS-Thread warten */ + fs_wait_for_transfer_complete(); return; } - char *path = (char *)path_buffer; - struct fs_dirent entry; - int rc = fs_pm_stat(path, &entry); - if (rc != 0) { - send_error_response(rc); + fs_msg_t eof_msg; + eof_msg.type = FS_MSG_EOF; + k_msgq_put(&fs_msgq, &eof_msg, K_FOREVER); + + LOG_DBG("Waiting for FS thread to finalize transfer of '%s'", path); + fs_wait_for_transfer_complete(); + LOG_DBG("FS thread signaled transfer completion for '%s'", path); + + uint8_t resp_end[5]; + resp_end[0] = FRAME_RESP_STREAM_END; + sys_put_le32(running_crc32, &resp_end[1]); + SEND_SYNC(); + uart_write(resp_end, sizeof(resp_end), PROTOCOL_TIMEOUT); +} + +static void proc_put_file(void) +{ + uint8_t buffer[MAX_PATH_LEN]; + if (!get_path(buffer, MAX_PATH_LEN - 1)) { return; } - if (entry.type == FS_DIR_ENTRY_DIR) { - send_error_response(-EISDIR); + + char *path = (char *)buffer; + char *last_slash = strrchr(path, '/'); + + if (last_slash != NULL && last_slash != path) { + *last_slash = '\0'; + int rc = fs_pm_mkdir_recursive(path); + if (rc != 0) { + *last_slash = '/'; + send_error_response(rc); + return; + } + *last_slash = '/'; + } + + uint32_t file_size; + if (uart_read_exact((uint8_t *)&file_size, 4, PROTOCOL_TIMEOUT) <= 0) { + return; + } + + process_incoming_stream(path, file_size, 0); + audio_refresh_file_count(); +} + +static void proc_put_tags(void) +{ + uint8_t buffer[MAX_PATH_LEN]; + if (!get_path(buffer, MAX_PATH_LEN - 1)) { + return; + } + + char *path = (char *)buffer; + + uint32_t tag_stream_size; + if (uart_read_exact((uint8_t *)&tag_stream_size, 4, PROTOCOL_TIMEOUT) <= 0) { return; } struct fs_file_t file; fs_file_t_init(&file); - rc = fs_pm_open(&file, path, FS_O_READ); + int rc = fs_pm_open(&file, path, FS_O_READ); if (rc != 0) { send_error_response(rc); - LOG_ERR("Failed to open file '%s' for CRC: %d", path, rc); + return; + } + + /* Das Limit der reinen Audiodaten suchen */ + ssize_t audio_len = fs_get_audio_data_len(&file); + fs_pm_close(&file); + + if (audio_len < 0) { + send_error_response((int)audio_len); + return; + } + + LOG_DBG("Audio data length for '%s': %zd bytes. Tag stream size: %u bytes", path, audio_len, tag_stream_size); + process_incoming_stream(path, tag_stream_size, (uint32_t)audio_len); +} + +static void proc_get_tags(void) +{ + uint8_t buffer[MAX_PATH_LEN]; + if (!get_path(buffer, MAX_PATH_LEN - 1)) { + return; + } + + char *path = (char *)buffer; + + struct fs_file_t file; + fs_file_t_init(&file); + int rc = fs_pm_open(&file, path, FS_O_READ); + if (rc != 0) { + send_error_response(rc); + return; + } + + uint8_t version; + size_t payload_len; + rc = fs_tag_open_read(&file, &version, &payload_len); + + if (rc == -ENOENT) { + payload_len = 0; /* Keine Tags vorhanden */ + } else if (rc != 0) { + send_error_response(rc); + fs_pm_close(&file); + return; + } + + /* Start-Frame mit der reinen Payload-Größe (ohne Footer) senden */ + uint8_t resp[5]; + resp[0] = FRAME_RESP_STREAM_START; + sys_put_le32((uint32_t)payload_len, &resp[1]); + SEND_SYNC(); + uart_write(resp, sizeof(resp), PROTOCOL_TIMEOUT); + + if (payload_len == 0) { + fs_pm_close(&file); + /* Leeren Stream sofort beenden */ + resp[0] = FRAME_RESP_STREAM_END; + sys_put_le32(0, &resp[1]); + SEND_SYNC(); + uart_write(resp, sizeof(resp), PROTOCOL_TIMEOUT); return; } @@ -516,45 +666,337 @@ static void proc_check_file_crc() return; } + uint32_t bytes_remaining = payload_len; + uint32_t running_crc32 = 0U; + bool stream_ok = true; + + /* Nur die Payload lesen (fs_tag_read_chunk liest nicht über die Payload hinaus) */ + while (bytes_remaining > 0) { + size_t to_read = MIN(bytes_remaining, 4096); + ssize_t bytes_read = fs_tag_read_chunk(&file, slab_ptr, to_read); + + if (bytes_read <= 0) { + stream_ok = false; + break; + } + + running_crc32 = crc32_ieee_update(running_crc32, slab_ptr, bytes_read); + if (uart_write(slab_ptr, bytes_read, PROTOCOL_TIMEOUT) < 0) { + stream_ok = false; + break; + } + bytes_remaining -= bytes_read; + } + + k_mem_slab_free(&file_buffer_slab, slab_ptr); + fs_pm_close(&file); + + if (!stream_ok) { + return; /* Kein reguläres Ende bei I/O Fehlern */ + } + + resp[0] = FRAME_RESP_STREAM_END; + sys_put_le32(running_crc32, &resp[1]); + SEND_SYNC(); + uart_write(resp, sizeof(resp), PROTOCOL_TIMEOUT); +} + +static void proc_crc32() +{ + uint8_t path_buffer[MAX_PATH_LEN]; + if (!get_path(path_buffer, MAX_PATH_LEN-1)) + { + return; + } + + char *path = (char *)path_buffer; + struct fs_dirent entry; + int rc = fs_pm_stat(path, &entry); + if (rc != 0) + { + send_error_response(rc); + return; + } + if (entry.type == FS_DIR_ENTRY_DIR) + { + send_error_response(-EISDIR); + return; + } + + struct fs_file_t file; + fs_file_t_init(&file); + rc = fs_pm_open(&file, path, FS_O_READ); + if (rc != 0) + { + send_error_response(rc); + LOG_ERR("Failed to open file '%s' for CRC: %d", path, rc); + return; + } + + ssize_t audio_size = fs_get_audio_data_len(&file); + uint8_t *slab_ptr = NULL; + rc = k_mem_slab_alloc(&file_buffer_slab, (void **)&slab_ptr, K_NO_WAIT); + if (rc != 0) + { + send_error_response(-ENOMEM); + fs_pm_close(&file); + return; + } + uint32_t crc32 = 0U; - while (1) { - ssize_t bytes_read = fs_read(&file, slab_ptr, 4096); - if (bytes_read < 0) { + uint32_t crc32_audio = 0U; + + while (1) + { + ssize_t bytes_read = fs_read_audio(&file, slab_ptr, 4096, (size_t)audio_size); + if (bytes_read < 0) + { send_error_response((int)bytes_read); - k_mem_slab_free(&file_buffer_slab, (void **)&slab_ptr); + k_mem_slab_free(&file_buffer_slab, slab_ptr); fs_pm_close(&file); return; } - if (bytes_read == 0) { + if (bytes_read == 0) + { break; } crc32 = crc32_ieee_update(crc32, slab_ptr, (size_t)bytes_read); } - k_mem_slab_free(&file_buffer_slab, (void **)&slab_ptr); + crc32_audio = crc32; + + while (1) + { + ssize_t bytes_read = fs_read(&file, slab_ptr, 4096); + if (bytes_read < 0) + { + send_error_response((int)bytes_read); + k_mem_slab_free(&file_buffer_slab, slab_ptr); + fs_pm_close(&file); + return; + } + if (bytes_read == 0) + { + break; + } + crc32 = crc32_ieee_update(crc32, slab_ptr, (size_t)bytes_read); + } + + k_mem_slab_free(&file_buffer_slab, slab_ptr); fs_pm_close(&file); - uint8_t resp[5]; + uint8_t resp[9]; resp[0] = FRAME_RESP_DATA; sys_put_le32(crc32, &resp[1]); + sys_put_le32(crc32_audio, &resp[5]); SEND_SYNC(); - usb_write_buffer(resp, sizeof(resp)); - LOG_DBG("CRC32 for '%s': 0x%08X", path, crc32); + uart_write(resp, sizeof(resp), PROTOCOL_TIMEOUT); + LOG_DBG("CRC32 for file '%s': 0x%08X, audio data: 0x%08X", path, crc32, crc32_audio); } -static protocol_state_t process_frame_type(protocol_frame_type_t frame_type) +static void proc_put_fw(void) { - switch (frame_type) - { - case FRAME_REQ: - return PS_READ_REQ; - case FRAME_REQ_DATA: - return PS_READ_REQ_DATA; - default: - LOG_ERR("Invalid frame type: %d", frame_type); - send_error_response(-EINVAL); - return PS_WAIT_SYNC; + uint32_t fw_size; + if (uart_read_exact((uint8_t *)&fw_size, 4, PROTOCOL_TIMEOUT) <= 0) { + return; } + + int rc = flash_init_firmware_upload(); + if (rc < 0) { + send_error_response(rc); + return; + } + + /* Host signalisieren, dass der Flash gelöscht und bereit ist */ + send_ack_response(); + + uint32_t bytes_remaining = fw_size; + uint32_t running_crc32 = 0U; + bool stream_ok = true; + k_timeout_t stream_timeout = K_MSEC(500); + + while (bytes_remaining > 0) { + uint8_t *slab_ptr = NULL; + + rc = k_mem_slab_alloc(&file_buffer_slab, (void **)&slab_ptr, K_FOREVER); + if (rc != 0) { + LOG_ERR("FW Stream error: Slab allocation failed (%d)", rc); + stream_ok = false; + break; + } + + size_t to_read = MIN(bytes_remaining, 4096); + + rc = uart_read_exact(slab_ptr, to_read, stream_timeout); + + if (rc <= 0) { + LOG_ERR("FW Stream RX Error (%d)", rc); + k_mem_slab_free(&file_buffer_slab, slab_ptr); + stream_ok = false; + break; + } + + running_crc32 = crc32_ieee_update(running_crc32, slab_ptr, to_read); + + bool is_last = (bytes_remaining == to_read); + rc = flash_write_firmware_block(slab_ptr, to_read, is_last); + + k_mem_slab_free(&file_buffer_slab, slab_ptr); + + if (rc != 0) { + stream_ok = false; + break; + } + + bytes_remaining -= to_read; + } + + if (!stream_ok) { + return; + } + + rc = boot_request_upgrade(BOOT_UPGRADE_TEST); + if (rc < 0) { + send_error_response(rc); + return; + } + + uint8_t resp_end[5]; + resp_end[0] = FRAME_RESP_STREAM_END; + sys_put_le32(running_crc32, &resp_end[1]); + SEND_SYNC(); + uart_write(resp_end, sizeof(resp_end), PROTOCOL_TIMEOUT); +} + +static void proc_confirm_fw(void) +{ + int rc = boot_write_img_confirmed(); + if (rc == 0) { + send_ack_response(); + audio_play("/lfs/sys/confirm"); + } else { + send_error_response(rc); + } +} + +static void proc_reboot(void) +{ + send_ack_response(); + k_sleep(K_MSEC(50)); + reboot_with_status(REBOOT_STATUS_NORMAL); +} + +static void proc_play(void) +{ + uint8_t flags; + if (uart_read_exact(&flags, 1, PROTOCOL_TIMEOUT) <= 0) { + return; + } + + uint8_t buffer[MAX_PATH_LEN]; + if (!get_path(buffer, MAX_PATH_LEN - 1)) { + return; + } + + /* Bit 0 prüft auf Interrupt */ + if (flags & 0x01) { + audio_stop(); + } + audio_play((char *)buffer); + send_ack_response(); + LOG_DBG("RESP: Play path '%s' (flags: 0x%02x)", (char *)buffer, flags); +} + +static void proc_stop(void) +{ + audio_stop(); + send_ack_response(); + LOG_DBG("RESP: Audio stopped"); +} + +#include /* Für sys_get_le16 */ + +static void proc_set_setting(void) +{ + uint8_t key_len, val_len; + char key[64] = {0}; + uint8_t val_buf[4] = {0}; /* Puffer für den raw value (max 4 bytes erwartet) */ + + /* 1. Key lesen */ + if (uart_read_exact(&key_len, 1, PROTOCOL_TIMEOUT) <= 0) return; + if (key_len >= sizeof(key)) key_len = sizeof(key) - 1; + if (uart_read_exact((uint8_t *)key, key_len, PROTOCOL_TIMEOUT) <= 0) return; + + /* 2. Value-Länge lesen */ + if (uart_read_exact(&val_len, 1, PROTOCOL_TIMEOUT) <= 0) return; + + /* 3. Value lesen (Verhindert Desynchronisation des UART-Puffers) */ + if (val_len > sizeof(val_buf)) { + /* Puffer-Überlauf absichern. Hier könnte man die Bytes verwerfen, + für diesen Anwendungsfall reicht ein harter Abbruch. */ + send_error_response(-EINVAL); + return; + } + if (uart_read_exact(val_buf, val_len, PROTOCOL_TIMEOUT) <= 0) return; + + /* 4. Auswertung basierend auf dem Key */ + if (strcmp(key, "audio/vol") == 0) { + if (val_len != 1) { send_error_response(-EINVAL); return; } + app_settings_set_audio_vol(val_buf[0]); + audio_stop(); + audio_play("/lfs/sys/voltest"); + LOG_DBG("Set Setting: audio/vol = %u", val_buf[0]); + + } else if (strcmp(key, "play/norepeat") == 0) { + if (val_len != 1) { send_error_response(-EINVAL); return; } + app_settings_set_play_norepeat(val_buf[0] > 0); + LOG_DBG("Set Setting: play/norepeat = %u", val_buf[0]); + + } else if (strcmp(key, "settings/storage_interval") == 0) { + if (val_len != 2) { send_error_response(-EINVAL); return; } + /* Sicheres Lesen von 2 Bytes aus dem Array in ein uint16_t */ + uint16_t interval = sys_get_le16(val_buf); + app_settings_set_storage_interval((uint32_t)interval); + LOG_DBG("Set Setting: settings/storage_interval = %u", interval); + + } else { + send_error_response(0x02); /* INVALID_PARAMETERS */ + return; + } + + send_ack_response(); +} + +static void proc_get_setting(void) +{ + uint8_t key_len; + char key[64] = {0}; + uint8_t val_buf[4] = {0}; + uint8_t val_len = 0; + + if (uart_read_exact(&key_len, 1, PROTOCOL_TIMEOUT) <= 0) return; + if (key_len >= sizeof(key)) key_len = sizeof(key) - 1; + if (uart_read_exact((uint8_t *)key, key_len, PROTOCOL_TIMEOUT) <= 0) return; + + if (strcmp(key, "audio/vol") == 0) { + val_len = 1; + val_buf[0] = app_settings.audio_vol; + } else if (strcmp(key, "play/norepeat") == 0) { + val_len = 1; + val_buf[0] = app_settings.play_norepeat ? 1 : 0; + } else if (strcmp(key, "settings/storage_interval") == 0) { + val_len = 2; + sys_put_le16(app_settings.storage_interval_s, val_buf); + } else { + send_error_response(0x02); /* INVALID_PARAMETERS */ + return; + } + + uint8_t frame_type = FRAME_RESP_DATA; + SEND_SYNC(); + uart_write(&frame_type, 1, PROTOCOL_TIMEOUT); + uart_write(&val_len, 1, PROTOCOL_TIMEOUT); + uart_write(val_buf, val_len, PROTOCOL_TIMEOUT); } static void process_req(uint8_t req) @@ -577,7 +1019,7 @@ static void process_req(uint8_t req) proc_ls(); return; case CMD_CHECK_FILE_CRC: - proc_check_file_crc(); + proc_crc32(); return; case CMD_RM: proc_rm(); @@ -586,19 +1028,80 @@ static void process_req(uint8_t req) proc_rename(); return; case CMD_PUT_FILE: - send_error_response(-ENOTSUP); + proc_put_file(); return; case CMD_GET_FILE: proc_get_file(); return; + case CMD_PUT_TAGS: + proc_put_tags(); + return; + case CMD_GET_TAGS: + proc_get_tags(); + return; + case CMD_PUT_FW: + proc_put_fw(); + return; + case CMD_CONFIRM_FIRMWARE: + proc_confirm_fw(); + return; + case CMD_REBOOT: + proc_reboot(); + return; + case CMD_PLAY: + proc_play(); + return; + case CMD_STOP: + proc_stop(); + return; + case CMD_SET_SETTING: + proc_set_setting(); + return; + case CMD_GET_SETTING: + proc_get_setting(); + return; default: LOG_ERR("Invalid req type: 0x%02x", req); - send_error_response(ENOTSUP); + send_error_response(-ENOTSUP); return; } return; } +static bool wait_for_sync(k_timeout_t timeout) +{ + k_timeout_t current_timeout = timeout; + uint8_t byte; + size_t sync_pos = 0; + + while (1) + { + if (uart_read_exact(&byte, 1, current_timeout) == -ETIMEDOUT) + { + return false; // Timeout + } + if (byte == sync_pattern[sync_pos]) + { + current_timeout = PROTOCOL_TIMEOUT; // Reset timeout after each successful sync byte + sync_pos++; + if (sync_pos == sizeof(sync_pattern)) + { + return true; // SYNC received + } + } + else if (byte == sync_pattern[0]) + { + sync_pos = 1; // Possible start of new SYNC + } + else + { + current_timeout = timeout; // Reset timeout on mismatch + sync_pos = 0; // Reset on mismatch + } + } + return false; +} + void protocol_thread_entry(void *p1, void *p2, void *p3) { ARG_UNUSED(p1); @@ -607,60 +1110,44 @@ void protocol_thread_entry(void *p1, void *p2, void *p3) LOG_INF("Protocol thread started"); + int rc = 0; + + uint8_t frame_type; + uint8_t cmd; + while (1) { - uint8_t byte; - protocol_state_t state = PS_WAIT_SYNC; - uint8_t sync_pos = 0; - k_timeout_t timeout = K_FOREVER; - - while (usb_wait_for_data(timeout)) - { - while (usb_read_byte(&byte)) - { - - switch (state) - { - case PS_WAIT_SYNC: - if (byte == sync_pattern[sync_pos]) - { - timeout = PROTOCOL_TIMEOUT; - sync_pos++; - LOG_DBG("SYNC %u", sync_pos); - if (sync_pos == sizeof(sync_pattern)) - { - sync_pos = 0; - state = PS_READ_FRAME_TYPE; - } - } - else - { - sync_pos = 0; - timeout = K_FOREVER; - } - break; - case PS_READ_FRAME_TYPE: - LOG_DBG("FT: 0x%02x", byte); - state = process_frame_type(byte); - break; - case PS_READ_REQ: - process_req(byte); - state = PS_WAIT_SYNC; - timeout = K_FOREVER; - break; - default: - LOG_WRN("Invalid protocol state: 0x%02x", state); - state = PS_WAIT_SYNC; - timeout = K_FOREVER; - break; - } - } + if (!usb_dtr_active()) + { + usb_wait_for_dtr(); + LOG_INF("DTR active, waiting for SYNC..."); + } + wait_for_sync(K_FOREVER); + LOG_DBG("SYNC received"); + + rc = uart_read_exact(&frame_type, 1, PROTOCOL_TIMEOUT); + if (rc <= 0) + { + LOG_ERR("Failed to read frame type byte: %d", rc); + continue; + } + + rc = uart_read_exact((uint8_t *)&cmd, 1, PROTOCOL_TIMEOUT); + if (rc <= 0) { + LOG_ERR("Failed to read command byte: %d", rc); + continue; + } + + if (frame_type == FRAME_REQ) + { + LOG_DBG("REQ: Command 0x%02x", cmd); + process_req(cmd); + } + else + { + LOG_ERR("Invalid frame type received: 0x%02x", frame_type); + send_error_response(-EINVAL); } - send_error_response(-ETIMEDOUT); - state = PS_WAIT_SYNC; - timeout = K_FOREVER; - LOG_ERR("TIMEOUT, PS 0x%02x", state); - continue; } } diff --git a/firmware/src/settings.c b/firmware/src/settings.c new file mode 100644 index 0000000..5b2ad2d --- /dev/null +++ b/firmware/src/settings.c @@ -0,0 +1,166 @@ +#include "settings.h" +#include +#include +#include + +LOG_MODULE_REGISTER(app_settings, LOG_LEVEL_DBG); + +/* Initialisierung mit Standardwerten als Fallback */ +app_settings_t app_settings = { + .audio_vol = 50, + .play_norepeat = false, + .storage_interval_s = 3600 +}; + +/* Flags zur Markierung ungespeicherter Änderungen */ +static bool dirty_audio_vol = false; +static bool dirty_play_norepeat = false; +static bool dirty_storage_interval = false; + +/* Workqueue-Objekt für das asynchrone Speichern */ +static struct k_work_delayable save_work; + +/* -------------------------------------------------------------------------- */ +/* Lese-Handler für Zephyr (Aufruf beim Booten durch settings_load) */ +/* -------------------------------------------------------------------------- */ + +static int audio_settings_set(const char *name, size_t len, settings_read_cb read_cb, void *cb_arg) +{ + if (settings_name_steq(name, "vol", NULL)) { + return read_cb(cb_arg, &app_settings.audio_vol, sizeof(app_settings.audio_vol)); + } + return -ENOENT; +} + +static int play_settings_set(const char *name, size_t len, settings_read_cb read_cb, void *cb_arg) +{ + if (settings_name_steq(name, "norepeat", NULL)) { + uint8_t val = 0; + int rc = read_cb(cb_arg, &val, sizeof(val)); + if (rc >= 0) { + app_settings.play_norepeat = (val > 0); + } + return rc; + } + return -ENOENT; +} + +static int sys_settings_set(const char *name, size_t len, settings_read_cb read_cb, void *cb_arg) +{ + if (settings_name_steq(name, "storage_interval", NULL)) { + return read_cb(cb_arg, &app_settings.storage_interval_s, sizeof(app_settings.storage_interval_s)); + } + return -ENOENT; +} + +/* Registrierung der Namespaces für das automatische Laden */ +SETTINGS_STATIC_HANDLER_DEFINE(audio, "audio", NULL, audio_settings_set, NULL, NULL); +SETTINGS_STATIC_HANDLER_DEFINE(play, "play", NULL, play_settings_set, NULL, NULL); +SETTINGS_STATIC_HANDLER_DEFINE(sys, "settings", NULL, sys_settings_set, NULL, NULL); + +/* -------------------------------------------------------------------------- */ +/* Schreib-Logik (Asynchron über System Workqueue) */ +/* -------------------------------------------------------------------------- */ + +static void save_work_handler(struct k_work *work) +{ + if (dirty_audio_vol) { + settings_save_one("audio/vol", &app_settings.audio_vol, sizeof(app_settings.audio_vol)); + dirty_audio_vol = false; + LOG_DBG("NVS Write: audio/vol = %d", app_settings.audio_vol); + } + if (dirty_play_norepeat) { + uint8_t val = app_settings.play_norepeat ? 1 : 0; + settings_save_one("play/norepeat", &val, sizeof(val)); + dirty_play_norepeat = false; + LOG_DBG("NVS Write: play/norepeat = %d", val); + } + if (dirty_storage_interval) { + settings_save_one("settings/storage_interval", &app_settings.storage_interval_s, sizeof(app_settings.storage_interval_s)); + dirty_storage_interval = false; + LOG_DBG("NVS Write: settings/storage_interval = %d", app_settings.storage_interval_s); + } +} + +static void schedule_save(void) +{ + if (app_settings.storage_interval_s == 0) { + /* Direkter Schreibvorgang, Work sofort einreihen */ + k_work_cancel_delayable(&save_work); + k_work_submit(&save_work.work); + } else { + /* Timer neustarten (überschreibt laufenden Countdown) */ + k_work_reschedule(&save_work, K_SECONDS(app_settings.storage_interval_s)); + } +} + +void app_settings_save_pending_now(void) +{ + struct k_work_sync sync; + k_work_cancel_delayable_sync(&save_work, &sync); + save_work_handler(&save_work.work); +} + +/* -------------------------------------------------------------------------- */ +/* Setter (API für das Protokoll) */ +/* -------------------------------------------------------------------------- */ + +void app_settings_set_audio_vol(uint8_t vol) +{ + if (vol > 100) vol = 100; + + if (app_settings.audio_vol != vol) { + app_settings.audio_vol = vol; + dirty_audio_vol = true; + schedule_save(); + } +} + +void app_settings_set_play_norepeat(bool norepeat) +{ + if (app_settings.play_norepeat != norepeat) { + app_settings.play_norepeat = norepeat; + dirty_play_norepeat = true; + schedule_save(); + } +} + +void app_settings_set_storage_interval(uint32_t interval_s) +{ + if (interval_s > 7200) interval_s = 7200; + + if (app_settings.storage_interval_s != interval_s) { + app_settings.storage_interval_s = interval_s; + dirty_storage_interval = true; + schedule_save(); + } +} + +/* -------------------------------------------------------------------------- */ +/* Initialisierung */ +/* -------------------------------------------------------------------------- */ + +int app_settings_init(void) +{ + int err; + + k_work_init_delayable(&save_work, save_work_handler); + + err = settings_subsys_init(); + if (err) { + LOG_ERR("settings_subsys_init failed (err %d)", err); + return err; + } + + /* Lädt alle Werte aus dem NVS in den RAM */ + err = settings_load(); + if (err) { + LOG_ERR("settings_load failed (err %d)", err); + return err; + } + + LOG_INF("Settings init ok. Vol=%d, NoRepeat=%d, Interval=%d", + app_settings.audio_vol, app_settings.play_norepeat, app_settings.storage_interval_s); + + return 0; +} \ No newline at end of file diff --git a/firmware/src/uart.c b/firmware/src/uart.c new file mode 100644 index 0000000..3517c8d --- /dev/null +++ b/firmware/src/uart.c @@ -0,0 +1,135 @@ +// uart.c +#include +#include +#include +#include + +LOG_MODULE_REGISTER(uart, LOG_LEVEL_INF); + +#define RX_RING_BUF_SIZE 1024 +#define TX_RING_BUF_SIZE 1024 + +const struct device *const uart_dev = DEVICE_DT_GET_ONE(zephyr_cdc_acm_uart); + +RING_BUF_ITEM_DECLARE(rx_ringbuf, RX_RING_BUF_SIZE); +RING_BUF_ITEM_DECLARE(tx_ringbuf, TX_RING_BUF_SIZE); +K_SEM_DEFINE(tx_done_sem, 0, 1); +K_SEM_DEFINE(rx_ready_sem, 0, 1); + +static void uart_isr(const struct device *dev, void *user_data) +{ + ARG_UNUSED(user_data); + + if (!uart_irq_update(dev)) + { + return; + } + + if (uart_irq_rx_ready(dev)) + { + uint8_t *data_ptr; + uint32_t claimed_len; + int recv_len; + + claimed_len = ring_buf_put_claim(&rx_ringbuf, &data_ptr, RX_RING_BUF_SIZE); + + if (claimed_len > 0) + { + recv_len = uart_fifo_read(dev, data_ptr, claimed_len); + ring_buf_put_finish(&rx_ringbuf, recv_len); + if (recv_len > 0) + { + k_sem_give(&rx_ready_sem); + } + } + else + { + uart_irq_rx_disable(dev); + } + } + if (uart_irq_tx_ready(dev)) + { + uint8_t *data_ptr; + uint32_t claim_len; + int written; + + claim_len = ring_buf_get_claim(&tx_ringbuf, &data_ptr, ring_buf_size_get(&tx_ringbuf)); + + if (claim_len > 0) + { + written = uart_fifo_fill(dev, data_ptr, claim_len); + ring_buf_get_finish(&tx_ringbuf, written); + } + else + { + uart_irq_tx_disable(dev); + } + k_sem_give(&tx_done_sem); + } +} + +int uart_init(void) +{ + if (!device_is_ready(uart_dev)) + { + LOG_ERR("UART device not ready"); + return -ENODEV; + } + + uart_irq_callback_set(uart_dev, uart_isr); + uart_irq_rx_enable(uart_dev); + + LOG_INF("UART device initialized"); + return 0; +} + +int uart_write(const uint8_t *data, size_t len, k_timeout_t timeout) +{ + size_t written_total = 0; + k_sem_reset(&tx_done_sem); + + while (written_total < len) + { + uint32_t written = ring_buf_put(&tx_ringbuf, &data[written_total], len - written_total); + written_total += written; + + if (written > 0) + { + uart_irq_tx_enable(uart_dev); + } + + if (written_total < len) + { + int ret = k_sem_take(&tx_done_sem, timeout); + if (ret != 0) + { + return ret; + } + } + } + return written_total; +} + +int uart_write_string(const char *str, k_timeout_t timeout) +{ + return uart_write((const uint8_t *)str, strlen(str), timeout); +} + +int uart_read(uint8_t *data, size_t len, k_timeout_t timeout) +{ + uint32_t read_len = ring_buf_get(&rx_ringbuf, data, len); + + if (read_len == 0 && !K_TIMEOUT_EQ(timeout, K_NO_WAIT)) { + k_sem_reset(&rx_ready_sem); + if (ring_buf_is_empty(&rx_ringbuf)) { + if (k_sem_take(&rx_ready_sem, timeout) != 0) { + return -ETIMEDOUT; + } + } + read_len = ring_buf_get(&rx_ringbuf, data, len); + } + if (read_len > 0) { + uart_irq_rx_enable(uart_dev); + } + return read_len; +} \ No newline at end of file diff --git a/firmware/src/usb.c b/firmware/src/usb.c index dd5a796..0962e94 100644 --- a/firmware/src/usb.c +++ b/firmware/src/usb.c @@ -1,202 +1,181 @@ -#include -#include -#include +#include #include -#include /* NEU */ +#include +#include +#include -#include +#include "usb.h" -#define RX_RING_BUF_SIZE 1024 +#define USB_MANUFACTURER_STRING "Iten Engineering" +#define USB_PRODUCT_STRING "Edis Buzzer" +#define USB_DEVICE_VID 0x1209 +#define USB_DEVICE_PID 0xEDED LOG_MODULE_REGISTER(usb, LOG_LEVEL_INF); -K_SEM_DEFINE(usb_rx_sem, 0, 1); -K_SEM_DEFINE(usb_tx_sem, 0, 1); +K_SEM_DEFINE(dtr_active_sem, 0, 1); +static uint32_t dtr_active = 0U; -#define UART_NODE DT_ALIAS(usb_uart) -const struct device *cdc_dev = DEVICE_DT_GET(UART_NODE); -static volatile bool rx_interrupt_enabled = false; +USBD_DEVICE_DEFINE(cdc_acm_serial, + DEVICE_DT_GET(DT_NODELABEL(zephyr_udc0)), + USB_DEVICE_VID, USB_DEVICE_PID); -RING_BUF_DECLARE(rx_ringbuf, RX_RING_BUF_SIZE); +USBD_DESC_LANG_DEFINE(cdc_acm_lang); +USBD_DESC_MANUFACTURER_DEFINE(cdc_acm_mfr, USB_MANUFACTURER_STRING); +USBD_DESC_PRODUCT_DEFINE(cdc_acm_product, USB_PRODUCT_STRING); +IF_ENABLED(CONFIG_HWINFO, (USBD_DESC_SERIAL_NUMBER_DEFINE(cdc_acm_sn))); -static void cdc_acm_irq_cb(const struct device *dev, void *user_data) +USBD_DESC_CONFIG_DEFINE(fs_cfg_desc, "FS Configuration"); +USBD_DESC_CONFIG_DEFINE(hs_cfg_desc, "HS Configuration"); + +USBD_CONFIGURATION_DEFINE(fs_config, 0U, 125U, &fs_cfg_desc); +USBD_CONFIGURATION_DEFINE(hs_config, 0U, 125U, &hs_cfg_desc); + +static void fix_code_triple(struct usbd_context *uds_ctx, enum usbd_speed speed) { - ARG_UNUSED(user_data); - - if (!uart_irq_update(dev)) { - return; - } - - if (uart_irq_rx_ready(dev)) { - uint8_t buffer[64]; - uint32_t space = ring_buf_space_get(&rx_ringbuf); - - if (space == 0) { - /* Backpressure anwenden: Ringpuffer ist voll. - Interrupt deaktivieren, damit Daten im HW-FIFO bleiben - und der USB-Stack den Host drosselt (NAK). */ - uart_irq_rx_disable(dev); - } else { - int to_read = MIN(sizeof(buffer), space); - int len = uart_fifo_read(dev, buffer, to_read); - - if (len > 0) { - ring_buf_put(&rx_ringbuf, buffer, len); - k_sem_give(&usb_rx_sem); - } - } - } - - if (uart_irq_tx_ready(dev)) { - uart_irq_tx_disable(dev); - k_sem_give(&usb_tx_sem); - } + if (IS_ENABLED(CONFIG_USBD_CDC_ACM_CLASS) || + IS_ENABLED(CONFIG_USBD_CDC_ECM_CLASS) || + IS_ENABLED(CONFIG_USBD_CDC_NCM_CLASS) || + IS_ENABLED(CONFIG_USBD_MIDI2_CLASS) || + IS_ENABLED(CONFIG_USBD_AUDIO2_CLASS) || + IS_ENABLED(CONFIG_USBD_VIDEO_CLASS)) { + usbd_device_set_code_triple(uds_ctx, speed, + USB_BCC_MISCELLANEOUS, 0x02, 0x01); + } else { + usbd_device_set_code_triple(uds_ctx, speed, 0, 0, 0); + } } -bool usb_wait_for_data(k_timeout_t timeout) +static void usbd_msg_cb(struct usbd_context *const ctx, const struct usbd_msg *const msg) { - if (!ring_buf_is_empty(&rx_ringbuf)) { - return true; - } - - /* Wenn der Puffer leer ist, sicherstellen, dass der RX-Interrupt - aktiviert ist, da sonst keine neuen Daten empfangen werden können. */ - if (device_is_ready(cdc_dev)) { - uart_irq_rx_enable(cdc_dev); - } - - return (k_sem_take(&usb_rx_sem, timeout) == 0); + int err; + + LOG_DBG("USBD message: %s", usbd_msg_type_string(msg->type)); + + if (usbd_can_detect_vbus(ctx)) { + if (msg->type == USBD_MSG_VBUS_READY) { + err = usbd_enable(ctx); + if (err) { + LOG_ERR("Failed to enable USB device (%d)", err); + } + } + + if (msg->type == USBD_MSG_VBUS_REMOVED) { + err = usbd_disable(ctx); + if (err) { + LOG_ERR("Failed to disable USB device (%d)", err); + } + } + } + + if (msg->type == USBD_MSG_CDC_ACM_CONTROL_LINE_STATE) { + uint32_t rts = 0U; + uint32_t dcd = 0U; + uint32_t dsr = 0U; + + if (msg->dev != NULL) { + (void)uart_line_ctrl_get(msg->dev, UART_LINE_CTRL_RTS, &rts); + (void)uart_line_ctrl_get(msg->dev, UART_LINE_CTRL_DTR, &dtr_active); + (void)uart_line_ctrl_get(msg->dev, UART_LINE_CTRL_DCD, &dcd); + (void)uart_line_ctrl_get(msg->dev, UART_LINE_CTRL_DSR, &dsr); + LOG_DBG("CDC ACM RTS: %u, DTR: %u, DCD: %u, DSR: %u", rts, dtr_active, dcd, dsr); + if (dtr_active) { + k_sem_give(&dtr_active_sem); + } + } + } } -bool usb_read_byte(uint8_t *c) +int usb_init(void) { - int ret = ring_buf_get(&rx_ringbuf, c, 1); - return (ret > 0); + int err; + + err = usbd_add_descriptor(&cdc_acm_serial, &cdc_acm_lang); + if (err) { + LOG_ERR("Failed to add language descriptor (%d)", err); + return err; + } + + err = usbd_add_descriptor(&cdc_acm_serial, &cdc_acm_mfr); + if (err) { + LOG_ERR("Failed to add manufacturer descriptor (%d)", err); + return err; + } + + err = usbd_add_descriptor(&cdc_acm_serial, &cdc_acm_product); + if (err) { + LOG_ERR("Failed to add product descriptor (%d)", err); + return err; + } + + IF_ENABLED(CONFIG_HWINFO, ( + err = usbd_add_descriptor(&cdc_acm_serial, &cdc_acm_sn); + )) + if (err) { + LOG_ERR("Failed to add serial-number descriptor (%d)", err); + return err; + } + + if (USBD_SUPPORTS_HIGH_SPEED && usbd_caps_speed(&cdc_acm_serial) == USBD_SPEED_HS) { + err = usbd_add_configuration(&cdc_acm_serial, USBD_SPEED_HS, &hs_config); + if (err) { + LOG_ERR("Failed to add HS configuration (%d)", err); + return err; + } + + err = usbd_register_class(&cdc_acm_serial, "cdc_acm_0", USBD_SPEED_HS, 1); + if (err) { + LOG_ERR("Failed to register HS CDC ACM class (%d)", err); + return err; + } + + fix_code_triple(&cdc_acm_serial, USBD_SPEED_HS); + } + + err = usbd_add_configuration(&cdc_acm_serial, USBD_SPEED_FS, &fs_config); + if (err) { + LOG_ERR("Failed to add FS configuration (%d)", err); + return err; + } + + err = usbd_register_class(&cdc_acm_serial, "cdc_acm_0", USBD_SPEED_FS, 1); + if (err) { + LOG_ERR("Failed to register FS CDC ACM class (%d)", err); + return err; + } + + fix_code_triple(&cdc_acm_serial, USBD_SPEED_FS); + + err = usbd_msg_register_cb(&cdc_acm_serial, usbd_msg_cb); + if (err) { + LOG_ERR("Failed to register USBD callback (%d)", err); + return err; + } + + err = usbd_init(&cdc_acm_serial); + if (err) { + LOG_ERR("Failed to initialize USBD (%d)", err); + return err; + } + + if (!usbd_can_detect_vbus(&cdc_acm_serial)) { + err = usbd_enable(&cdc_acm_serial); + if (err) { + LOG_ERR("Failed to enable USBD (%d)", err); + return err; + } + } + + LOG_INF("USBD CDC ACM initialized"); + return 0; } -int usb_read_buffer(uint8_t *buf, size_t max_len) +void usb_wait_for_dtr(void) { - int ret = ring_buf_get(&rx_ringbuf, buf, max_len); - return ret; + k_sem_take(&dtr_active_sem, K_FOREVER); } -void usb_resume_rx(void) +bool usb_dtr_active(void) { - if (device_is_ready(cdc_dev)) { - uart_irq_rx_enable(cdc_dev); - } -} - -void usb_write_byte(uint8_t c) -{ - if (!device_is_ready(cdc_dev)) { - return; - } - uart_poll_out(cdc_dev, c); -} - -int usb_write_buffer(const uint8_t *buf, size_t len) -{ - if (!device_is_ready(cdc_dev)) - { - return -ENODEV; - } - - size_t written; - while (len > 0) - { - written = uart_fifo_fill(cdc_dev, buf, len); - - len -= written; - buf += written; - - uart_irq_tx_enable(cdc_dev); - - if (len > 0) - { - - if (k_sem_take(&usb_tx_sem, K_MSEC(100)) != 0) - { - LOG_WRN("USB TX timeout - consumer not reading?"); - return -ETIMEDOUT; - } - } - } - return 0; -} - -void usb_flush_rx(void) -{ - uint8_t dummy; - if (!device_is_ready(cdc_dev)) return; - - /* Hardware-FIFO leeren, falls Reste vorhanden */ - while (uart_fifo_read(cdc_dev, &dummy, 1) > 0); - - /* Ringpuffer und Semaphore zurücksetzen */ - ring_buf_reset(&rx_ringbuf); - k_sem_reset(&usb_rx_sem); -} - -static void usb_status_cb(enum usb_dc_status_code cb_status, const uint8_t *param) -{ - switch (cb_status) { - case USB_DC_CONNECTED: - /* VBUS wurde vom Zephyr-Stack erkannt */ - LOG_DBG("VBUS detected, USB device connected"); - break; - case USB_DC_CONFIGURED: - LOG_DBG("USB device configured by host"); - io_usb_status(true); - if (device_is_ready(cdc_dev)) { - (void)uart_line_ctrl_set(cdc_dev, UART_LINE_CTRL_DCD, 1); - (void)uart_line_ctrl_set(cdc_dev, UART_LINE_CTRL_DSR, 1); - - /* Interrupt-Handler binden und initial aktivieren */ - uart_irq_callback_set(cdc_dev, cdc_acm_irq_cb); - uart_irq_rx_enable(cdc_dev); - } - break; - case USB_DC_DISCONNECTED: - /* Kabel wurde gezogen */ - LOG_DBG("VBUS removed, USB device disconnected"); - if (device_is_ready(cdc_dev)) { - uart_irq_rx_disable(cdc_dev); - } - io_usb_status(false); - break; - case USB_DC_RESET: - LOG_DBG("USB bus reset"); - break; - default: - break; - } -} - -int usb_cdc_acm_init(void) -{ - LOG_DBG("Initializing USB Stack..."); - - /* Zephyr-Treiber registrieren. Verbraucht keinen Strom ohne VBUS. */ - int ret = usb_enable(usb_status_cb); - if (ret != 0) { - LOG_ERR("Failed to enable USB (%d)", ret); - return ret; - } - -#if DT_NODE_HAS_STATUS(DT_NODELABEL(cdc_acm_uart0), okay) - const struct device *cdc_dev = DEVICE_DT_GET(DT_NODELABEL(cdc_acm_uart0)); - - if (!device_is_ready(cdc_dev)) { - LOG_ERR("CDC ACM device not ready"); - return -ENODEV; - } - -#else - LOG_ERR("CDC ACM UART device not found in devicetree"); - return -ENODEV; -#endif - - LOG_DBG("USB Stack enabled and waiting for VBUS in hardware"); - return 0; + return dtr_active != 0U; } \ No newline at end of file diff --git a/firmware/src/usb.h b/firmware/src/usb.h deleted file mode 100644 index 0ca56ce..0000000 --- a/firmware/src/usb.h +++ /dev/null @@ -1,55 +0,0 @@ -#ifndef USB_CDC_ACM_H -#define USB_CDC_ACM_H - -/** - * @brief Initializes the USB CDC ACM device - * @return 0 on success, negative error code on failure - */ -int usb_cdc_acm_init(void); - -/** - * @brief Waits until data is available in the USB RX FIFO or the timeout expires - * @param timeout Maximum time to wait for data. Use K_FOREVER for infinite wait. - * @return true if data is available, false if timeout occurred - */ -bool usb_wait_for_data(k_timeout_t timeout); - -/** - * @brief Reads a single character from the USB RX FIFO - * @param c Pointer to store the read character - * @return true if a character was read, false if no data was available - */ -bool usb_read_byte(uint8_t *c); - -/** - * @brief Reads a block of data from the USB RX FIFO - * @param buf Buffer to store the read data - * @param max_len Maximum number of bytes to read - * @return Number of bytes read - */ -int usb_read_buffer(uint8_t *buf, size_t max_len); - -/** - * @brief Resumes the USB RX interrupt when all data has been read - */ -void usb_resume_rx(void); - -/** - * @brief Writes a single character to the USB TX FIFO - * @param c Character to write - */ -void usb_write_byte(uint8_t c); - -/** - * @brief Writes a block of data to the USB TX FIFO - * @param buf Buffer containing the data to write - * @param len Number of bytes to write - */ -int usb_write_buffer(const uint8_t *buf, size_t len); - -/** - * @brief Flushes the USB RX FIFO - */ -void usb_flush_rx(void); - -#endif // USB_CDC_ACM_H \ No newline at end of file diff --git a/firmware/src/utils.c b/firmware/src/utils.c index ccd75b4..3fb6e96 100644 --- a/firmware/src/utils.c +++ b/firmware/src/utils.c @@ -4,6 +4,8 @@ #include #include +#include + #if IS_ENABLED(CONFIG_SOC_SERIES_NRF52X) #include #elif IS_ENABLED(CONFIG_SOC_SERIES_STM32G0X) @@ -17,6 +19,7 @@ LOG_MODULE_REGISTER(utils, LOG_LEVEL_DBG); void reboot_with_status(uint8_t status) { + app_settings_save_pending_now(); #if IS_ENABLED(CONFIG_SOC_SERIES_NRF52X) /* Korrigierter Aufruf mit Register-Index 0 */ nrf_power_gpregret_set(NRF_POWER, REBOOT_STATUS_REG_IDX, (uint32_t)status); diff --git a/firmware/sysbuild/mcuboot.conf b/firmware/sysbuild/mcuboot.conf index 04ca385..ee3485f 100644 --- a/firmware/sysbuild/mcuboot.conf +++ b/firmware/sysbuild/mcuboot.conf @@ -1,2 +1,2 @@ CONFIG_LOG=y -CONFIG_MCUBOOT_LOG_LEVEL_INF=y \ No newline at end of file +CONFIG_MCUBOOT_LOG_LEVEL_INF=y diff --git a/sounds/sys/404 b/sounds/sys/404 new file mode 100644 index 0000000..3ffc5ad Binary files /dev/null and b/sounds/sys/404 differ diff --git a/sounds/sys/confirm b/sounds/sys/confirm new file mode 100644 index 0000000..6d7a28c Binary files /dev/null and b/sounds/sys/confirm differ diff --git a/sounds/sys/update b/sounds/sys/update new file mode 100644 index 0000000..a81c2f1 Binary files /dev/null and b/sounds/sys/update differ diff --git a/sounds/sys/voltest b/sounds/sys/voltest new file mode 100644 index 0000000..4c69bfa Binary files /dev/null and b/sounds/sys/voltest differ diff --git a/temp b/temp new file mode 100644 index 0000000..6d7a28c Binary files /dev/null and b/temp differ diff --git a/tool/buzz.py b/tool/buzz.py index 2d54146..b9e8b54 100644 --- a/tool/buzz.py +++ b/tool/buzz.py @@ -22,6 +22,10 @@ def main(): # Subparser für Befehle subparsers = parser.add_subparsers(dest="command", help="Verfügbare Befehle") + # Befehl: crc32 + crc32_parser = subparsers.add_parser("crc32", help="CRC32-Checksumme einer Datei oder eines Verzeichnisses berechnen") + crc32_parser.add_argument("path", help="Pfad der Datei auf dem Zielsystem") + # Befehl: flash_info flash_info_parser = subparsers.add_parser("flash_info", help="Informationen über den Flash-Speicher des Controllers abfragen") @@ -41,6 +45,21 @@ def main(): # Befehl: proto proto_parser = subparsers.add_parser("proto", help="Protokollversion des Controllers abfragen") + # Befehl: put_file + put_file_parser = subparsers.add_parser("put_file", help="Datei auf das Zielsystem hochladen") + put_file_parser.add_argument("source_path", help="Pfad der Datei auf dem lokalen System") + put_file_parser.add_argument("dest_path", help="Zielpfad auf dem Zielsystem") + put_file_parser.add_argument("-t", "--tags", help="Optionale JSON Tags für den Upload", type=str) + + # Befehl: get_tags + get_tags_parser = subparsers.add_parser("get_tags", help="Tags einer Datei anzeigen") + get_tags_parser.add_argument("path", help="Pfad der Datei auf dem Zielsystem") + + # Befehl: put_tags + put_tags_parser = subparsers.add_parser("put_tags", help="Tags schreiben") + put_tags_parser.add_argument("path", help="Pfad der Datei auf dem Zielsystem") + put_tags_parser.add_argument("json", help="JSON String (z.B. '{\"json\": {\"t\": \"Titel\"}}')") + put_tags_parser.add_argument("-o", "--overwrite", help="Alle bestehenden JSON-Tags vorher löschen", action="store_true") # Befehl: rename rename_parser = subparsers.add_parser("rename", help="Benennen Sie eine Datei oder einen Ordner auf dem Zielsystem um") rename_parser.add_argument("source_path", help="Aktueller Pfad der Datei/des Ordners auf dem Zielsystem") @@ -54,6 +73,32 @@ def main(): stat_parser = subparsers.add_parser("stat", help="Informationen zu einer Datei/Ordner") stat_parser.add_argument("path", help="Pfad auf dem Zielsystem") + # Befehl: put_fw + put_fw_parser = subparsers.add_parser("put_fw", help="Firmware-Image auf den Controller hochladen") + put_fw_parser.add_argument("file_path", help="Pfad zur Firmware-Datei auf dem lokalen System") + + # Befehl: confirm_fw + confirm_fw_parser = subparsers.add_parser("confirm_fw", help="Bestätigt ein als 'Testing' markiertes Firmware-Image, damit es beim permanent wird") + + # Befehl: reboot + reboot_parser = subparsers.add_parser("reboot", help="Neustart des Controllers") + + # Befehl: play + play_parser = subparsers.add_parser("play", help="Startet die Wiedergabe einer Datei") + play_parser.add_argument("path", help="Pfad der Datei auf dem Zielsystem") + play_parser.add_argument("-i", "--interrupt", help="Sofortige Wiedergabe (Interrupt)", action="store_true") + + # Befehl: stop + stop_parser = subparsers.add_parser("stop", help="Stoppt die aktuelle Wiedergabe") + + # Befehl: set + set_parser = subparsers.add_parser("set", help="System-Einstellung setzen") + set_parser.add_argument("key", help="Schlüssel (z.B. audio/vol)") + set_parser.add_argument("value", help="Wert") + + # Befehl: get + get_parser = subparsers.add_parser("get", help="System-Einstellung auslesen") + get_parser.add_argument("key", help="Schlüssel (z.B. audio/vol)") args = parser.parse_args() if not args.command: @@ -103,7 +148,12 @@ def main(): try: bus.open() - if args.command == "get_file": + if args.command == "crc32": + from core.cmd.crc32 import crc32 + cmd = crc32(bus) + result = cmd.get(args.path) + cmd.print(result, args.path) + elif args.command == "get_file": from core.cmd.get_file import get_file cmd = get_file(bus) result = cmd.get(args.source_path, args.dest_path) @@ -128,6 +178,11 @@ def main(): cmd = proto(bus) result = cmd.get() cmd.print(result) + elif args.command == "put_file": + from core.cmd.put_file import put_file + cmd = put_file(bus) + result = cmd.get(args.source_path, args.dest_path) + cmd.print(result) elif args.command == "rename": from core.cmd.rename import rename cmd = rename(bus) @@ -143,7 +198,56 @@ def main(): cmd = stat(bus) result = cmd.get(args.path) cmd.print(result, args.path) - + elif args.command == "put_file": + from core.cmd.put_file import put_file + cmd = put_file(bus) + result = cmd.get(args.source_path, args.dest_path, cli_tags_json=args.tags) + cmd.print(result) + elif args.command == "get_tags": + from core.cmd.get_tags import get_tags + cmd = get_tags(bus) + result = cmd.get(args.path) + cmd.print(result, args.path) + elif args.command == "put_tags": + from core.cmd.put_tags import put_tags + cmd = put_tags(bus) + result = cmd.get(args.path, args.json, overwrite=args.overwrite) + cmd.print(result, args.path) + elif args.command == "confirm_fw": + from core.cmd.fw_confirm import fw_confirm + cmd = fw_confirm(bus) + result = cmd.get() + cmd.print(result) + elif args.command == "put_fw": + from core.cmd.put_fw import put_fw + cmd = put_fw(bus) + result = cmd.get(args.file_path) + cmd.print(result) + elif args.command == "reboot": + from core.cmd.reboot import reboot + cmd = reboot(bus) + result = cmd.get() + cmd.print(result) + elif args.command == "play": + from core.cmd.play import play + cmd = play(bus) + result = cmd.get(args.path, interrupt=args.interrupt) + cmd.print(result, args.path, interrupt=args.interrupt) + elif args.command == "stop": + from core.cmd.stop import stop + cmd = stop(bus) + result = cmd.get() + cmd.print(result) + elif args.command == "set": + from core.cmd.set_setting import set_setting + cmd = set_setting(bus) + result = cmd.get(args.key, args.value) + cmd.print(result, args.key, args.value) + elif args.command == "get": + from core.cmd.get_setting import get_setting + cmd = get_setting(bus) + result = cmd.get(args.key) + cmd.print(result, args.key) finally: bus.close() diff --git a/tool/core/cmd/crc32.py b/tool/core/cmd/crc32.py new file mode 100644 index 0000000..384af1e --- /dev/null +++ b/tool/core/cmd/crc32.py @@ -0,0 +1,36 @@ +# tool/core/cmd/crc32.py +import struct +from core.utils import console, console_err +from core.protocol import COMMANDS, ERRORS + +class crc32: + def __init__(self, bus): + self.bus = bus + + def get(self, path: str): + path_bytes = path.encode('utf-8') + payload = struct.pack('B', len(path_bytes)) + path_bytes + self.bus.send_request(COMMANDS['crc_32'], payload) + + # 1 Byte Type + 4 Byte Size = 5 + data = self.bus.receive_response(length=8, timeout=5) + + if not data or data.get('type') == 'error': + return None + + payload = data['data'] + crc_value = struct.unpack(' 0: + console.print(f" • Dauer: [info]{result['duration']:.2f} s[/info]") \ No newline at end of file diff --git a/tool/core/cmd/get_setting.py b/tool/core/cmd/get_setting.py new file mode 100644 index 0000000..6ac491d --- /dev/null +++ b/tool/core/cmd/get_setting.py @@ -0,0 +1,37 @@ +import struct +from core.utils import console +from core.protocol import COMMANDS + +class get_setting: + def __init__(self, bus): + self.bus = bus + + def get(self, key: str): + key_bytes = key.encode('utf-8') + payload = struct.pack('B', len(key_bytes)) + key_bytes + + self.bus.send_request(COMMANDS['get_setting'], payload) + + # varlen_params=1 liest exakt 1 Byte Länge + entsprechend viele Datenbytes + data = self.bus.receive_response(length=0, varlen_params=1) + + if not data or data.get('type') == 'error': + return None + + raw = data['data'] + val_len = raw[0] + val_buf = raw[1:1+val_len] + + # Binärdaten zurück in Python-Typen parsen + if key == "audio/vol" and val_len == 1: + return struct.unpack(' 0: + console.print(f" • Dauer: [info]{result['duration']:.2f} s[/info]") \ No newline at end of file diff --git a/tool/core/cmd/put_fw.py b/tool/core/cmd/put_fw.py new file mode 100644 index 0000000..777226a --- /dev/null +++ b/tool/core/cmd/put_fw.py @@ -0,0 +1,89 @@ +import struct +import zlib +from pathlib import Path +from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, DownloadColumn, TransferSpeedColumn, TimeRemainingColumn +from core.utils import console, console_err +from core.protocol import COMMANDS + +class put_fw: + def __init__(self, bus): + self.bus = bus + + def get(self, file_path: str): + try: + p = Path(file_path) + if not p.exists() or not p.is_file(): + console_err.print(f"Fehler: Firmware-Datei existiert nicht: {file_path}") + return None + + file_size = p.stat().st_size + with open(p, 'rb') as f: + file_data = f.read() + except Exception as e: + console_err.print(f"Lese-Fehler: {e}") + return None + + # 1. Schritt: Löschvorgang mit minimalem Feedback + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + BarColumn(), + console=console, + transient=True + ) as progress: + erase_task = progress.add_task("Lösche Firmware Slot...", total=None) + + payload = struct.pack(' len(file_data) or total_size < cls.FOOTER_SIZE: + return file_data, [] + + audio_limit = len(file_data) - total_size + audio_data = file_data[:audio_limit] + tag_data = file_data[audio_limit:-cls.FOOTER_SIZE] + + tlvs = cls.parse_tlvs(tag_data) + return audio_data, tlvs + + @classmethod + def parse_tlvs(cls, tag_data: bytes): + """Parst einen rohen TLV-Byteblock in eine Liste von Dictionaries.""" + tlvs = [] + pos = 0 + while pos + 4 <= len(tag_data): + t, i, length = struct.unpack(' len(tag_data): + break # Korrupt + val = tag_data[pos:pos+length] + tlvs.append({'type': t, 'index': i, 'value': val}) + pos += length + return tlvs + + @classmethod + def build_blob(cls, tlvs: list): + """Baut aus einer TLV-Liste den fertigen Byte-Blob inklusive Footer.""" + # Sortierung: Type 0x00 (System) zwingend nach vorne + tlvs = sorted(tlvs, key=lambda x: x['type']) + + payload = b"" + for tlv in tlvs: + payload += struct.pack(' import { onMount } from 'svelte'; + import { bus } from '../lib/bus/SerialBus'; import { buzzer } from '../lib/buzzerStore'; - import { connectToPort, disconnectBuzzer,initSerialListeners } from '../lib/buzzerActions'; - import { PlugsIcon, PlugsConnectedIcon } from 'phosphor-svelte'; + import { GetProtocolCommand } from '../lib/protocol/commands/GetProtocol'; + import { addToast } from '../lib/toastStore'; + import { + PlugsIcon, + PlugsConnectedIcon, + CaretDownIcon, + BluetoothIcon, + TrashIcon, + PlusCircleIcon + } from 'phosphor-svelte'; + import { slide } from 'svelte/transition'; + import { initializeBuzzer } from '../lib/buzzerActions'; - const BUZZER_FILTER = [ - { usbVendorId: 0x2fe3, usbProductId: 0x0001 }, - ]; - onMount(() => { - initSerialListeners(); - }); + const BUZZER_FILTER = [{ usbVendorId: 0x1209, usbProductId: 0xEDED }]; + + let showMenu = false; + let menuElement: HTMLElement; - async function handleConnectClick() { - try { - if ($buzzer.connected) { - console.log("Trenne verbindung zum aktuellen Buzzer..."); - await disconnectBuzzer(); - console.log("Verbindung getrennt"); - } - - const port = await navigator.serial.requestPort({ filters: BUZZER_FILTER }); - console.log("Port ausgewählt, versuche Verbindung.", port.getInfo()); - await connectToPort(port); - } catch (e) { - // Verhindert das Error-Logging, wenn der User einfach nur "Abbrechen" klickt - if (e instanceof Error && e.name === 'NotFoundError') { - console.log("Keine Verbindung ausgewählt, Abbruch durch Nutzer."); - return; - } - console.error("Verbindung abgebrochen", e); + // Schließt das Menü bei Klick außerhalb + function handleOutsideClick(event: MouseEvent) { + if (showMenu && menuElement && !menuElement.contains(event.target as Node)) { + showMenu = false; } } + + async function connectTo(port: SerialPort) { + try { + await bus.connect(port); + // Kurze Pause für die Hardware-Bereitschaft + await new Promise(r => setTimeout(r, 100)); + + // Logische Initialisierung starten + await initializeBuzzer(); + + } catch (e: any) { + console.error("Port-Fehler:", e); + } + } + + async function handleMainAction() { + if ($buzzer.connected) { + await bus.disconnect(); + buzzer.update(s => ({ ...s, connected: false })); + return; + } + + const ports = await navigator.serial.getPorts(); + if (ports.length > 0) { + await connectTo(ports[0]); + } else { + await pairNewDevice(); + } + } + + async function pairNewDevice() { + showMenu = false; + try { + const port = await navigator.serial.requestPort({ filters: BUZZER_FILTER }); + await connectTo(port); + } catch (e) { + console.log("Pairing abgebrochen"); + } + } + + async function forgetDevice() { + showMenu = false; + const ports = await navigator.serial.getPorts(); + for (const port of ports) { + if ('forget' in port) { + await (port as any).forget(); + } + } + if ($buzzer.connected) { + await bus.disconnect(); + buzzer.update(s => ({ ...s, connected: false })); + } + addToast("Geräte entkoppelt", "info"); + } + + onMount(() => { + window.addEventListener('click', handleOutsideClick); + return () => window.removeEventListener('click', handleOutsideClick); + }); - + + + + {#if showMenu} +
+
+ + +
+ + +
+
{/if} - \ No newline at end of file + \ No newline at end of file diff --git a/webpage/src/components/DeviceInfo.svelte b/webpage/src/components/DeviceInfo.svelte index 547925a..d1c715c 100644 --- a/webpage/src/components/DeviceInfo.svelte +++ b/webpage/src/components/DeviceInfo.svelte @@ -1,17 +1,25 @@ - -
-

- Firmware: {$buzzer.version} -

-

- Protocol: {$buzzer.protocol} -

-

- Status: - {$buzzer.connected ? 'Confirmed' : 'Disconnected'} - -

-
\ No newline at end of file +
+
+ +
+

+ Device Info +

+
+

+ Firmware: {$buzzer.version} +

+

+ Protocol: {$buzzer.protocol} +

+

+ Status: + {$buzzer.connected ? 'Confirmed' : 'Disconnected'} + +

+
+
\ No newline at end of file diff --git a/webpage/src/components/DiskUsage.svelte b/webpage/src/components/DiskUsage.svelte index 1469fda..c0dfeae 100644 --- a/webpage/src/components/DiskUsage.svelte +++ b/webpage/src/components/DiskUsage.svelte @@ -14,7 +14,7 @@ $: isDisconnected = !$buzzer.connected; -
+
diff --git a/webpage/src/components/FileRow.svelte b/webpage/src/components/FileRow.svelte index 8d0c01d..dc4bfdb 100644 --- a/webpage/src/components/FileRow.svelte +++ b/webpage/src/components/FileRow.svelte @@ -1,10 +1,31 @@
+ + diff --git a/webpage/src/components/FileStorage.svelte b/webpage/src/components/FileStorage.svelte index d1240d0..ba42810 100644 --- a/webpage/src/components/FileStorage.svelte +++ b/webpage/src/components/FileStorage.svelte @@ -1,11 +1,11 @@ diff --git a/webpage/src/lib/bus/SerialBus.ts b/webpage/src/lib/bus/SerialBus.ts new file mode 100644 index 0000000..d806764 --- /dev/null +++ b/webpage/src/lib/bus/SerialBus.ts @@ -0,0 +1,134 @@ +import { SYNC_SEQ, FrameType } from '../protocol/constants'; +import { buzzer } from '../buzzerStore'; + +class SerialBus { + public port: SerialPort | null = null; + private reader: ReadableStreamDefaultReader | null = null; + private internalBuffer: Uint8Array = new Uint8Array(0); + + async connect(port: SerialPort) { + if (this.port) await this.disconnect(); + await port.open({ baudRate: 115200 }); + this.port = port; + this.internalBuffer = new Uint8Array(0); + this.reader = null; + port.addEventListener('disconnect', () => { + console.warn("Hardware-Verbindung verloren!"); + this.disconnect(); + buzzer.update(s => ({ ...s, connected: false })); + }); + (window as any).buzzerBus = this; + } + + // Hilfsmethode: Stellt sicher, dass wir einen aktiven Reader haben ohne zu crashen + private async ensureReader() { + if (!this.port?.readable) throw new Error("Port nicht lesbar"); + if (!this.reader) { + this.reader = this.port.readable.getReader(); + } + } + + async sendRequest(cmd: number, payload: Uint8Array = new Uint8Array(0)) { + if (!this.port?.writable) throw new Error("Port nicht bereit"); + const writer = this.port.writable.getWriter(); + const header = new Uint8Array([FrameType.REQUEST, cmd]); + const frame = new Uint8Array(SYNC_SEQ.length + header.length + payload.length); + frame.set(SYNC_SEQ, 0); + frame.set(header, SYNC_SEQ.length); + frame.set(payload, SYNC_SEQ.length + header.length); + await writer.write(frame); + writer.releaseLock(); + } + + async waitForSync(timeoutMs = 2000): Promise { + await this.ensureReader(); + const startTime = Date.now(); + + while (Date.now() - startTime < timeoutMs) { + // 1. Zuerst im Puffer schauen (verhindert Datenverlust zwischen Frames!) + for (let i = 0; i <= this.internalBuffer.length - SYNC_SEQ.length; i++) { + if (this.internalBuffer[i] === SYNC_SEQ[0] && this.internalBuffer[i+1] === SYNC_SEQ[1] && + this.internalBuffer[i+2] === SYNC_SEQ[2] && this.internalBuffer[i+3] === SYNC_SEQ[3]) { + this.internalBuffer = this.internalBuffer.subarray(i + SYNC_SEQ.length); + return true; + } + } + + // 2. Neue Daten lesen + const { value, done } = await this.reader!.read(); + if (done) break; + if (value) { + const next = new Uint8Array(this.internalBuffer.length + value.length); + next.set(this.internalBuffer); + next.set(value, this.internalBuffer.length); + this.internalBuffer = next; + } + } + return false; + } + + async readExact(len: number): Promise { + await this.ensureReader(); + while (this.internalBuffer.length < len) { + const { value, done } = await this.reader!.read(); + if (done || !value) throw new Error("Stream closed"); + const next = new Uint8Array(this.internalBuffer.length + value.length); + next.set(this.internalBuffer); + next.set(value, this.internalBuffer.length); + this.internalBuffer = next; + } + const res = this.internalBuffer.subarray(0, len); + this.internalBuffer = this.internalBuffer.subarray(len); + return res; + } + + public releaseReadLock() { + if (this.reader) { + this.reader.releaseLock(); + this.reader = null; + } + } + + async disconnect() { + this.releaseReadLock(); + if (this.port) { + try { await this.port.close(); } catch (e) {} + this.port = null; + } + this.internalBuffer = new Uint8Array(0); + } +} + +const existingBus = typeof window !== 'undefined' ? (window as any).buzzerBus : null; + +export const bus: SerialBus = existingBus || new SerialBus(); + +if (typeof window !== 'undefined') { + (window as any).buzzerBus = bus; + + (window as any).initDebug = async () => { + console.log("🚀 Lade Debug-Kommandos..."); + + // Nutze hier am besten absolute Pfade ab /src/ + const [proto, settings, list, play, flash] = await Promise.all([ + import('../protocol/commands/GetProtocol.ts'), + import('../protocol/commands/GetSettings.ts'), + import('../protocol/commands/ListDir.ts'), + import('../protocol/commands/PlayFile.ts'), // Pfad prüfen! + import('../protocol/commands/GetFlashInfo.ts') // Pfad prüfen! + ]); + + (window as any).GetProtocolCommand = proto.GetProtocolCommand; + (window as any).GetSettingCommand = settings.GetSettingCommand; + (window as any).ListDirCommand = list.ListDirCommand; + (window as any).PlayFileCommand = play.PlayFileCommand; + (window as any).GetFlashInfoCommand = flash.GetFlashInfoCommand; + + console.log("✅ Alle Commands geladen und an window gebunden."); + }; + + (window as any).run = async (CommandClass: any, ...args: any[]) => { + const cmd = new CommandClass(); + return await cmd.execute(...args); + }; +} \ No newline at end of file diff --git a/webpage/src/lib/buzzerActions.ts b/webpage/src/lib/buzzerActions.ts index 64c515c..18a21ff 100644 --- a/webpage/src/lib/buzzerActions.ts +++ b/webpage/src/lib/buzzerActions.ts @@ -1,362 +1,46 @@ -import { buzzer } from './buzzerStore'; -import { get } from 'svelte/store'; -import { addToast } from './toastStore'; - -let isConnecting = false; -const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); - -type Task = { - command: string; - priority: number; // 0 = Hintergrund, 1 = User-Aktion - resolve: (lines: string[]) => void; - key?: string; -}; - -class SerialQueue { - private queue: Task[] = []; - private isProcessing = false; - private port: SerialPort | null = null; - - private reader: ReadableStreamDefaultReader | null = null; - private writer: WritableStreamDefaultWriter | null = null; - - setPort(port: SerialPort | null) { this.port = port; } - - async add(command: string, priority = 1, key?: string): Promise { - if (key) { - this.queue = this.queue.filter(t => t.key !== key); - } - - return new Promise((resolve) => { - const task = { command, priority, resolve, key }; - if (priority === 1) { - const lastUserTaskIndex = this.queue.findLastIndex(t => t.priority === 1); - this.queue.splice(lastUserTaskIndex + 1, 0, task); - } else { - this.queue.push(task); - } - this.process(); - }); - } - - private async process() { - if (this.isProcessing || !this.port || this.queue.length === 0) return; - this.isProcessing = true; - - const task = this.queue.shift()!; - try { - const encoder = new TextEncoder(); - const decoder = new TextDecoder(); - - // Reader und Writer in der Instanz speichern - this.writer = this.port.writable.getWriter(); - this.reader = this.port.readable.getReader(); - - await this.writer.write(encoder.encode(task.command + "\n")); - - // Writer sofort wieder freigeben nach dem Senden - this.writer.releaseLock(); - this.writer = null; - - let raw = ""; - while (true) { - // Hier könnte die Queue hängen bleiben, wenn das Gerät nicht antwortet - const { value, done } = await this.reader.read(); - if (done) break; - raw += decoder.decode(value); - if (raw.includes("OK") || raw.includes("ERR")) break; - } - - this.reader.releaseLock(); - this.reader = null; - - const lines = raw.split('\n').map(l => l.trim()).filter(l => l); - const errorLine = lines.find(l => l.startsWith("ERR")); - if (errorLine) addToast(`Gerätefehler: ${errorLine}`, 'error', 5000); - - task.resolve(lines.filter(l => l !== "OK" && !l.startsWith("ERR") && !l.startsWith(task.command))); - } catch (e) { - // Im Fehlerfall Locks sicher aufheben - this.cleanupLocks(); - if (e instanceof Error && e.name !== 'AbortError') { - console.error("Queue Error:", e); - } - } finally { - this.isProcessing = false; - this.process(); - } - } - - // Hilfsmethode zum Aufräumen der Sperren - private cleanupLocks() { - if (this.reader) { - try { this.reader.releaseLock(); } catch { } - this.reader = null; - } - if (this.writer) { - try { this.writer.releaseLock(); } catch { } - this.writer = null; - } - } - - async close() { - this.queue = []; - if (this.port) { - try { - // Erst die Streams abbrechen, um laufende Reads zu beenden - if (this.reader) { - await this.reader.cancel(); - } - if (this.writer) { - await this.writer.abort(); - } - // Dann die Locks freigeben - this.cleanupLocks(); - await this.port.close(); - // console.log("Port erfolgreich geschlossen"); - } catch (e) { - console.error("Port-Fehler beim Schließen:", e); - this.cleanupLocks(); - } - this.port = null; - } - this.isProcessing = false; - } -} - -const queue = new SerialQueue(); - /** - * Initialisiert die globalen Serial-Listener (aufgerufen beim Start der App) + * Initialisiert den Buzzer nach dem physikalischen Verbindungsaufbau. */ -export function initSerialListeners() { - if (typeof navigator === 'undefined' || !navigator.serial) return; - - // 1. Wenn ein bereits gekoppeltes Gerät eingesteckt wird - navigator.serial.addEventListener('connect', (event) => { - // console.log('Neues Gerät erkannt, starte Auto-Connect...'); - autoConnect(); - }); - - // Beim Laden der Seite prüfen, ob wir bereits Zugriff auf Geräte haben - autoConnect(); -} - -/** - * Versucht eine Verbindung zu bereits gekoppelten Geräten herzustellen - */ -export async function autoConnect() { - if (typeof navigator === 'undefined' || !navigator.serial) return; - - const ports = await navigator.serial.getPorts(); - if (ports.length > 0) { - const port = ports[0]; - const retryDelays = [100, 500]; - - // Erster Versuch + 2 Retries = max 3 Versuche - for (let i = 0; i <= retryDelays.length; i++) { - try { - // console.log("Auto-Connect Versuch mit Port:", port.getInfo()); - await connectToPort(port); - return; // Erfolg! - } catch (e) { - if (i < retryDelays.length) { - // console.log(`Reconnect Versuch ${i + 1} fehlgeschlagen, warte ${retryDelays[i]}ms...`); - await delay(retryDelays[i]); - } else { - console.error('Auto-Connect nach Retries endgültig fehlgeschlagen.'); - } - } - } +export async function initializeBuzzer() { + try { + const version = await new GetProtocolCommand().execute(); + + if (version !== null) { + buzzer.update(s => ({ ...s, connected: true, protocol: version })); + + // FIX 1: Flash-Info muss auch beim Start geladen werden! + await refreshFileList(); + await refreshFlashInfo(); + + addToast(`Buzzer bereit (v${version})`, 'success'); + return true; } + } catch (e: any) { + console.error("Initialisierung fehlgeschlagen:", e); + addToast(`Fehler: ${e.message}`, "error"); + await bus.disconnect(); + buzzer.update(s => ({ ...s, connected: false })); + } + return false; } -/** - * Kernfunktion für den Verbindungsaufbau - */ -export async function connectToPort(port: SerialPort) { - if (isConnecting || get(buzzer).connected) return; - isConnecting = true; - - try { - // console.log("Versuche Verbindung mit Port:", port.getInfo()); - await port.open({ baudRate: 115200 }); - await delay(100); - setActivePort(port); - - try { - // Validierung: Antwortet das Teil auf "info"? - const success = await Promise.race([ - updateDeviceInfo(port), - new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), 1500)) - ]); - - if (!success) throw new Error("Kein Buzzer"); - - port.addEventListener('disconnect', () => { - addToast("Buzzer-Verbindung verloren!", "warning"); - handleDisconnect(); - }); - - buzzer.update(s => ({ ...s, connected: true })); - addToast("Buzzer erfolgreich verbunden", "success"); - - await refreshFileList(); - } catch (validationError) { - addToast("Buzzer-Validierung fehlgeschlagen!", "error"); - await disconnectBuzzer(); - - try { - if ('forget' in port) { // Check für Browser-Support - await (port as any).forget(); - console.log("Gerät wurde erfolgreich entkoppelt."); - } - } catch (forgetError) { - console.error("Entkoppeln fehlgeschlagen:", forgetError); - } - - throw new Error("Device ist kein gültiger Buzzer"); - throw validationError; // Fehler an den äußeren Block weitergeben - } - } catch (e) { - setActivePort(null); - // Hier landen wir, wenn der User den Port-Dialog abbricht oder die Validierung fehlschlägt - throw e; - } finally { - isConnecting = false; +export async function refreshFlashInfo() { + try { + const flashInfo = await new GetFlashInfoCommand().execute(); + if (flashInfo) { + const totalSize = (flashInfo.total_size / (1024 * 1024)); + const freeSize = (flashInfo.free_size / (1024 * 1024)); + const fwSlotSize = (flashInfo.fw_slot_size / 1024); + const maxPathLength = flashInfo.max_path_length; + + buzzer.update(s => ({ + ...s, + storage: { total: totalSize, available: freeSize }, // FIX 2: "storage" korrekt geschrieben + fw_slot_size: fwSlotSize, + max_path_length: maxPathLength + })); } -} - -function handleDisconnect() { - setActivePort(null); - buzzer.update(s => ({ - ...s, - connected: false, - files: [] // Liste leeren, da Gerät weg - })); -} - -// --- EXPORTE --- - -export function setActivePort(port: SerialPort | null) { - queue.setPort(port); -} - -export async function disconnectBuzzer() { - await queue.close(); - handleDisconnect(); -} - -export async function updateDeviceInfo(port: SerialPort): Promise { - const lines = await queue.add("info", 1); - if (lines.length > 0) { - const parts = lines[0].split(';'); - if (parts.length >= 6) { - const pageSize = parseInt(parts[2]); - const totalPages = parseInt(parts[3]); - const availablePages = parseInt(parts[4]); - - // MB Berechnung mit dem korrekten Divisor (1024 * 1024) - const totalMB = (totalPages * pageSize) / 1048576; - const availableMB = (availablePages * pageSize) / 1048576; - - buzzer.update(s => ({ - ...s, - version: parts[1], - protocol: parseInt(parts[0]), - storage: { ...s.storage, total: totalMB, available: availableMB } - })); - return true; // Validierung erfolgreich - } - } - return false; // Keine gültigen Daten erhalten -} - -export async function refreshFileList() { - let totalSystemBytes = 0; - let totalAudioBytes = 0; - - // 1. System-Größe abfragen (nur Summieren, keine Liste speichern) - const syslines = await queue.add("ls /lfs/sys", 1, 'ls'); - syslines.forEach(line => { - const parts = line.split(','); - if (parts.length >= 2) { - totalSystemBytes += parseInt(parts[1]); - } - }); - - // 2. Audio-Files abfragen und Liste für das UI erstellen - const lines = await queue.add("ls /lfs/a", 1, 'ls'); - const audioFiles = lines.map(line => { - const parts = line.split(','); - if (parts.length < 3) return null; - const size = parseInt(parts[1]); - totalAudioBytes += size; - - return { - name: parts[2], - size: (size / 1024).toFixed(1) + " KB", - crc32: 0, - isSystem: false - }; - }).filter(f => f !== null) as any[]; - - // 3. Den Store mit MB-Werten aktualisieren - buzzer.update(s => { - // Konvertierung in MB (1024 * 1024 = 1048576) - const audioMB = totalAudioBytes / 1048576; - const sysMB = totalSystemBytes / 1048576; - const usedTotalMB = s.storage.total - s.storage.available; - const unknownMB = Math.max(0, usedTotalMB - audioMB - sysMB); - console.log(`Storage: Total ${s.storage.total} MB, Used ${usedTotalMB.toFixed(2)} MB, Audio ${audioMB.toFixed(2)} MB, System ${sysMB.toFixed(2)} MB, Unknown ${unknownMB.toFixed(2)} MB`); - return { - ...s, - files: audioFiles, - storage: { - ...s.storage, - usedSys: sysMB, - usedAudio: audioMB, - unknown: unknownMB - } - }; - }); - - startBackgroundCrcCheck(); -} - -async function startBackgroundCrcCheck() { - const currentFiles = get(buzzer).files; - for (const file of currentFiles) { - if (true) {//(!file.crc32) { - const tagresponse = await queue.add(`gett /lfs/a/${file.name}`, 0); - if (tagresponse.length > 0) { - console.log(`Tag für ${file.name}:`, tagresponse[0]); - } - const response = await queue.add(`check /lfs/a/${file.name}`, 0); - if (response.length > 0) { - const match = response[0].match(/0x([0-9a-fA-F]+)/); - if (match) { - const crc = parseInt(match[1], 16); - updateFileCrc(file.name, crc); - } - } - } - } -} - -function updateFileCrc(name: string, crc: number) { - buzzer.update(s => ({ - ...s, - files: s.files.map(f => f.name === name ? { ...f, crc32: crc } : f) - })); -} - -export async function playFile(filename: string) { - return queue.add(`play /lfs/a/${filename}`, 1, 'play'); -} - -export async function deleteFile(filename: string) { - if (!confirm(`Datei ${filename} wirklich löschen?`)) return; - await queue.add(`rm /lfs/a/${filename}`, 1); - await refreshFileList(); -} \ No newline at end of file + } catch (e) { // FIX 3: try/catch Block sauber schließen + console.error("Fehler beim Abrufen der Flash-Info:", e); + } +} // Funktion schließen \ No newline at end of file diff --git a/webpage/src/lib/buzzerStore.ts b/webpage/src/lib/buzzerStore.ts index 7921844..9b14a08 100644 --- a/webpage/src/lib/buzzerStore.ts +++ b/webpage/src/lib/buzzerStore.ts @@ -2,15 +2,18 @@ import { writable } from 'svelte/store'; export const buzzer = writable({ connected: false, - version: 'v0.0.0', protocol: 0, - build: 'unknown', + version: 'v0.0.0', + kernel_version: 'v0.0.0', storage: { - total: 8.0, // 8 MB Flash laut Spezifikation + total: 8.0, available: 0.0, - unknown: 8.0, usedSys: 0.0, - usedAudio: 0.0 + usedAudio: 0.0, + unknown: 0.0 }, - files: [] as {name: string, size: string, crc32: number, isSystem: boolean, isSynced: boolean}[] + max_path_length: 15, + fw_slot_size: 0, + + files: [] as {name: string, size: string, crc32: number | null, isSystem: boolean}[] }); \ No newline at end of file diff --git a/webpage/src/lib/protocol/commands/GetFlashInfo.ts b/webpage/src/lib/protocol/commands/GetFlashInfo.ts new file mode 100644 index 0000000..ecb3041 --- /dev/null +++ b/webpage/src/lib/protocol/commands/GetFlashInfo.ts @@ -0,0 +1,49 @@ +import { bus } from '../../bus/SerialBus'; +import { Command, FrameType } from '../constants'; +import { BinaryUtils } from '../../utils/BinaryUtils'; + +export interface FlashInfo { + total_size: number; // in Bytes + free_size: number; // in Bytes + fw_slot_size: number; // in Bytes + ext_flash_erase_size: number; // in Bytes + int_flash_erase_size: number; // in Bytes + max_path_length: number; +} + +export class GetFlashInfoCommand { + async execute(): Promise { + try { + await bus.sendRequest(Command.GET_FLASH_INFO); + + if (await bus.waitForSync()) { + const typeArr = await bus.readExact(1); + if (typeArr[0] === FrameType.RESPONSE) { + // Wir lesen exakt 21 Bytes für die Flash-Info + const data = await bus.readExact(21); + const pageSize = BinaryUtils.readUint32LE(data.subarray(0, 4)); + const totalSize = BinaryUtils.readUint32LE(data.subarray(4, 8)) * pageSize; + const freeSize = BinaryUtils.readUint32LE(data.subarray(8, 12)) * pageSize; // Aktuell haben wir keine Info über belegten Speicher, also annehmen, dass alles frei ist + const fwSlotSize = BinaryUtils.readUint32LE(data.subarray(12, 16)); + const extEraseSize = BinaryUtils.readUint16LE(data.subarray(16, 18)); + const intEraseSize = BinaryUtils.readUint16LE(data.subarray(18, 20)); + const maxPathLength = data[20]; + console.log("Flash Info:", { pageSize, totalSize, freeSize, fwSlotSize, extEraseSize, intEraseSize, maxPathLength }); + return { + total_size: totalSize, + free_size: totalSize, // Aktuell haben wir keine Info über belegten Speicher, also annehmen, dass alles frei ist + fw_slot_size: fwSlotSize, + ext_flash_erase_size: extEraseSize, + int_flash_erase_size: intEraseSize, + max_path_length: maxPathLength + }; + } + } + } catch (e) { + console.error("GetFlashInfoCommand failed:", e); + } finally { + bus.releaseReadLock(); + } + return null; + } +} \ No newline at end of file diff --git a/webpage/src/lib/protocol/commands/GetProtocol.ts b/webpage/src/lib/protocol/commands/GetProtocol.ts new file mode 100644 index 0000000..56203b3 --- /dev/null +++ b/webpage/src/lib/protocol/commands/GetProtocol.ts @@ -0,0 +1,26 @@ +import { bus } from '../../bus/SerialBus'; +import { Command, FrameType } from '../constants'; +import { BinaryUtils } from '../../utils/BinaryUtils'; + +export class GetProtocolCommand { + async execute(): Promise { + try { + await bus.sendRequest(Command.GET_PROTOCOL_VERSION); + + if (await bus.waitForSync()) { + const typeArr = await bus.readExact(1); + if (typeArr[0] === FrameType.RESPONSE) { + // Wir lesen exakt 2 Bytes für die Version + const data = await bus.readExact(2); + // Nutze die neue Hilfsfunktion + return BinaryUtils.readUint16LE(data); + } + } + } catch (e) { + console.error("GetProtocolCommand failed:", e); + } finally { + bus.releaseReadLock(); + } + return null; + } +} \ No newline at end of file diff --git a/webpage/src/lib/protocol/commands/GetSettings.ts b/webpage/src/lib/protocol/commands/GetSettings.ts new file mode 100644 index 0000000..dc1b88b --- /dev/null +++ b/webpage/src/lib/protocol/commands/GetSettings.ts @@ -0,0 +1,37 @@ +import { bus } from '../../bus/SerialBus'; +import { Command, FrameType } from '../constants'; +import { BinaryUtils } from '../../utils/BinaryUtils'; + +export class GetSettingCommand { + async execute(key: string): Promise { + try { + const keyBuf = new TextEncoder().encode(key); + const payload = new Uint8Array(1 + keyBuf.length); + payload[0] = keyBuf.length; + payload.set(keyBuf, 1); + + await bus.sendRequest(Command.GET_SETTING, payload); + + if (await bus.waitForSync()) { + const typeArr = await bus.readExact(1); + if (typeArr[0] === FrameType.RESPONSE) { + const lenArr = await bus.readExact(1); + const valLen = lenArr[0]; + const data = await bus.readExact(valLen); + + // Typ-Konvertierung analog zu C/Python + if (key === "audio/vol" || key === "play/norepeat") { + return data[0] === 1 ? true : (key === "audio/vol" ? data[0] : false); + } else if (key === "settings/storage_interval") { + return BinaryUtils.readUint16LE(data); + } + } + } + } catch (e) { + console.error("GetSetting failed:", e); + } finally { + bus.releaseReadLock(); + } + return null; + } +} \ No newline at end of file diff --git a/webpage/src/lib/protocol/commands/ListDir.ts b/webpage/src/lib/protocol/commands/ListDir.ts new file mode 100644 index 0000000..46bce64 --- /dev/null +++ b/webpage/src/lib/protocol/commands/ListDir.ts @@ -0,0 +1,43 @@ +// src/lib/protocol/commands/ListDir.ts +import { bus } from '../../bus/SerialBus'; +import { Command, FrameType } from '../constants'; +import { BinaryUtils } from '../../utils/BinaryUtils'; + +export class ListDirCommand { + async execute(path: string) { + try { + const p = new TextEncoder().encode(path); + const req = new Uint8Array(p.length + 1); + req[0] = p.length; req.set(p, 1); + + await bus.sendRequest(Command.LIST_DIR, req); + const entries = []; + + while (true) { + // Wichtig: Wir rufen waitForSync ohne releaseReadLock zwischendurch auf! + if (!(await bus.waitForSync())) break; + + const type = (await bus.readExact(1))[0]; + if (type === FrameType.LIST_START) continue; + if (type === FrameType.LIST_END) { + const expected = BinaryUtils.readUint16LE(await bus.readExact(2)); + console.log(`Erwartet: ${expected}, Erhalten: ${entries.length}`); + return entries; + } + if (type === FrameType.LIST_CHUNK) { + const len = BinaryUtils.readUint16LE(await bus.readExact(2)); + const data = await bus.readExact(len); + entries.push({ + isDir: data[0] === 1, + size: data[0] === 1 ? null : BinaryUtils.readUint32LE(data.subarray(1, 5)), + name: new TextDecoder().decode(data.subarray(5)).replace(/\0/g, '') + }); + } + if (type === FrameType.ERROR) break; + } + } finally { + bus.releaseReadLock(); // ERST HIER LOCK LÖSEN! + } + return null; + } +} \ No newline at end of file diff --git a/webpage/src/lib/protocol/commands/PlayFile.ts b/webpage/src/lib/protocol/commands/PlayFile.ts new file mode 100644 index 0000000..5ba33f8 --- /dev/null +++ b/webpage/src/lib/protocol/commands/PlayFile.ts @@ -0,0 +1,31 @@ +import { bus } from '../../bus/SerialBus'; +import { Command, FrameType } from '../constants'; + +export class PlayFileCommand { + async execute(path: string): Promise { + try { + const p = new TextEncoder().encode(path); + // Wir brauchen: 1 Byte Flags + 1 Byte Länge + Pfad + const req = new Uint8Array(p.length + 2); + + req[0] = 0x01; // 1. Byte: Flags (LSB: 1 = sofort abspielen) + req[1] = p.length; // 2. Byte: Länge für get_path() im C-Code + req.set(p, 2); // Ab 3. Byte: Der Pfad-String + + await bus.sendRequest(Command.PLAY, req); + + // Warten auf das ACK vom Board + if (await bus.waitForSync()) { + const typeArr = await bus.readExact(1); + if (typeArr[0] === FrameType.ACK) { + return true; + } + } + } catch (e) { + console.error("PlayFileCommand failed:", e); + } finally { + bus.releaseReadLock(); + } + return null; + } +} \ No newline at end of file diff --git a/webpage/src/lib/protocol/constants.ts b/webpage/src/lib/protocol/constants.ts new file mode 100644 index 0000000..dbd9251 --- /dev/null +++ b/webpage/src/lib/protocol/constants.ts @@ -0,0 +1,91 @@ +export const SYNC_SEQ = new TextEncoder().encode('BUZZ'); + +export enum FrameType { + REQUEST = 0x01, + + ACK = 0x10, + RESPONSE = 0x11, + STREAM_START = 0x12, + STREAM_CHUNK = 0x13, + STREAM_END = 0x14, + LIST_START = 0x15, + LIST_CHUNK = 0x16, + LIST_END = 0x17, + + ERROR = 0xFF, +} + +export enum Command { + GET_PROTOCOL_VERSION = 0x00, + GET_FIRMWARE_STATUS = 0x01, + GET_FLASH_INFO = 0x02, + CONFIRM_FIRMWARE = 0x03, + REBOOT = 0x04, + + LIST_DIR = 0x10, + CRC32 = 0x11, + MKDIR = 0x12, + RM = 0x13, + STAT = 0x18, + RENAME = 0x19, + + PUT_FILE = 0x20, + PUT_FW = 0x21, + GET_FILE = 0x22, + PUT_TAGS = 0x24, + GET_TAGS = 0x25, + + PLAY = 0x30, + STOP = 0x31, + + SET_SETTING = 0x40, + GET_SETTING = 0x41, +} + +export const ERRORS: Record = { + 0x00: "NONE", + 0x01: "INVALID_COMMAND", + 0x02: "INVALID_PARAMETERS", + 0x03: "COMMAND_TOO_LONG", + + 0x10: "FILE_NOT_FOUND", + 0x11: "ALREADY_EXISTS", + 0x12: "NOT_A_DIRECTORY", + 0x13: "IS_A_DIRECTORY", + 0x14: "ACCESS_DENIED", + 0x15: "NO_SPACE", + 0x16: "FILE_TOO_LARGE", + + 0x20: "IO_ERROR", + 0x21: "TIMEOUT", + 0x22: "CRC_MISMATCH", + 0x23: "TRANSFER_ABORTED", + + 0x30: "NOT_SUPPORTED", + 0x31: "BUSY", + 0x32: "INTERNAL_ERROR", +}; + +export enum ErrorCode { + P_ERR_NONE = 0x00, + P_ERR_INVALID_COMMAND = 0x01, + P_ERR_INVALID_PARAMETERS = 0x02, + P_ERR_COMMAND_TOO_LONG = 0x03, + + P_ERR_FILE_NOT_FOUND = 0x10, + P_ERR_ALREADY_EXISTS = 0x11, + P_ERR_NOT_A_DIRECTORY = 0x12, + P_ERR_IS_A_DIRECTORY = 0x13, + P_ERR_ACCESS_DENIED = 0x14, + P_ERR_NO_SPACE = 0x15, + P_ERR_FILE_TOO_LARGE = 0x16, + + P_ERR_IO = 0x20, + P_ERR_TIMEOUT = 0x21, + P_ERR_CRC_MISMATCH = 0x22, + P_ERR_TRANSFER_ABORTED = 0x23, + + P_ERR_NOT_SUPPORTED = 0x30, + P_ERR_BUSY = 0x31, + P_ERR_INTERNAL = 0x32, +}; \ No newline at end of file diff --git a/webpage/src/lib/utils/BinaryUtils.ts b/webpage/src/lib/utils/BinaryUtils.ts new file mode 100644 index 0000000..3543f81 --- /dev/null +++ b/webpage/src/lib/utils/BinaryUtils.ts @@ -0,0 +1,34 @@ +export class BinaryUtils { + /** + * Konvertiert 2 Bytes (Little Endian) in eine Zahl (uint16) + */ + static readUint16LE(data: Uint8Array, offset = 0): number { + const view = new DataView(data.buffer, data.byteOffset, data.byteLength); + return view.getUint16(offset, true); // true = Little Endian + } + + /** + * Konvertiert 4 Bytes (Little Endian) in eine Zahl (uint32) + */ + static readUint32LE(data: Uint8Array, offset = 0): number { + const view = new DataView(data.buffer, data.byteOffset, data.byteLength); + return view.getUint32(offset, true); + } + + /** + * Erstellt ein Uint8Array aus einer Zahl (uint16 LE) + */ + static writeUint16LE(value: number): Uint8Array { + const buf = new Uint8Array(2); + const view = new DataView(buf.buffer); + view.setUint16(0, value, true); + return buf; + } + + static writeUint32LE(value: number): Uint8Array { + const buf = new Uint8Array(4); + const view = new DataView(buf.buffer); + view.setUint32(0, value, true); + return buf; + } +} \ No newline at end of file diff --git a/webpage/src/pages/index.astro b/webpage/src/pages/index.astro index bb22865..65b5f3d 100644 --- a/webpage/src/pages/index.astro +++ b/webpage/src/pages/index.astro @@ -67,17 +67,7 @@ import type { loadRenderers } from "astro:container";
-
-
- -
-

- Device Info -

- -
- +