This commit is contained in:
2026-03-07 08:51:50 +01:00
parent 4f3fbff258
commit f85143d7e5
60 changed files with 3245 additions and 1205 deletions

View File

@@ -9,9 +9,12 @@ target_sources(app PRIVATE
src/io.c src/io.c
src/audio.c src/audio.c
src/usb.c src/usb.c
src/uart.c
src/protocol.c src/protocol.c
src/utils.c src/utils.c
src/settings.c
) )
zephyr_include_directories(src)
zephyr_include_directories(include)

View File

@@ -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 | Offset | Feld | Typ | Beschreibung |
- `metadata`: Folge von Tag-Einträgen | :--- | :--- | :--- | :--- |
- `tag_version_u8`: 1 Byte Versionsnummer des Tag-Formats | 0 | `total_size` | `uint16_t` | Gesamtgröße in Bytes (Summe aller TLV-Blöcke + 8 Bytes Footer). |
- `footer_len_le16`: 2 Byte, Little Endian | 2 | `version` | `uint16_t` | Format-Version. Aktuell `0x0001`. |
- `"TAG!"`: 4 Byte Magic (`0x54 0x41 0x47 0x21`) | 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`. #### Index `0x01`: Audio CRC32
- Der Host darf nur bekannte Versionen interpretieren. 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.
- Bei unbekannter Version: Tag-Block ignorieren oder als "nicht unterstützt" melden. * **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**. * **Typ:** `0x10`
- Tag-Einträge sind TLV-basiert: * **Länge:** Variabel
- `type`: `uint8_t` * **Payload:** UTF-8-kodierter JSON-String (ohne Null-Terminator).
- `len`: `uint16_t`
- `value`: `byte[len]`
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) ```json
- `0x01`: `AUTHOR` {
- `0x10`: `CRC32_RAW` "t": "Testaufnahme System A",
- `0x20`: `FILE_FORMAT` (Info für Host, Player wertet derzeit nicht aus) "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 ## 4. Lese-Algorithmus (Parser-Logik)
- `len`: Anzahl Bytes des UTF-8-Texts
### 5.2 `0x01` AUTHOR Der Controller extrahiert die Hardware-Parameter nach folgendem Ablauf:
- `value`: UTF-8-Text 1. **Footer lokalisieren:** * Gehe zu `EOF - 8`. Lese 8 Bytes in das `tag_footer_t` Struct.
- `len`: Anzahl Bytes des UTF-8-Texts * 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) ## 5. Hex-Beispiel
- `len`: **muss 4** sein
### 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`: **1. TLV 0x00 (Audio Format):**
- `bits_per_sample`: `uint8_t` * Header: `00 00 08 00` (Type 0, Index 0, Length 8)
- `sample_rate`: `uint32_t` (Little Endian) * Payload: `00 10 00 00 80 3E 00 00` (Mono, 16-Bit, Reserved, 16000 Hz)
- `len`: **muss 5** sein
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 **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.*
Aktueller Stand: **jeder Tag-Typ darf maximal 1x vorkommen**. * Version: `01 00`
* Magic: `54 41 47 21` (`TAG!`)
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.

View File

@@ -1,6 +1,6 @@
VERSION_MAJOR = 0 VERSION_MAJOR = 0
VERSION_MINOR = 2 VERSION_MINOR = 3
PATCHLEVEL = 19 PATCHLEVEL = 5
VERSION_TWEAK = 0 VERSION_TWEAK = 0
#if (IS_ENABLED(CONFIG_LOG)) #if (IS_ENABLED(CONFIG_LOG))
EXTRAVERSION = debug EXTRAVERSION = debug

View File

@@ -10,6 +10,37 @@ typedef struct slot_info_t {
size_t size; size_t size;
} slot_info_t; } 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 * @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); 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 * @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 * @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); 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. * @brief Setzt die Synchronisation für einen neuen Dateitransfer zurück.
* @param fp Pointer to an open fs_file_t structure representing the audio file
* @return 0 on success, negative error code on failure
*/ */
int fs_tag_open_write(struct fs_file_t *fp); void fs_reset_transfer_sync(void);
/** /**
* @brief Writes a raw tag payload chunk. * @brief Blockiert den aufrufenden Thread, bis der FS-Thread den Transfer
* @param fp Pointer to an open fs_file_t positioned for tag payload write * (EOF oder ABORT) vollständig auf dem Flash abgeschlossen hat.
* @param buffer Source buffer
* @param len Number of bytes to write
* @return Number of bytes written, negative error code on failure
*/ */
ssize_t fs_tag_write_chunk(struct fs_file_t *fp, const void *buffer, size_t len); void fs_wait_for_transfer_complete(void);
/**
* @brief Finalizes tags by appending version + footer and truncating file.
* @param fp Pointer to an open fs_file_t structure representing the audio file
* @param version Tag format version to write
* @param payload_len Tag payload length in bytes
* @return 0 on success, negative error code on failure
*/
int fs_tag_finish_write(struct fs_file_t *fp, uint8_t version, size_t payload_len);
/** /**
* @brief Retrieves information about the firmware slot, such as start address and size * @brief Retrieves information about the firmware slot, such as start address and size

View File

@@ -30,6 +30,14 @@ typedef enum {
CMD_PUT_FILE = 0x20, CMD_PUT_FILE = 0x20,
CMD_PUT_FW = 0x21, CMD_PUT_FW = 0x21,
CMD_GET_FILE = 0x22, 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; } protocol_cmd_t;
typedef enum { typedef enum {

View File

@@ -0,0 +1,28 @@
#ifndef BUZZER_SETTINGS_H
#define BUZZER_SETTINGS_H
#include <stdint.h>
#include <stdbool.h>
/* 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 */

8
firmware/include/uart.h Normal file
View File

@@ -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_ */

8
firmware/include/usb.h Normal file
View File

@@ -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_ */

View File

@@ -3,7 +3,7 @@ mcuboot:
size: 0xC000 size: 0xC000
region: flash_primary region: flash_primary
# Primary Slot: Start bleibt 0xC000, Größe jetzt 200KB (0x32000) # Primary Slot: Start bleibt 0xC000, Größe 200KB (0x32000)
mcuboot_primary: mcuboot_primary:
address: 0xC000 address: 0xC000
size: 0x32000 size: 0x32000
@@ -26,7 +26,13 @@ mcuboot_secondary:
size: 0x32000 size: 0x32000
region: flash_primary 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: littlefs_storage:
address: 0x0 address: 0x0
size: 0x800000 size: 0x800000

View File

@@ -1,7 +1,7 @@
# --- GPIO & Logging --- # --- GPIO & Logging ---
CONFIG_GPIO=y CONFIG_GPIO=y
CONFIG_LOG=y CONFIG_LOG=y
CONFIG_POLL=y CONFIG_POLL=n
# --- Power Management (Fix für HAS_PM & Policy) --- # --- Power Management (Fix für HAS_PM & Policy) ---
# CONFIG_PM=y # CONFIG_PM=y
@@ -13,24 +13,25 @@ CONFIG_FLASH_MAP=y
CONFIG_FILE_SYSTEM=y CONFIG_FILE_SYSTEM=y
CONFIG_FILE_SYSTEM_LITTLEFS=y CONFIG_FILE_SYSTEM_LITTLEFS=y
CONFIG_FILE_SYSTEM_MKFS=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_PROG_SIZE=256
CONFIG_FS_LITTLEFS_CACHE_SIZE=512 CONFIG_FS_LITTLEFS_CACHE_SIZE=4096
CONFIG_FS_LITTLEFS_LOOKAHEAD_SIZE=128 CONFIG_FS_LITTLEFS_LOOKAHEAD_SIZE=256
CONFIG_FS_LITTLEFS_BLOCK_CYCLES=512 CONFIG_FS_LITTLEFS_BLOCK_CYCLES=512
CONFIG_MAIN_STACK_SIZE=2048 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 --- # --- USB Device & CDC ACM ---
CONFIG_USB_DEVICE_STACK=y CONFIG_USB_DEVICE_STACK_NEXT=y
CONFIG_DEPRECATION_TEST=y CONFIG_USBD_CDC_ACM_CLASS=y
CONFIG_USB_DEVICE_MANUFACTURER="Eduard Iten" CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT=n
CONFIG_USB_DEVICE_PRODUCT="Edis Buzzer" CONFIG_USBD_LOG_LEVEL_ERR=y
CONFIG_USB_DEVICE_PID=0x0001 CONFIG_UDC_DRIVER_LOG_LEVEL_ERR=y
CONFIG_USB_DRIVER_LOG_LEVEL_ERR=y CONFIG_USBD_CDC_ACM_LOG_LEVEL_OFF=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
# --- UART (für USB-CDC) --- # --- UART (für USB-CDC) ---
CONFIG_SERIAL=y CONFIG_SERIAL=y

View File

@@ -9,6 +9,7 @@
#include <audio.h> #include <audio.h>
#include <fs.h> #include <fs.h>
#include <io.h> #include <io.h>
#include <settings.h>
#define AUDIO_THREAD_STACK_SIZE 2048 #define AUDIO_THREAD_STACK_SIZE 2048
#define AUDIO_THREAD_PRIORITY 5 #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 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 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 volatile bool abort_playback = false;
static char next_random_filename[64] = {0}; 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_mutex i2s_lock;
static struct k_work audio_stop_work; static struct k_work audio_stop_work;
static uint32_t last_played_index = 0xFFFFFFFF;
static void audio_stop_work_handler(struct k_work *work) static void audio_stop_work_handler(struct k_work *work)
{ {
ARG_UNUSED(work); ARG_UNUSED(work);
@@ -89,6 +91,52 @@ void i2s_resume(void)
k_mutex_unlock(&i2s_lock); 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) void audio_refresh_file_count(void)
{ {
static struct fs_dir_t dirp; static struct fs_dir_t dirp;
@@ -112,6 +160,7 @@ void audio_refresh_file_count(void)
fs_pm_closedir(&dirp); fs_pm_closedir(&dirp);
audio_file_count = count; audio_file_count = count;
LOG_INF("Audio cache refreshed: %u files found in %s", count, AUDIO_PATH); 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) 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) void audio_system_ready(void)
{ {
k_sem_give(&audio_ready_sem); k_sem_give(&audio_ready_sem);
@@ -250,8 +265,8 @@ void audio_thread(void *arg1, void *arg2, void *arg3)
bool trigger_started = false; bool trigger_started = false;
int queued_blocks = 0; int queued_blocks = 0;
uint8_t factor = MIN(255, current_volume * 0xFF / 100); uint8_t factor = MIN(255, app_settings.audio_vol * 0xFF / 100);
LOG_INF("Volume factor: %u (for volume %d%%)", factor, current_volume); LOG_INF("Volume factor: %u (for volume %d%%)", factor, app_settings.audio_vol);
while (!abort_playback) while (!abort_playback)
{ {

View File

@@ -1,4 +1,5 @@
#include <zephyr/fs/littlefs.h> #include <zephyr/fs/littlefs.h>
#include <zephyr/sys/byteorder.h>
#include <zephyr/drivers/flash.h> #include <zephyr/drivers/flash.h>
#include <zephyr/storage/flash_map.h> #include <zephyr/storage/flash_map.h>
#include <zephyr/dfu/flash_img.h> #include <zephyr/dfu/flash_img.h>
@@ -9,18 +10,23 @@
LOG_MODULE_REGISTER(buzz_fs, LOG_LEVEL_INF); 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 "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 STORAGE_PARTITION_ID FIXED_PARTITION_ID(littlefs_storage)
#define SLOT1_ID FIXED_PARTITION_ID(slot1_partition) #define SLOT1_ID FIXED_PARTITION_ID(slot1_partition)
FS_LITTLEFS_DECLARE_DEFAULT_CONFIG(fs_storage_data); 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) #define QSPI_FLASH_NODE DT_ALIAS(qspi_flash)
#if !DT_NODE_EXISTS(QSPI_FLASH_NODE) #if !DT_NODE_EXISTS(QSPI_FLASH_NODE)
#error "QSPI Flash alias not defined in devicetree" #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 slot_info_t slot1_info;
static struct flash_img_context flash_ctx; static struct flash_img_context flash_ctx;
extern struct k_mem_slab file_buffer_slab;
static struct fs_mount_t fs_storage_mnt = { static struct fs_mount_t fs_storage_mnt = {
.type = FS_LITTLEFS, .type = FS_LITTLEFS,
.fs_data = &fs_storage_data, .fs_data = &fs_storage_data,
@@ -40,6 +48,17 @@ static struct fs_mount_t fs_storage_mnt = {
.mnt_point = "/lfs", .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 fs_init(void) {
int rc = fs_mount(&fs_storage_mnt); int rc = fs_mount(&fs_storage_mnt);
if (rc < 0) { if (rc < 0) {
@@ -121,10 +140,7 @@ int fs_pm_close(struct fs_file_t *file)
{ {
LOG_DBG("PM Closing file"); LOG_DBG("PM Closing file");
int rc = fs_close(file); int rc = fs_close(file);
if (rc == 0)
{
fs_pm_flash_suspend(); fs_pm_flash_suspend();
}
return rc; return rc;
} }
@@ -144,10 +160,7 @@ int fs_pm_closedir(struct fs_dir_t *dirp)
{ {
LOG_DBG("PM Closing directory"); LOG_DBG("PM Closing directory");
int rc = fs_closedir(dirp); int rc = fs_closedir(dirp);
if (rc == 0)
{
fs_pm_flash_suspend(); fs_pm_flash_suspend();
}
return rc; return rc;
} }
@@ -196,11 +209,159 @@ int fs_pm_rename(const char *old_path, const char *new_path)
return rc; 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, 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) size_t *audio_limit, size_t *payload_len, bool *has_tag)
{ {
uint8_t footer[6]; tag_footer_t footer;
uint16_t tag_len;
if (audio_limit == NULL || payload_len == NULL || has_tag == NULL) { if (audio_limit == NULL || payload_len == NULL || has_tag == NULL) {
return -EINVAL; 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; *audio_limit = (size_t)file_size;
*payload_len = 0U; *payload_len = 0U;
if (file_size < (off_t)TAG_FOOTER_V1_LEN) { if (file_size < (off_t)sizeof(tag_footer_t)) {
return 0; return 0;
} }
fs_seek(fp, -(off_t)(TAG_LEN_FIELD_LEN + TAG_MAGIC_LEN), FS_SEEK_END); /* Den 8-Byte-Footer direkt in das Struct einlesen */
if (fs_read(fp, footer, sizeof(footer)) != sizeof(footer)) { 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); fs_seek(fp, 0, FS_SEEK_SET);
return -EIO; 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); fs_seek(fp, 0, FS_SEEK_SET);
return 0; return 0;
} }
tag_len = (uint16_t)footer[0] | ((uint16_t)footer[1] << 8); /* 2. Endianness konvertieren */
if (tag_len > (uint16_t)file_size || tag_len < TAG_FOOTER_V1_LEN) { uint16_t tag_version = sys_le16_to_cpu(footer.version);
fs_seek(fp, 0, FS_SEEK_SET); uint16_t tag_len = sys_le16_to_cpu(footer.total_size);
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;
}
/* 3. Version und Größe validieren */
if (tag_version != TAG_FORMAT_VERSION) { if (tag_version != TAG_FORMAT_VERSION) {
fs_seek(fp, 0, FS_SEEK_SET); fs_seek(fp, 0, FS_SEEK_SET);
return -ENOTSUP; 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; *has_tag = true;
*audio_limit = (size_t)file_size - tag_len; *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); fs_seek(fp, 0, FS_SEEK_SET);
return 0; 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); 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) { int flash_get_slot_info(slot_info_t *info) {
if (slot1_info.size != 0) { if (slot1_info.size != 0) {
*info = slot1_info; *info = slot1_info;
@@ -493,3 +608,112 @@ size_t fs_get_internal_flash_page_size(void) {
return info.size; return info.size;
} }
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(&current_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(&current_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(&current_file, current_filename, FS_O_CREATE | FS_O_WRITE);
if (rc == 0) {
if (msg.start.start_position > 0) {
fs_seek(&current_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(&current_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(&current_file);
state = FS_STATE_IDLE;
k_sem_give(&fs_transfer_done_sem);
}
else if (msg.type == FS_MSG_ABORT)
{
fs_pm_close(&current_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);

View File

@@ -15,6 +15,8 @@
#include <io.h> #include <io.h>
#include <usb.h> #include <usb.h>
#include <utils.h> #include <utils.h>
#include <uart.h>
#include <settings.h>
LOG_MODULE_REGISTER(main, LOG_LEVEL_INF); LOG_MODULE_REGISTER(main, LOG_LEVEL_INF);
@@ -38,6 +40,13 @@ int main(void)
int rc; int rc;
rc = app_settings_init();
if (rc < 0)
{
LOG_ERR("Settings initialization failed: %d", rc);
return rc;
}
rc = fs_init(); rc = fs_init();
if (rc < 0) if (rc < 0)
{ {
@@ -52,13 +61,21 @@ int main(void)
return rc; return rc;
} }
rc = usb_cdc_acm_init(); rc = usb_init();
if (rc < 0) if (rc < 0)
{ {
LOG_ERR("USB initialization failed: %d", rc); LOG_ERR("USB initialization failed: %d", rc);
return rc; return rc;
} }
rc = uart_init();
if (rc < 0)
{
LOG_ERR("UART initialization failed: %d", rc);
return rc;
}
rc = io_init(); rc = io_init();
if (rc < 0) if (rc < 0)
{ {
@@ -81,6 +98,7 @@ int main(void)
{ {
LOG_INF("Firmware image already confirmed. No need to confirm again."); LOG_INF("Firmware image already confirmed. No need to confirm again.");
} }
while (1) while (1)
{ {
k_sleep(K_FOREVER); k_sleep(K_FOREVER);

File diff suppressed because it is too large Load Diff

166
firmware/src/settings.c Normal file
View File

@@ -0,0 +1,166 @@
#include "settings.h"
#include <zephyr/kernel.h>
#include <zephyr/settings/settings.h>
#include <zephyr/logging/log.h>
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;
}

135
firmware/src/uart.c Normal file
View File

@@ -0,0 +1,135 @@
// uart.c
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/drivers/uart.h>
#include <zephyr/sys/ring_buffer.h>
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;
}

View File

@@ -1,202 +1,181 @@
#include <zephyr/kernel.h> #include <zephyr/device.h>
#include <zephyr/logging/log.h>
#include <zephyr/usb/usb_device.h>
#include <zephyr/drivers/uart.h> #include <zephyr/drivers/uart.h>
#include <zephyr/sys/ring_buffer.h> /* NEU */ #include <errno.h>
#include <zephyr/logging/log.h>
#include <zephyr/usb/usbd.h>
#include <io.h> #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); LOG_MODULE_REGISTER(usb, LOG_LEVEL_INF);
K_SEM_DEFINE(usb_rx_sem, 0, 1); K_SEM_DEFINE(dtr_active_sem, 0, 1);
K_SEM_DEFINE(usb_tx_sem, 0, 1); static uint32_t dtr_active = 0U;
#define UART_NODE DT_ALIAS(usb_uart) USBD_DEVICE_DEFINE(cdc_acm_serial,
const struct device *cdc_dev = DEVICE_DT_GET(UART_NODE); DEVICE_DT_GET(DT_NODELABEL(zephyr_udc0)),
static volatile bool rx_interrupt_enabled = false; 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 (IS_ENABLED(CONFIG_USBD_CDC_ACM_CLASS) ||
IS_ENABLED(CONFIG_USBD_CDC_ECM_CLASS) ||
if (!uart_irq_update(dev)) { IS_ENABLED(CONFIG_USBD_CDC_NCM_CLASS) ||
return; IS_ENABLED(CONFIG_USBD_MIDI2_CLASS) ||
} IS_ENABLED(CONFIG_USBD_AUDIO2_CLASS) ||
IS_ENABLED(CONFIG_USBD_VIDEO_CLASS)) {
if (uart_irq_rx_ready(dev)) { usbd_device_set_code_triple(uds_ctx, speed,
uint8_t buffer[64]; USB_BCC_MISCELLANEOUS, 0x02, 0x01);
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 { } else {
int to_read = MIN(sizeof(buffer), space); usbd_device_set_code_triple(uds_ctx, speed, 0, 0, 0);
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);
} }
} }
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)) { int err;
return true;
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);
}
} }
/* Wenn der Puffer leer ist, sicherstellen, dass der RX-Interrupt if (msg->type == USBD_MSG_VBUS_REMOVED) {
aktiviert ist, da sonst keine neuen Daten empfangen werden können. */ err = usbd_disable(ctx);
if (device_is_ready(cdc_dev)) { if (err) {
uart_irq_rx_enable(cdc_dev); LOG_ERR("Failed to disable USB device (%d)", err);
}
}
} }
return (k_sem_take(&usb_rx_sem, timeout) == 0); if (msg->type == USBD_MSG_CDC_ACM_CONTROL_LINE_STATE) {
} uint32_t rts = 0U;
uint32_t dcd = 0U;
uint32_t dsr = 0U;
bool usb_read_byte(uint8_t *c) if (msg->dev != NULL) {
{ (void)uart_line_ctrl_get(msg->dev, UART_LINE_CTRL_RTS, &rts);
int ret = ring_buf_get(&rx_ringbuf, c, 1); (void)uart_line_ctrl_get(msg->dev, UART_LINE_CTRL_DTR, &dtr_active);
return (ret > 0); (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);
int usb_read_buffer(uint8_t *buf, size_t max_len) if (dtr_active) {
{ k_sem_give(&dtr_active_sem);
int ret = ring_buf_get(&rx_ringbuf, buf, max_len); }
return ret; }
}
void usb_resume_rx(void)
{
if (device_is_ready(cdc_dev)) {
uart_irq_rx_enable(cdc_dev);
} }
} }
void usb_write_byte(uint8_t c) int usb_init(void)
{ {
if (!device_is_ready(cdc_dev)) { int err;
return;
}
uart_poll_out(cdc_dev, c);
}
int usb_write_buffer(const uint8_t *buf, size_t len) err = usbd_add_descriptor(&cdc_acm_serial, &cdc_acm_lang);
{ if (err) {
if (!device_is_ready(cdc_dev)) LOG_ERR("Failed to add language descriptor (%d)", err);
{ return err;
return -ENODEV;
} }
size_t written; err = usbd_add_descriptor(&cdc_acm_serial, &cdc_acm_mfr);
while (len > 0) if (err) {
{ LOG_ERR("Failed to add manufacturer descriptor (%d)", err);
written = uart_fifo_fill(cdc_dev, buf, len); return err;
}
len -= written; err = usbd_add_descriptor(&cdc_acm_serial, &cdc_acm_product);
buf += written; if (err) {
LOG_ERR("Failed to add product descriptor (%d)", err);
return err;
}
uart_irq_tx_enable(cdc_dev); 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 (len > 0) 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;
}
if (k_sem_take(&usb_tx_sem, K_MSEC(100)) != 0) err = usbd_register_class(&cdc_acm_serial, "cdc_acm_0", USBD_SPEED_HS, 1);
{ if (err) {
LOG_WRN("USB TX timeout - consumer not reading?"); LOG_ERR("Failed to register HS CDC ACM class (%d)", err);
return -ETIMEDOUT; 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; return 0;
} }
void usb_flush_rx(void) void usb_wait_for_dtr(void)
{ {
uint8_t dummy; k_sem_take(&dtr_active_sem, K_FOREVER);
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) bool usb_dtr_active(void)
{ {
switch (cb_status) { return dtr_active != 0U;
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;
} }

View File

@@ -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

View File

@@ -4,6 +4,8 @@
#include <zephyr/logging/log_ctrl.h> #include <zephyr/logging/log_ctrl.h>
#include <zephyr/sys/reboot.h> #include <zephyr/sys/reboot.h>
#include <settings.h>
#if IS_ENABLED(CONFIG_SOC_SERIES_NRF52X) #if IS_ENABLED(CONFIG_SOC_SERIES_NRF52X)
#include <hal/nrf_power.h> #include <hal/nrf_power.h>
#elif IS_ENABLED(CONFIG_SOC_SERIES_STM32G0X) #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) void reboot_with_status(uint8_t status)
{ {
app_settings_save_pending_now();
#if IS_ENABLED(CONFIG_SOC_SERIES_NRF52X) #if IS_ENABLED(CONFIG_SOC_SERIES_NRF52X)
/* Korrigierter Aufruf mit Register-Index 0 */ /* Korrigierter Aufruf mit Register-Index 0 */
nrf_power_gpregret_set(NRF_POWER, REBOOT_STATUS_REG_IDX, (uint32_t)status); nrf_power_gpregret_set(NRF_POWER, REBOOT_STATUS_REG_IDX, (uint32_t)status);

BIN
sounds/sys/404 Normal file

Binary file not shown.

BIN
sounds/sys/confirm Normal file

Binary file not shown.

BIN
sounds/sys/update Normal file

Binary file not shown.

BIN
sounds/sys/voltest Normal file

Binary file not shown.

BIN
temp Normal file

Binary file not shown.

View File

@@ -22,6 +22,10 @@ def main():
# Subparser für Befehle # Subparser für Befehle
subparsers = parser.add_subparsers(dest="command", help="Verfügbare 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 # Befehl: flash_info
flash_info_parser = subparsers.add_parser("flash_info", help="Informationen über den Flash-Speicher des Controllers abfragen") 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 # Befehl: proto
proto_parser = subparsers.add_parser("proto", help="Protokollversion des Controllers abfragen") 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 # Befehl: rename
rename_parser = subparsers.add_parser("rename", help="Benennen Sie eine Datei oder einen Ordner auf dem Zielsystem um") 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") 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 = subparsers.add_parser("stat", help="Informationen zu einer Datei/Ordner")
stat_parser.add_argument("path", help="Pfad auf dem Zielsystem") 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() args = parser.parse_args()
if not args.command: if not args.command:
@@ -103,7 +148,12 @@ def main():
try: try:
bus.open() 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 from core.cmd.get_file import get_file
cmd = get_file(bus) cmd = get_file(bus)
result = cmd.get(args.source_path, args.dest_path) result = cmd.get(args.source_path, args.dest_path)
@@ -128,6 +178,11 @@ def main():
cmd = proto(bus) cmd = proto(bus)
result = cmd.get() result = cmd.get()
cmd.print(result) 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": elif args.command == "rename":
from core.cmd.rename import rename from core.cmd.rename import rename
cmd = rename(bus) cmd = rename(bus)
@@ -143,7 +198,56 @@ def main():
cmd = stat(bus) cmd = stat(bus)
result = cmd.get(args.path) result = cmd.get(args.path)
cmd.print(result, 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: finally:
bus.close() bus.close()

36
tool/core/cmd/crc32.py Normal file
View File

@@ -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('<I', payload[0:4])[0]
audio_crc_value = struct.unpack('<I', payload[4:8])[0]
result = {
'crc32': crc_value,
'audio_crc32': audio_crc_value
}
return result
def print(self, result, path: str):
if not result:
return
console.print(f"[info_title]CRC32[/info_title] für [info]{path}[/info]:")
console.print(f" • CRC32 Datei: [info]{result['crc32']:08X}[/info]")
console.print(f" • CRC32 Audio: [info]{result['audio_crc32']:08X}[/info]")

View File

@@ -0,0 +1,23 @@
from core.utils import console, console_err
from core.protocol import COMMANDS
class fw_confirm:
def __init__(self, bus):
self.bus = bus
def get(self):
# Fehler 1: Der Key in COMMANDS heißt 'confirm_fw'
self.bus.send_request(COMMANDS['confirm_fw'])
# Fehler 2: Try-Except entfernt, damit der ControllerError (z.B. bei nicht-pending Image)
# sauber nach buzz.py durchschlägt.
data = self.bus.receive_ack()
return data is not None and data.get('type') == 'ack'
def print(self, result):
if result:
console.print("✓ Laufende Firmware wurde [info]erfolgreich bestätigt[/info] (Permanent).")
else:
# Wird im Fehlerfall eigentlich nicht mehr erreicht, da buzz.py abbricht,
# bleibt aber als Fallback für leere Antworten.
console_err.print("❌ Fehler beim Bestätigen der Firmware.")

View File

@@ -2,6 +2,7 @@
import struct import struct
import zlib import zlib
from pathlib import Path from pathlib import Path
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, DownloadColumn, TransferSpeedColumn, TimeRemainingColumn
from core.utils import console, console_err from core.utils import console, console_err
from core.protocol import COMMANDS from core.protocol import COMMANDS
@@ -21,24 +22,34 @@ class get_file:
source_path_bytes = source_path.encode('utf-8') source_path_bytes = source_path.encode('utf-8')
payload = struct.pack('B', len(source_path_bytes)) + source_path_bytes payload = struct.pack('B', len(source_path_bytes)) + source_path_bytes
device_file_crc = None
try:
self.bus.send_request(COMMANDS['crc_32'], payload)
crc_resp = self.bus.receive_response(length=4)
if crc_resp and crc_resp.get('type') == 'response':
device_file_crc = struct.unpack('<I', crc_resp['data'])[0]
except Exception:
device_file_crc = None
self.bus.send_request(COMMANDS['get_file'], payload) self.bus.send_request(COMMANDS['get_file'], payload)
stream_res = self.bus.receive_stream() # Fortschrittsbalken Setup
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
BarColumn(),
DownloadColumn(),
TransferSpeedColumn(),
"",
TimeRemainingColumn(),
console=console,
transient=False
) as progress:
task = progress.add_task(f"Lade {source_path}...", total=None)
def update_bar(received, total):
progress.update(task, total=total, completed=received)
stream_res = self.bus.receive_stream(progress_callback=update_bar)
if not stream_res or stream_res.get('type') == 'error': if not stream_res or stream_res.get('type') == 'error':
return None return None
file_data = stream_res['data'] file_data = stream_res['data']
remote_crc = stream_res['crc32'] remote_crc = stream_res.get('crc32')
local_crc = zlib.crc32(file_data) & 0xFFFFFFFF
duratuion = stream_res.get('duration')
if local_crc == remote_crc: if local_crc == remote_crc:
with open(p, 'wb') as f: with open(p, 'wb') as f:
@@ -55,8 +66,8 @@ class get_file:
'dest_path': dest_path, 'dest_path': dest_path,
'crc32_remote': remote_crc, 'crc32_remote': remote_crc,
'crc32_local': local_crc, 'crc32_local': local_crc,
'crc32_device_file': device_file_crc, 'size': len(file_data),
'size': len(file_data) 'duration': duratuion
} }
def print(self, result): def print(self, result):
@@ -74,3 +85,5 @@ class get_file:
if result.get('crc32_device_file') is not None: if result.get('crc32_device_file') is not None:
console.print(f" • Device CRC: [info]{result['crc32_device_file']:08X}[/info]") console.print(f" • Device CRC: [info]{result['crc32_device_file']:08X}[/info]")
console.print(f" • Zielpfad: [info]{result['dest_path']}[/info]") console.print(f" • Zielpfad: [info]{result['dest_path']}[/info]")
if result.get('duration') is not None and result.get('duration') > 0:
console.print(f" • Dauer: [info]{result['duration']:.2f} s[/info]")

View File

@@ -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('<B', val_buf)[0]
elif key == "play/norepeat" and val_len == 1:
return bool(struct.unpack('<B', val_buf)[0])
elif key == "settings/storage_interval" and val_len == 2:
return struct.unpack('<H', val_buf)[0]
else:
return None
def print(self, result, key: str):
if result is not None:
console.print(f"⚙️ [info]{key}[/info] = [info]{result}[/info]")

43
tool/core/cmd/get_tags.py Normal file
View File

@@ -0,0 +1,43 @@
# tool/core/cmd/get_tags.py
import struct
import json
from core.utils import console
from core.protocol import COMMANDS
from core.tag import TagManager
class get_tags:
def __init__(self, bus):
self.bus = bus
def get_raw_tlvs(self, path: str):
"""Holt die rohen TLVs vom Gerät."""
path_bytes = path.encode('utf-8')
payload = struct.pack('B', len(path_bytes)) + path_bytes
self.bus.send_request(COMMANDS['get_tags'], payload)
stream_res = self.bus.receive_stream()
if not stream_res or stream_res.get('type') == 'error': return []
return TagManager.parse_tlvs(stream_res['data'])
def get(self, path: str):
tlvs = self.get_raw_tlvs(path)
result = {"system": {}, "json": {}}
for tlv in tlvs:
if tlv['type'] == 0x00:
if tlv['index'] == 0x00 and len(tlv['value']) == 8:
codec, bit_depth, _, samplerate = struct.unpack('<BBHI', tlv['value'])
result["system"]["format"] = {"codec": codec, "bit_depth": bit_depth, "samplerate": samplerate}
elif tlv['index'] == 0x01 and len(tlv['value']) == 4:
result["system"]["crc32"] = f"0x{struct.unpack('<I', tlv['value'])[0]:08X}"
elif tlv['type'] == 0x10:
try:
result["json"].update(json.loads(tlv['value'].decode('utf-8')))
except:
pass
return result
def print(self, result, path: str):
console.print(f"[info]Metadaten[/info] für [info]{path}[/info]:")
console.print(json.dumps(result, indent=2, ensure_ascii=False))

23
tool/core/cmd/play.py Normal file
View File

@@ -0,0 +1,23 @@
import struct
from core.utils import console, console_err
from core.protocol import COMMANDS
class play:
def __init__(self, bus):
self.bus = bus
def get(self, path: str, interrupt: bool):
flags = 0x01 if interrupt else 0x00
path_bytes = path.encode('utf-8')
# Payload: [1 Byte Flags] + [1 Byte Path Length] + [Path String]
payload = struct.pack('B', flags) + struct.pack('B', len(path_bytes)) + path_bytes
self.bus.send_request(COMMANDS['play'], payload)
data = self.bus.receive_ack()
return data is not None and data.get('type') == 'ack'
def print(self, result, path: str, interrupt: bool):
if result:
mode = "sofort (Interrupt)" if interrupt else "in die Warteschlange (Queue)"
console.print(f"▶ Wiedergabe von [info]{path}[/info] {mode} eingereiht.")

100
tool/core/cmd/put_file.py Normal file
View File

@@ -0,0 +1,100 @@
# tool/core/cmd/put_file.py
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
from core.tag import TagManager
from core.cmd.put_tags import put_tags
class put_file:
def __init__(self, bus):
self.bus = bus
def get(self, source_path: str, dest_path: str, cli_tags_json: str = None):
try:
p = Path(source_path)
if not p.exists() or not p.is_file():
console_err.print(f"Fehler: Quelldatei existiert nicht: {source_path}")
return None
with open(p, 'rb') as f:
file_data = f.read()
except Exception as e:
console_err.print(f"Fehler beim Lesen: {e}")
return None
# 1. Lokale Tags abtrennen
audio_data, local_tlvs = TagManager.split_file(file_data)
audio_size = len(audio_data)
# 2. Upload der REINEN Audiodaten
dest_path_bytes = dest_path.encode('utf-8')
payload = struct.pack('B', len(dest_path_bytes)) + dest_path_bytes + struct.pack('<I', audio_size)
self.bus.send_request(COMMANDS['put_file'], payload)
self.bus.receive_ack(timeout=5.0)
with Progress(SpinnerColumn(), TextColumn("[progress.description]{task.description}"), BarColumn(), DownloadColumn(), TransferSpeedColumn(), "", TimeRemainingColumn(), console=console, transient=False) as progress:
task = progress.add_task(f"Sende {source_path}...", total=audio_size)
stream_res = self.bus.send_stream(audio_data, progress_callback=lambda sent, total: progress.update(task, total=total, completed=sent))
if not stream_res: return None
remote_crc = stream_res.get('crc32')
local_crc = zlib.crc32(audio_data) & 0xFFFFFFFF
if local_crc != remote_crc:
return {'success': False, 'source_path': source_path, 'crc32_remote': remote_crc, 'crc32_local': local_crc}
# 3. Tags aktualisieren (CRC32 + evtl. CLI-Tags)
# Alten CRC-Tag entfernen, neuen einsetzen
final_tlvs = [t for t in local_tlvs if not (t['type'] == 0x00 and t['index'] == 0x01)]
final_tlvs.append({'type': 0x00, 'index': 0x01, 'value': struct.pack('<I', local_crc)})
# Falls CLI-Tags übergeben wurden (-t), diese priorisiert anwenden
if cli_tags_json:
try:
cli_tlvs = TagManager.parse_cli_json(cli_tags_json)
# Bestehendes JSON löschen, wenn neues im CLI-Input definiert ist
if any(t['type'] == 0x10 for t in cli_tlvs):
final_tlvs = [t for t in final_tlvs if t['type'] != 0x10]
final_tlvs.extend(cli_tlvs)
except ValueError as e:
console_err.print(f"[warning]Warnung: Tags konnten nicht geparst werden ({e}). Datei wurde ohne extra Tags hochgeladen.[/warning]")
# 4. Tags via separatem Befehl anhängen
tag_cmd = put_tags(self.bus)
tag_blob = TagManager.build_blob(final_tlvs)
tag_cmd.send_blob(dest_path, tag_blob)
return {
'success': True,
'source_path': source_path,
'dest_path': dest_path,
'crc32_remote': remote_crc,
'crc32_local': local_crc,
'size': audio_size,
'duration': stream_res.get('duration')
}
def print(self, result):
if not result:
return
if result.get('success'):
console.print(f"✓ Datei [info]{result['source_path']}[/info] erfolgreich hochgeladen und Tags generiert.")
console.print(f" • Größe: [info]{result['size'] / 1024:.2f} KB[/info]")
else:
console_err.print(f"❌ CRC-FEHLER: Datei [error]{result['source_path']}[/error] wurde auf dem Gerät korrumpiert!")
if 'crc32_remote' in result:
console.print(f" • Remote CRC: [info]{result['crc32_remote']:08X}[/info]")
if 'crc32_local' in result:
console.print(f" • Local CRC: [info]{result['crc32_local']:08X}[/info]")
if 'dest_path' in result:
console.print(f" • Zielpfad: [info]{result['dest_path']}[/info]")
if result.get('duration') is not None and result.get('duration') > 0:
console.print(f" • Dauer: [info]{result['duration']:.2f} s[/info]")

89
tool/core/cmd/put_fw.py Normal file
View File

@@ -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('<I', file_size)
self.bus.send_request(COMMANDS['put_fw'], payload)
# Warten auf ACK (Balken pulsiert ohne Byte-Anzeige)
self.bus.receive_ack(timeout=10.0)
progress.update(erase_task, description="✓ Slot gelöscht", completed=100, total=100)
# 2. Schritt: Eigentlicher Transfer mit allen Metriken (Bytes, Speed, Time)
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
BarColumn(),
DownloadColumn(),
TransferSpeedColumn(),
"",
TimeRemainingColumn(),
console=console,
transient=False
) as progress:
transfer_task = progress.add_task("Sende Firmware...", total=file_size)
stream_res = self.bus.send_stream(
file_data,
progress_callback=lambda sent, total: progress.update(transfer_task, total=total, completed=sent)
)
if not stream_res:
return None
remote_crc = stream_res.get('crc32')
local_crc = zlib.crc32(file_data) & 0xFFFFFFFF
return {
'success': local_crc == remote_crc,
'source_path': file_path,
'crc32_remote': remote_crc,
'crc32_local': local_crc,
'size': file_size,
'duration': stream_res.get('duration')
}
def print(self, result):
if not result:
return
if result['success']:
console.print(f"✓ Firmware [info]{result['source_path']}[/info] erfolgreich in Slot 1 geschrieben.")
console.print(" [warning]Achtung:[/warning] Das Image ist als 'Pending' markiert. Führe 'reboot' aus, um das Update zu installieren.")
else:
console_err.print(f"❌ CRC-FEHLER: Die Firmware wurde korrumpiert übertragen!")
console.print(f" • Größe: [info]{result['size'] / 1024:.2f} KB[/info]")
console.print(f" • Remote CRC: [info]{result['crc32_remote']:08X}[/info]")
console.print(f" • Local CRC: [info]{result['crc32_local']:08X}[/info]")

68
tool/core/cmd/put_tags.py Normal file
View File

@@ -0,0 +1,68 @@
# tool/core/cmd/put_tags.py
import struct
import json
from core.utils import console, console_err
from core.protocol import COMMANDS
from core.tag import TagManager
from core.cmd.get_tags import get_tags
class put_tags:
def __init__(self, bus):
self.bus = bus
def get(self, path: str, json_str: str, overwrite: bool):
try:
new_tlvs = TagManager.parse_cli_json(json_str)
except ValueError as e:
console_err.print(f"[error]{e}[/error]")
return False
getter = get_tags(self.bus)
existing_tlvs = getter.get_raw_tlvs(path)
if overwrite:
# Bei Overwrite: Alle alten JSON-Tags löschen
existing_tlvs = [t for t in existing_tlvs if t['type'] != 0x10]
else:
# Ohne Overwrite: Bestehende JSON-Werte mit neuen mischen
existing_json = {}
for t in existing_tlvs:
if t['type'] == 0x10:
try: existing_json.update(json.loads(t['value'].decode('utf-8')))
except: pass
# Neues JSON einmischen
for nt in new_tlvs:
if nt['type'] == 0x10:
try: existing_json.update(json.loads(nt['value'].decode('utf-8')))
except: pass
existing_tlvs = [t for t in existing_tlvs if t['type'] != 0x10]
if existing_json:
existing_tlvs.append({'type': 0x10, 'index': 0x00, 'value': json.dumps(existing_json, ensure_ascii=False).encode('utf-8')})
# System-Tags (0x00) überschreiben alte direkt
new_sys_tlvs = [t for t in new_tlvs if t['type'] == 0x00]
for nt in new_sys_tlvs:
existing_tlvs = [t for t in existing_tlvs if not (t['type'] == nt['type'] and t['index'] == nt['index'])]
existing_tlvs.append(nt)
new_tlvs = existing_tlvs
blob = TagManager.build_blob(new_tlvs)
return self.send_blob(path, blob)
def send_blob(self, path: str, blob: bytes):
path_bytes = path.encode('utf-8')
req_payload = struct.pack('B', len(path_bytes)) + path_bytes + struct.pack('<I', len(blob))
self.bus.send_request(COMMANDS['put_tags'], req_payload)
self.bus.receive_ack(timeout=2.0)
if self.bus.send_stream(blob):
return True
return False
def print(self, result, path: str):
if result:
console.print(f"✓ Metadaten erfolgreich auf [info]{path}[/info] geschrieben.")

23
tool/core/cmd/reboot.py Normal file
View File

@@ -0,0 +1,23 @@
import serial
from core.utils import console, console_err
from core.protocol import COMMANDS
class reboot:
def __init__(self, bus):
self.bus = bus
def get(self):
self.bus.send_request(COMMANDS['reboot'])
try:
data = self.bus.receive_ack()
return data is not None and data.get('type') == 'ack'
except serial.SerialException:
# SerialException MUSS hier ignoriert werden, da der Controller
# den USB-Port beim Reboot hart schließt
return True
def print(self, result):
if result:
console.print("🔄 Neustart-Befehl erfolgreich gesendet. Controller [info]bootet neu...[/info]")
else:
console_err.print("❌ Fehler beim Senden des Neustart-Befehls.")

View File

@@ -0,0 +1,38 @@
import struct
from core.utils import console, console_err
from core.protocol import COMMANDS
class set_setting:
def __init__(self, bus):
self.bus = bus
def get(self, key: str, value: str):
key_bytes = key.encode('utf-8')
val_bytes = b''
# Typen-Konvertierung basierend auf dem Key
try:
if key == "audio/vol":
val_bytes = struct.pack('<B', int(value))
elif key == "play/norepeat":
val_int = 1 if str(value).lower() in ['1', 'true', 'on', 'yes'] else 0
val_bytes = struct.pack('<B', val_int)
elif key == "settings/storage_interval":
val_bytes = struct.pack('<H', int(value))
else:
console_err.print(f"[error]Unbekannter Key: {key}[/error]")
return False
except ValueError:
console_err.print(f"[error]Ungültiger Wert für {key}: {value}[/error]")
return False
# Payload: [Key Len] [Key] [Val Len] [Val Bytes]
payload = struct.pack('B', len(key_bytes)) + key_bytes + struct.pack('B', len(val_bytes)) + val_bytes
self.bus.send_request(COMMANDS['set_setting'], payload)
data = self.bus.receive_ack()
return data is not None and data.get('type') == 'ack'
def print(self, result, key: str, value: str):
if result:
console.print(f"✓ Setting [info]{key}[/info] wurde auf [info]{value}[/info] gesetzt.")

15
tool/core/cmd/stop.py Normal file
View File

@@ -0,0 +1,15 @@
from core.utils import console, console_err
from core.protocol import COMMANDS
class stop:
def __init__(self, bus):
self.bus = bus
def get(self):
self.bus.send_request(COMMANDS['stop'])
data = self.bus.receive_ack()
return data is not None and data.get('type') == 'ack'
def print(self, result):
if result:
console.print("⏹ Wiedergabe gestoppt und Warteschlange geleert.")

View File

@@ -2,7 +2,7 @@
VERSION = { VERSION = {
"min_protocol_version": 1, "min_protocol_version": 1,
"max_protocol_version": 1, "max_protocol_version": 1,
"current_protocol_version": None "current_protocol_version": None,
} }
SYNC_SEQ = b'BUZZ' SYNC_SEQ = b'BUZZ'
@@ -33,56 +33,45 @@ ERRORS = {
0x40: "NOT_IMPLEMENTED", 0x40: "NOT_IMPLEMENTED",
} }
FRAME_TYPE_REQUEST = 0x01
FRAME_TYPE_ACK = 0x10
FRAME_TYPE_RESPONSE = 0x11
FRAME_TYPE_STREAM_START = 0x12
FRAME_TYPE_STREAM_CHUNK = 0x13
FRAME_TYPE_STREAM_END = 0x14
FRAME_TYPE_LIST_START = 0x15
FRAME_TYPE_LIST_CHUNK = 0x16
FRAME_TYPE_LIST_END = 0x17
FRAME_TYPE_ERROR = 0xFF
FRAME_TYPES = { FRAME_TYPES = {
'request': FRAME_TYPE_REQUEST, 'request': 0x01,
'ack': FRAME_TYPE_ACK,
'response': FRAME_TYPE_RESPONSE, 'ack': 0x10,
'error': FRAME_TYPE_ERROR,
'stream_start': FRAME_TYPE_STREAM_START, 'response': 0x11,
'stream_chunk': FRAME_TYPE_STREAM_CHUNK, 'stream_start': 0x12,
'stream_end': FRAME_TYPE_STREAM_END, 'stream_chunk': 0x13,
'list_start': FRAME_TYPE_LIST_START, 'stream_end': 0x14,
'list_chunk': FRAME_TYPE_LIST_CHUNK, 'list_start': 0x15,
'list_end': FRAME_TYPE_LIST_END 'list_chunk': 0x16,
'list_end': 0x17,
'error': 0xFF,
} }
CMD_GET_PROTOCOL_VERSION = 0x00
CMD_GET_FIRMWARE_STATUS = 0x01
CMD_GET_FLASH_INFO = 0x02
CMD_LIST_DIR = 0x10
CMD_CRC_32 = 0x11
CMD_MKDIR = 0x12
CMD_RM = 0x13
CMD_STAT = 0x18
CMD_RENAME = 0x19
CMD_PUT_FILE = 0x20
CMD_PUT_FW = 0x21
CMD_GET_FILE = 0x22
COMMANDS = { COMMANDS = {
'get_protocol_version': CMD_GET_PROTOCOL_VERSION, 'get_protocol_version': 0x00,
'get_firmware_status': CMD_GET_FIRMWARE_STATUS, 'get_firmware_status': 0x01,
'get_flash_info': CMD_GET_FLASH_INFO, 'get_flash_info': 0x02,
'list_dir': CMD_LIST_DIR, 'confirm_fw': 0x03,
'stat': CMD_STAT, 'reboot': 0x04,
'rm': CMD_RM,
'rename': CMD_RENAME, 'list_dir': 0x10,
'mkdir': CMD_MKDIR, 'crc_32': 0x11,
'crc_32': CMD_CRC_32, 'mkdir': 0x12,
'put_file': CMD_PUT_FILE, 'rm': 0x13,
'get_file': CMD_GET_FILE, 'stat': 0x18,
'put_fw': CMD_PUT_FW, '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,
} }

View File

@@ -120,6 +120,40 @@ class SerialBus:
full_frame += payload full_frame += payload
self.send_binary(full_frame) self.send_binary(full_frame)
def send_stream(self, data: bytes, chunk_size: int = 4096, progress_callback=None):
"""Sendet einen Datenstrom in Chunks und wartet auf die Bestätigung (CRC)."""
start_time = time.time()
size = len(data)
sent_size = 0
while sent_size < size:
chunk = data[sent_size:sent_size+chunk_size]
self.connection.write(chunk)
sent_size += len(chunk)
if progress_callback:
progress_callback(sent_size, size)
if not self.wait_for_sync(SYNC_SEQ, max_time=self.timeout + 5.0):
raise TimeoutError("Timeout beim Warten auf Stream-Ende (Flash ist evtl. noch beschäftigt).")
ftype = self._read_exact(1, "Frame-Typ")[0]
if ftype == FRAME_TYPES['stream_end']:
end_time = time.time()
crc32 = struct.unpack('<I', self._read_exact(4, "CRC32"))[0]
return {
'crc32': crc32,
'duration': end_time - start_time
}
elif ftype == FRAME_TYPES['error']:
err_code_raw = self.connection.read(1)
err_code = err_code_raw[0] if err_code_raw else 0xFF
err_name = ERRORS.get(err_code, "UNKNOWN")
raise ControllerError(err_code, err_name)
else:
raise ValueError(f"Unerwarteter Frame-Typ nach Stream-Upload: 0x{ftype:02X}")
def receive_ack(self, timeout: float = None): def receive_ack(self, timeout: float = None):
wait_time = timeout if timeout is not None else self.timeout wait_time = timeout if timeout is not None else self.timeout
@@ -212,42 +246,46 @@ class SerialBus:
err_name = ERRORS.get(err_code, "UNKNOWN") err_name = ERRORS.get(err_code, "UNKNOWN")
raise ControllerError(err_code, err_name) raise ControllerError(err_code, err_name)
def receive_stream(self, chunk_size: int = 1024): def receive_stream(self, chunk_size: int = 1024, progress_callback=None):
"""Liest einen Datenstrom in Chunks, bis ein Fehler oder Ende-Signal kommt.""" """Liest einen Datenstrom in Chunks, bis ein Fehler oder Ende-Signal kommt."""
is_stream = False is_stream = False
data_chunks = [] data_chunks = []
start_time = None
while True: while True:
if not self.wait_for_sync(SYNC_SEQ): if not self.wait_for_sync(SYNC_SEQ):
raise TimeoutError("Timeout beim Warten auf Sync im Stream-Modus.") raise TimeoutError("Timeout beim Warten auf Sync im Stream-Modus.")
ftype = self._read_exact(1, "Frame-Typ")[0] ftype = self._read_exact(1, "Frame-Typ")[0]
if self.debug: console.print(f"Empfangener Frame-Typ: 0x{ftype:02X} (start: 0x{FRAME_TYPES['stream_start']:02X}, chunk: 0x{FRAME_TYPES['stream_chunk']:02X}, end: 0x{FRAME_TYPES['stream_end']:02X})")
if ftype == FRAME_TYPES['stream_start']: if ftype == FRAME_TYPES['stream_start']:
is_stream = True is_stream = True
data_chunks = [] data_chunks = []
size = struct.unpack('<I', self._read_exact(4, "Stream-Größe"))[0] size = struct.unpack('<I', self._read_exact(4, "Stream-Größe"))[0]
if self.debug: console.print(f"Stream gestartet, erwartete Gesamtgröße: {size} Bytes") start_time = time.time()
received_size = 0 received_size = 0
while received_size < size: while received_size < size:
chunk_length = min(chunk_size, size - received_size) chunk_length = min(chunk_size, size - received_size)
try:
chunk_data = self._read_exact(chunk_length, f"Daten-Chunk @ {received_size}/{size}") chunk_data = self._read_exact(chunk_length, f"Daten-Chunk @ {received_size}/{size}")
except Exception as e:
raise IOError(f"Stream-Abbruch bei {received_size}/{size} Bytes: {e}") from e
data_chunks.append(chunk_data) data_chunks.append(chunk_data)
received_size += len(chunk_data) received_size += len(chunk_data)
if self.debug: console.print(f"Empfangen: {received_size}/{size} Bytes ({(received_size/size)*100:.2f}%)")
# Callback für UI-Update (z.B. Progress Bar)
if progress_callback:
progress_callback(received_size, size)
if self.debug: console.print("Stream vollständig empfangen.") if self.debug: console.print("Stream vollständig empfangen.")
elif ftype == FRAME_TYPES['stream_end']: elif ftype == FRAME_TYPES['stream_end']:
end_time = time.time()
if not is_stream: raise ValueError("Ende ohne Start.") if not is_stream: raise ValueError("Ende ohne Start.")
crc32 = struct.unpack('<I', self._read_exact(4, "CRC32"))[0] crc32 = struct.unpack('<I', self._read_exact(4, "CRC32"))[0]
if self.debug: console.print(f"Stream-Ende empfangen, CRC32: 0x{crc32:08X}") return {
'data': b''.join(data_chunks),
return {'data': b''.join(data_chunks), 'crc32': crc32} 'crc32': crc32,
'duration': end_time - start_time if start_time and end_time else None
}
# elif ftype == FRAME_TYPES['list_chunk']: # elif ftype == FRAME_TYPES['list_chunk']:
# if not is_list: raise ValueError("Chunk ohne Start.") # if not is_list: raise ValueError("Chunk ohne Start.")

95
tool/core/tag.py Normal file
View File

@@ -0,0 +1,95 @@
# tool/core/tag.py
import struct
import json
class TagManager:
FOOTER_MAGIC = b'TAG!'
FOOTER_SIZE = 8
FORMAT_VERSION = 1
@classmethod
def split_file(cls, file_data: bytes):
"""Trennt eine Datei in reine Audiodaten und eine Liste von TLVs auf."""
if len(file_data) < cls.FOOTER_SIZE:
return file_data, []
footer = file_data[-cls.FOOTER_SIZE:]
total_size, version, magic = struct.unpack('<HH4s', footer)
if magic != cls.FOOTER_MAGIC or version != cls.FORMAT_VERSION:
return file_data, []
if total_size > 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('<BBH', tag_data[pos:pos+4])
pos += 4
if pos + length > 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('<BBH', tlv['type'], tlv['index'], len(tlv['value']))
payload += tlv['value']
total_size = len(payload) + cls.FOOTER_SIZE
footer = struct.pack('<HH4s', total_size, cls.FORMAT_VERSION, cls.FOOTER_MAGIC)
return payload + footer
@classmethod
def parse_cli_json(cls, json_str: str):
"""Konvertiert den Kommandozeilen-JSON-String in neue TLVs."""
try:
data = json.loads(json_str)
except json.JSONDecodeError as e:
raise ValueError(f"Ungültiges JSON-Format: {e}")
new_tlvs = []
# 1. System Tags (0x00)
if "system" in data:
sys_data = data["system"]
if "format" in sys_data:
fmt = sys_data["format"]
if isinstance(fmt, str) and fmt.startswith("0x"):
val = bytes.fromhex(fmt[2:])
else:
val = struct.pack('<BBHI', fmt.get("codec", 0), fmt.get("bit_depth", 16), 0, fmt.get("samplerate", 16000))
new_tlvs.append({'type': 0x00, 'index': 0x00, 'value': val})
if "crc32" in sys_data:
crc_str = sys_data["crc32"]
crc_val = int(crc_str, 16) if isinstance(crc_str, str) else crc_str
new_tlvs.append({'type': 0x00, 'index': 0x01, 'value': struct.pack('<I', crc_val)})
# 2. JSON Tags (0x10)
if "json" in data:
# Bei leerem JSON-Objekt ("{}") wird kein 0x10 TLV erstellt
if data["json"]:
json_bytes = json.dumps(data["json"], ensure_ascii=False).encode('utf-8')
new_tlvs.append({'type': 0x10, 'index': 0x00, 'value': json_bytes})
return new_tlvs

View File

@@ -1,50 +1,143 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { bus } from '../lib/bus/SerialBus';
import { buzzer } from '../lib/buzzerStore'; import { buzzer } from '../lib/buzzerStore';
import { connectToPort, disconnectBuzzer,initSerialListeners } from '../lib/buzzerActions'; import { GetProtocolCommand } from '../lib/protocol/commands/GetProtocol';
import { PlugsIcon, PlugsConnectedIcon } from 'phosphor-svelte'; 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 = [ const BUZZER_FILTER = [{ usbVendorId: 0x1209, usbProductId: 0xEDED }];
{ usbVendorId: 0x2fe3, usbProductId: 0x0001 },
];
onMount(() => {
initSerialListeners();
});
async function handleConnectClick() { let showMenu = false;
try { let menuElement: HTMLElement;
if ($buzzer.connected) {
console.log("Trenne verbindung zum aktuellen Buzzer..."); // Schließt das Menü bei Klick außerhalb
await disconnectBuzzer(); function handleOutsideClick(event: MouseEvent) {
console.log("Verbindung getrennt"); if (showMenu && menuElement && !menuElement.contains(event.target as Node)) {
showMenu = false;
}
} }
const port = await navigator.serial.requestPort({ filters: BUZZER_FILTER }); async function connectTo(port: SerialPort) {
console.log("Port ausgewählt, versuche Verbindung.", port.getInfo()); try {
await connectToPort(port); await bus.connect(port);
} catch (e) { // Kurze Pause für die Hardware-Bereitschaft
// Verhindert das Error-Logging, wenn der User einfach nur "Abbrechen" klickt await new Promise(r => setTimeout(r, 100));
if (e instanceof Error && e.name === 'NotFoundError') {
console.log("Keine Verbindung ausgewählt, Abbruch durch Nutzer."); // 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; return;
} }
console.error("Verbindung abgebrochen", e);
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);
});
</script> </script>
<button <div class="relative inline-flex shadow-lg" bind:this={menuElement}>
on:click={handleConnectClick} <button
class="flex items-center gap-2 px-4 py-2 rounded-xl transition-all on:click={handleMainAction}
class="flex items-center gap-3 px-5 py-2.5 rounded-l-xl transition-all border-r border-white/10
{$buzzer.connected {$buzzer.connected
? 'bg-emerald-500/20 text-emerald-400 border border-emerald-500/50' ? 'bg-emerald-600 hover:bg-emerald-500 text-white'
: 'bg-slate-700 hover:bg-slate-600 text-slate-200 border border-slate-600'}" : 'bg-slate-700 hover:bg-slate-600 text-slate-200'}"
> >
{#if $buzzer.connected} {#if $buzzer.connected}
<PlugsConnectedIcon size={18} weight="fill" /> <PlugsConnectedIcon size={20} weight="fill" />
<span class="text-xs font-bold uppercase tracking-wider text-emerald-300">Verbunden</span> <span class="text-sm font-bold uppercase tracking-wide">Trennen</span>
{:else} {:else}
<PlugsIcon size={18} weight="bold" /> <PlugsIcon size={20} weight="bold" />
<span class="text-xs font-bold uppercase tracking-wider">Verbinden</span> <span class="text-sm font-bold uppercase tracking-wide">Verbinden</span>
{/if} {/if}
</button> </button>
<button
on:click={() => showMenu = !showMenu}
class="px-3 py-2.5 rounded-r-xl transition-all
{$buzzer.connected
? 'bg-emerald-600 hover:bg-emerald-500 text-white'
: 'bg-slate-700 hover:bg-slate-600 text-slate-200'}"
>
<CaretDownIcon size={16} weight="bold" class="transition-transform {showMenu ? 'rotate-180' : ''}" />
</button>
{#if showMenu}
<div
transition:slide={{ duration: 150 }}
class="absolute top-full right-0 mt-2 w-56 bg-slate-800 border border-slate-700 rounded-xl overflow-hidden z-50 shadow-2xl"
>
<div class="p-2 flex flex-col gap-1">
<button
on:click={pairNewDevice}
class="flex items-center gap-3 w-full px-3 py-2 text-sm text-slate-300 hover:bg-slate-700 rounded-lg transition-colors"
>
<PlusCircleIcon size={18} class="text-emerald-400" />
Neuen Buzzer koppeln
</button>
<div class="h-px bg-slate-700 my-1"></div>
<button
on:click={forgetDevice}
class="flex items-center gap-3 w-full px-3 py-2 text-sm text-rose-400 hover:bg-rose-500/10 rounded-lg transition-colors"
>
<TrashIcon size={18} />
Buzzer entkoppeln
</button>
</div>
</div>
{/if}
</div>

View File

@@ -1,8 +1,15 @@
<script> <script>
import { buzzer } from '../lib/buzzerStore'; import { buzzer } from '../lib/buzzerStore';
import { CpuIcon } from 'phosphor-svelte';
</script> </script>
<div class="h-48 bg-indigo-950/20 border border-indigo-500/20 rounded-[2rem] p-6 relative overflow-hidden shrink-0">
<div class="text-[11px] font-mono text-slate-400 space-y-1 relative z-10"> <div class="absolute top-6 right-6 text-indigo-500 opacity-20">
<CpuIcon class="w-12 h-12" weight="fill" />
</div>
<h3 class="text-indigo-400 text-[10px] font-black uppercase tracking-[0.2em] mb-4">
Device Info
</h3>
<div class="text-[11px] font-mono text-slate-400 space-y-1 relative z-10 transition-all duration-300 {!$buzzer.connected ? 'blur-[1px] opacity-30 grayscale pointer-events-none' : ''}">
<p> <p>
Firmware: <span class="text-indigo-300">{$buzzer.version}</span> Firmware: <span class="text-indigo-300">{$buzzer.version}</span>
</p> </p>
@@ -14,4 +21,5 @@
{$buzzer.connected ? 'Confirmed' : 'Disconnected'} {$buzzer.connected ? 'Confirmed' : 'Disconnected'}
</span> </span>
</p> </p>
</div>
</div> </div>

View File

@@ -14,7 +14,7 @@
$: isDisconnected = !$buzzer.connected; $: isDisconnected = !$buzzer.connected;
</script> </script>
<div class="flex flex-col gap-1.5 w-96 transition-all duration-700 {isDisconnected ? 'blur-sm opacity-30 grayscale pointer-events-none' : ''}"> <div class="flex flex-col gap-1.5 w-96 transition-all duration-300 {isDisconnected ? 'blur-[1px] opacity-30 grayscale pointer-events-none' : ''}">
<div class="h-3.5 w-full bg-slate-800 rounded-full overflow-hidden flex border border-slate-700 shadow-inner"> <div class="h-3.5 w-full bg-slate-800 rounded-full overflow-hidden flex border border-slate-700 shadow-inner">
<div class="h-full bg-slate-300 transition-all duration-500" style="width: {pMeta}%"></div> <div class="h-full bg-slate-300 transition-all duration-500" style="width: {pMeta}%"></div>

View File

@@ -1,10 +1,31 @@
<script lang="ts"> <script lang="ts">
import { buzzer } from '../lib/buzzerStore'; import { buzzer } from '../lib/buzzerStore';
import { playFile, deleteFile } from '../lib/buzzerActions'; import { InfoIcon, MusicNotesIcon, WrenchIcon, PlayIcon, TrashIcon, ArrowsLeftRightIcon, QuestionMarkIcon } from "phosphor-svelte";
import { MusicNotesIcon, WrenchIcon, PlayIcon, TrashIcon, ArrowsLeftRightIcon, QuestionMarkIcon } from "phosphor-svelte"; import { PlayFileCommand } from '../lib/protocol/commands/PlayFile';
export let file: { name: string, size: string, isSystem: boolean, crc32?: number}; export let file: { name: string, size: string, isSystem: boolean, crc32?: number};
export let selected = false; export let selected = false;
async function handlePlay() {
try {
const cmd = new PlayFileCommand();
// 1. WICHTIG: Der Buzzer braucht den absoluten Pfad!
const fullPath = `/lfs/a/${file.name}`;
console.log("Sende Play-Befehl für:", fullPath);
const success = await cmd.execute(fullPath);
if (success) {
// Optional: Erfolg kurz im Log zeigen
console.log("Wiedergabe läuft...");
} else {
addToast(`Buzzer konnte ${file.name} nicht abspielen.`, "error");
}
} catch (e: any) {
addToast(`Fehler: ${e.message}`, "error");
}
}
</script> </script>
<div <div
@@ -48,17 +69,24 @@
<div class="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity shrink-0 ml-4"> <div class="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity shrink-0 ml-4">
<button <button
on:click|stopPropagation={() => playFile(file.name)} class="p-2 hover:bg-gray-500/20 rounded-lg text-white-400 transition-colors"
title="Datei-Infos"
>
<InfoIcon size={16} />
</button>
<button
on:click|stopPropagation={handlePlay}
class="p-2 hover:bg-blue-500/20 rounded-lg text-blue-400 transition-colors" class="p-2 hover:bg-blue-500/20 rounded-lg text-blue-400 transition-colors"
title="Play Sound" title="Auf dem Buzzer abspielen"
> >
<PlayIcon size={16} weight="fill" /> <PlayIcon size={16} weight="fill" />
</button> </button>
<button <button
on:click|stopPropagation={() => deleteFile(file.name)} // on:click|stopPropagation={() => deleteFile(file.name)}
class="p-2 hover:bg-red-500/20 rounded-lg text-red-400 transition-colors" class="p-2 hover:bg-red-500/20 rounded-lg text-red-400 transition-colors"
title="Delete File" title="Datei vom Buzzer löschen"
> >
<TrashIcon size={16} weight="fill" /> <TrashIcon size={16} weight="fill" />
</button> </button>

View File

@@ -1,10 +1,10 @@
<script lang="ts"> <script lang="ts">
import { buzzer } from '../lib/buzzerStore'; import { buzzer } from '../lib/buzzerStore';
import { refreshFileList } from '../lib/buzzerActions';
import FileRow from './FileRow.svelte'; import FileRow from './FileRow.svelte';
import { ArrowsCounterClockwiseIcon } from 'phosphor-svelte'; import { ArrowsCounterClockwiseIcon } from 'phosphor-svelte';
import { refreshFileList } from '../lib/buzzerActions';
async function handleRefresh() { async function handleRefresh() {
console.log("Aktualisiere Dateiliste...");
await refreshFileList(); await refreshFileList();
} }
</script> </script>

View File

@@ -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<Uint8Array> | 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<boolean> {
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<Uint8Array> {
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);
};
}

View File

@@ -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<Uint8Array> | null = null;
private writer: WritableStreamDefaultWriter<Uint8Array> | null = null;
setPort(port: SerialPort | null) { this.port = port; }
async add(command: string, priority = 1, key?: string): Promise<string[]> {
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() { export async function initializeBuzzer() {
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 { try {
// console.log("Auto-Connect Versuch mit Port:", port.getInfo()); const version = await new GetProtocolCommand().execute();
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.');
}
}
}
}
}
/** if (version !== null) {
* Kernfunktion für den Verbindungsaufbau buzzer.update(s => ({ ...s, connected: true, protocol: version }));
*/
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<boolean>((_, 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");
// FIX 1: Flash-Info muss auch beim Start geladen werden!
await refreshFileList(); await refreshFileList();
} catch (validationError) { await refreshFlashInfo();
addToast("Buzzer-Validierung fehlgeschlagen!", "error");
await disconnectBuzzer();
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;
}
export async function refreshFlashInfo() {
try { try {
if ('forget' in port) { // Check für Browser-Support const flashInfo = await new GetFlashInfoCommand().execute();
await (port as any).forget(); if (flashInfo) {
console.log("Gerät wurde erfolgreich entkoppelt."); const totalSize = (flashInfo.total_size / (1024 * 1024));
} const freeSize = (flashInfo.free_size / (1024 * 1024));
} catch (forgetError) { const fwSlotSize = (flashInfo.fw_slot_size / 1024);
console.error("Entkoppeln fehlgeschlagen:", forgetError); const maxPathLength = flashInfo.max_path_length;
}
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;
}
}
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<boolean> {
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 => ({ buzzer.update(s => ({
...s, ...s,
version: parts[1], storage: { total: totalSize, available: freeSize }, // FIX 2: "storage" korrekt geschrieben
protocol: parseInt(parts[0]), fw_slot_size: fwSlotSize,
storage: { ...s.storage, total: totalMB, available: availableMB } max_path_length: maxPathLength
})); }));
return true; // Validierung erfolgreich
} }
} catch (e) { // FIX 3: try/catch Block sauber schließen
console.error("Fehler beim Abrufen der Flash-Info:", e);
} }
return false; // Keine gültigen Daten erhalten } // Funktion schließen
}
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();
}

View File

@@ -2,15 +2,18 @@ import { writable } from 'svelte/store';
export const buzzer = writable({ export const buzzer = writable({
connected: false, connected: false,
version: 'v0.0.0',
protocol: 0, protocol: 0,
build: 'unknown', version: 'v0.0.0',
kernel_version: 'v0.0.0',
storage: { storage: {
total: 8.0, // 8 MB Flash laut Spezifikation total: 8.0,
available: 0.0, available: 0.0,
unknown: 8.0,
usedSys: 0.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}[]
}); });

View File

@@ -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<FlashInfo | null> {
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;
}
}

View File

@@ -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<number | null> {
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;
}
}

View File

@@ -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<number | boolean | null> {
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;
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,31 @@
import { bus } from '../../bus/SerialBus';
import { Command, FrameType } from '../constants';
export class PlayFileCommand {
async execute(path: string): Promise<boolean | null> {
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;
}
}

View File

@@ -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<number, string> = {
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,
};

View File

@@ -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;
}
}

View File

@@ -67,17 +67,7 @@ import type { loadRenderers } from "astro:container";
</section> </section>
<section class="flex-1 flex flex-col gap-6 min-h-0"> <section class="flex-1 flex flex-col gap-6 min-h-0">
<div
class="h-48 bg-indigo-950/20 border border-indigo-500/20 rounded-[2rem] p-6 relative overflow-hidden shrink-0">
<div class="absolute top-6 right-6 text-indigo-500 opacity-20">
<Icon name="ph:cpu-fill" class="w-12 h-12" />
</div>
<h3 class="text-indigo-400 text-[10px] font-black uppercase tracking-[0.2em] mb-4">
Device Info
</h3>
<DeviceInfo client:load /> <DeviceInfo client:load />
</div>
<div class="flex-1 flex flex-col overflow-hidden"> <div class="flex-1 flex flex-col overflow-hidden">
<FileStorage client:load /> <FileStorage client:load />
</div> </div>