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/audio.c
src/usb.c
src/uart.c
src/protocol.c
src/utils.c
src/settings.c
)
zephyr_include_directories(src)
zephyr_include_directories(include)

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
- `metadata`: Folge von Tag-Einträgen
- `tag_version_u8`: 1 Byte Versionsnummer des Tag-Formats
- `footer_len_le16`: 2 Byte, Little Endian
- `"TAG!"`: 4 Byte Magic (`0x54 0x41 0x47 0x21`)
| Offset | Feld | Typ | Beschreibung |
| :--- | :--- | :--- | :--- |
| 0 | `total_size` | `uint16_t` | Gesamtgröße in Bytes (Summe aller TLV-Blöcke + 8 Bytes Footer). |
| 2 | `version` | `uint16_t` | Format-Version. Aktuell `0x0001`. |
| 4 | `magic` | `char[4]` | Fixe Signatur: `"TAG!"` (Hex: `54 41 47 21`). |
## 2) Bedeutung von `footer_len_le16`
---
`footer_len_le16` ist die **Gesamtlänge des Footers**, also:
## 2. TLV-Header (Type-Length-Value)
Jeder Metadaten-Block beginnt mit einem exakt 4 Bytes großen Header. Unbekannte Typen können vom Controller durch einen relativen Sprung (`fs_seek` um `length` Bytes) übersprungen werden.
`footer_len = metadata_len + 1 + 2 + 4`
| Offset | Feld | Typ | Beschreibung |
| :--- | :--- | :--- | :--- |
| 0 | `type` | `uint8_t` | Definiert den Inhalt des Blocks (siehe Typen-Definitionen). |
| 1 | `index` | `uint8_t` | Erlaubt die Fragmentierung großer Datensätze (z.B. bei JSON > 64 KB). Standard: `0x00`. |
| 2 | `length` | `uint16_t` | Größe der folgenden Payload in Bytes (ohne diesen Header). |
Damit beginnt `metadata` bei:
---
`metadata_start = file_size - footer_len`
## 3. Typen-Definitionen
Das passt zur aktuellen Implementierung in der Firmware.
### Type `0x00`: Binary System Metadata
Dieser Typ gruppiert maschinenlesbare, binäre Systeminformationen. Die Unterscheidung erfolgt über das `Index`-Feld.
### Tag-Version
#### Index `0x00`: Audio Format
Dieser Block konfiguriert den I2S-Treiber vor der Wiedergabe.
* **Typ:** `0x00`
* **Index:** `0x00`
* **Länge:** `0x0008` (8 Bytes)
* **Payload:** `[codec: 1 Byte] [bit_depth: 1 Byte] [reserved: 2 Bytes] [samplerate: 4 Bytes]`
- `tag_version` ist aktuell `0x01`.
- Der Host darf nur bekannte Versionen interpretieren.
- Bei unbekannter Version: Tag-Block ignorieren oder als "nicht unterstützt" melden.
#### Index `0x01`: Audio CRC32
Speichert die CRC32-Prüfsumme (IEEE) der reinen Audiodaten (vom Dateianfang bis zum Beginn des ersten TLV-Blocks). Dient Synchronisations-Tools für einen schnellen Integritäts- und Abgleich-Check, ohne die gesamte Datei neu hashen zu müssen.
* **Typ:** `0x00`
* **Index:** `0x01`
* **Länge:** `0x0004` (4 Bytes)
* **Payload:** `uint32_t` (Little-Endian)
## 3) Endianness und Typen
### Type `0x10`: JSON Metadata
Dieser Block enthält Metadaten, die primär für das Host-System (z. B. das Python-Tool) zur Verwaltung, Kategorisierung und Anzeige bestimmt sind. Der Mikrocontroller ignoriert und überspringt diesen Block während der Audiowiedergabe.
- Alle Multi-Byte-Werte sind **Little Endian**.
- Tag-Einträge sind TLV-basiert:
- `type`: `uint8_t`
- `len`: `uint16_t`
- `value`: `byte[len]`
* **Typ:** `0x10`
* **Länge:** Variabel
* **Payload:** UTF-8-kodierter JSON-String (ohne Null-Terminator).
Dadurch können auch unbekannte Typen sauber übersprungen werden.
#### Standardisierte JSON-Schlüssel
Die nachfolgenden Schlüssel (Keys) sind im Basis-Standard definiert. Die Integration weiterer, proprietärer Schlüssel ist technisch möglich. Es wird jedoch empfohlen, dies mit Vorsicht zu handhaben, da zukünftige Standardisierungen diese Schlüsselnamen belegen könnten (Namenskollision).
## 4) Unterstützte Tag-Typen
| Schlüssel | Datentyp | Beschreibung |
| :--- | :--- | :--- |
| `t` | String | Titel der Audiodatei |
| `a` | String | Autor oder Ersteller |
| `r` | String | Bemerkungen (Remarks) oder Beschreibung |
| `c` | Array of Strings | Kategorien zur Gruppierung |
| `dc` | String | Erstellungsdatum (Date Created), idealerweise nach ISO 8601 |
| `ds` | String | Speicher- oder Änderungsdatum (Date Saved), idealerweise nach ISO 8601 |
Aktuell definierte Typen:
**Beispiel-Payload:**
Ein vollständiger JSON-Datensatz gemäß dieser Spezifikation hat folgendes Format:
- `0x00`: `DESCRIPTION` (Beschreibung des Samples)
- `0x01`: `AUTHOR`
- `0x10`: `CRC32_RAW`
- `0x20`: `FILE_FORMAT` (Info für Host, Player wertet derzeit nicht aus)
```json
{
"t": "Testaufnahme System A",
"a": "Entwickler-Team",
"r": "Überprüfung der Mikrofon-Aussteuerung.",
"c": ["Test", "Audio", "V1"],
"dc": "2026-03-05T13:00:00Z",
"ds": "2026-03-05T13:10:00Z"
}
```
## 5) Value-Format pro Tag
*(Hinweis zur Skalierbarkeit: Für zukünftige Erweiterungen können dedizierte TLV-Typen definiert werden, wie beispielsweise 0x11 für GZIP-komprimierte JSON-Daten oder 0x20 für binäre Bilddaten wie PNG-Cover).*
### 5.1 `0x00` DESCRIPTION
---
- `value`: UTF-8-Text
- `len`: Anzahl Bytes des UTF-8-Texts
## 4. Lese-Algorithmus (Parser-Logik)
### 5.2 `0x01` AUTHOR
Der Controller extrahiert die Hardware-Parameter nach folgendem Ablauf:
- `value`: UTF-8-Text
- `len`: Anzahl Bytes des UTF-8-Texts
1. **Footer lokalisieren:** * Gehe zu `EOF - 8`. Lese 8 Bytes in das `tag_footer_t` Struct.
* Validiere `magic == "TAG!"` und `version == 0x0001` (unter Berücksichtigung von Little-Endian Konvertierung via `sys_le16_to_cpu`).
2. **Grenzen berechnen:**
* Lese `total_size`.
* Die reinen Audiodaten enden bei `audio_limit = EOF - total_size`.
* Gehe zur Position `audio_limit`.
3. **TLV-Blöcke iterieren:**
* Solange die aktuelle Leseposition kleiner als `EOF - 8` ist:
* Lese 4 Bytes in den `tlv_header_t`.
* Wenn `type == 0x00`: Lese die nächsten 8 Bytes in das `tlv_audio_format_t` Struct.
* Wenn `type != 0x00`: Führe `fs_seek(header.length, FS_SEEK_CUR)` aus.
### 5.3 `0x10` CRC32_RAW
---
- `value`: `uint32_t crc32` (4 Byte, Little Endian)
- `len`: **muss 4** sein
## 5. Hex-Beispiel
### 5.4 `0x20` FILE_FORMAT
Eine fiktive Datei enthält Audio-Daten. Es soll ein PCM-Mono Format (16 Bit, 16 kHz) sowie ein kurzes JSON `{"t":"A"}` (9 Bytes) angehängt werden.
- `value`:
- `bits_per_sample`: `uint8_t`
- `sample_rate`: `uint32_t` (Little Endian)
- `len`: **muss 5** sein
**1. TLV 0x00 (Audio Format):**
* Header: `00 00 08 00` (Type 0, Index 0, Length 8)
* Payload: `00 10 00 00 80 3E 00 00` (Mono, 16-Bit, Reserved, 16000 Hz)
Beispielwerte aktuell oft: `bits_per_sample = 16`, `sample_rate = 16000`.
**2. TLV 0x10 (JSON):**
* Header: `10 00 09 00` (Type 16, Index 0, Length 9)
* Payload: `7B 22 74 22 3A 22 41 22 7D` (`{"t":"A"}`)
## 6) Vorkommen je Typ
Aktueller Stand: **jeder Tag-Typ darf maximal 1x vorkommen**.
Empfohlene Host-Regel:
- Falls ein Typ mehrfach vorkommt, letzte Instanz gewinnt (`last-wins`) und ein Warnhinweis wird geloggt.
## 7) Validierungsregeln (Host)
Beim Lesen:
1. Prüfen, ob Datei mindestens 7 Byte hat.
2. Letzte 6 Byte prüfen: `footer_len_le16` + `TAG!`.
3. `footer_len` gegen Dateigröße validieren (`6 <= footer_len <= file_size`).
4. `tag_version` an Position `file_size - 6 - 1` lesen und validieren.
5. Im Metadatenbereich TLV-Einträge lesen, bis Ende erreicht.
6. Für bekannte Typen feste Längen prüfen (`CRC32_RAW=4`, `FILE_FORMAT=5`).
7. Unbekannte Typen über `len` überspringen.
Beim Schreiben:
1. Vorhandene Tags entfernen/ersetzen (audio-Ende bestimmen).
2. Neue TLV-Metadaten schreiben.
3. `tag_version_u8` schreiben (`0x01`).
4. `footer_len_le16` schreiben (inkl. 1+2+4).
5. `TAG!` schreiben.
5. Datei auf neue Länge truncaten.
## 8) Beispiel (hex)
Beispiel mit:
- DESCRIPTION = "Kick"
- AUTHOR = "Edi"
- CRC32_RAW = `0x12345678`
TLV-Daten:
- `00 04 00 4B 69 63 6B`
- `01 03 00 45 64 69`
- `10 04 00 78 56 34 12`
`metadata_len = 7 + 6 + 7 = 20 (0x0014)`
`footer_len = 20 + 1 + 2 + 4 = 27 (0x001B)`
Footer-Ende:
- `01 1B 00 54 41 47 21`
## 9) Hinweis zur aktuellen Firmware
Die Firmware verarbeitet Tag-Payload direkt binär (Chunk-Streaming über das Protokoll). Das dateiinterne Format entspricht direkt diesem Dokument.
**3. Footer:**
* Total Size: `2D 00` (45 Bytes = 12 Bytes Audio-TLV + 13 Bytes JSON-TLV + 12 Bytes Padding/Zusatz + 8 Bytes Footer) -> *Hinweis: Size ist in diesem Konstrukt abhängig vom genauen Payload.*
* Version: `01 00`
* Magic: `54 41 47 21` (`TAG!`)

View File

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

View File

@@ -10,6 +10,37 @@ typedef struct slot_info_t {
size_t size;
} slot_info_t;
typedef enum {
FS_MSG_START,
FS_MSG_CHUNK,
FS_MSG_EOF,
FS_MSG_ABORT
} fs_msg_type_t;
typedef struct {
fs_msg_type_t type;
/* Die Union spart RAM, da Start- und Chunk-Parameter
nie gleichzeitig im selben Message-Paket benötigt werden. */
union {
/* Payload für FS_MSG_START */
struct {
/* Der String wird sicher in die Queue kopiert */
char filename[MAX_PATH_LEN];
uint32_t expected_size;
uint32_t start_position;
} start;
/* Payload für FS_MSG_CHUNK */
struct {
void *slab_ptr;
uint32_t chunk_size;
} chunk;
};
} fs_msg_t;
extern struct k_msgq fs_msgq;
/**
* @brief Initializes the filesystem by mounting it
*/
@@ -102,6 +133,21 @@ int fs_pm_mkdir(const char *path);
*/
int fs_pm_rename(const char *old_path, const char *new_path);
/**
* @brief Recursively creates directories for the given path, ensuring the flash is active during the operation
* @param path Path to the directory to create (can include multiple levels, e.g. "/dir1/dir2/dir3")
* @return 0 on success, negative error code on failure
*/
int fs_pm_mkdir_recursive(char *path);
/**
* @brief Recursively removes a directory and all its contents, ensuring the flash is active during the operation
* @param path Path to the directory to remove
* @param max_len Maximum length of the path buffer
* @return 0 on success, negative error code on failure
*/
int fs_pm_rm_recursive(char *path, size_t max_len);
/**
* @brief Gets the length of the audio data in a file, accounting for any metadata tags
* @param fp Pointer to an open fs_file_t structure representing the audio file
@@ -138,29 +184,15 @@ int fs_tag_open_read(struct fs_file_t *fp, uint8_t *version, size_t *payload_len
ssize_t fs_tag_read_chunk(struct fs_file_t *fp, void *buffer, size_t len);
/**
* @brief Positions file pointer for tag payload overwrite at end of audio data.
* @param fp Pointer to an open fs_file_t structure representing the audio file
* @return 0 on success, negative error code on failure
* @brief Setzt die Synchronisation für einen neuen Dateitransfer zurück.
*/
int fs_tag_open_write(struct fs_file_t *fp);
void fs_reset_transfer_sync(void);
/**
* @brief Writes a raw tag payload chunk.
* @param fp Pointer to an open fs_file_t positioned for tag payload write
* @param buffer Source buffer
* @param len Number of bytes to write
* @return Number of bytes written, negative error code on failure
* @brief Blockiert den aufrufenden Thread, bis der FS-Thread den Transfer
* (EOF oder ABORT) vollständig auf dem Flash abgeschlossen hat.
*/
ssize_t fs_tag_write_chunk(struct fs_file_t *fp, const void *buffer, size_t len);
/**
* @brief Finalizes tags by appending version + footer and truncating file.
* @param fp Pointer to an open fs_file_t structure representing the audio file
* @param version Tag format version to write
* @param payload_len Tag payload length in bytes
* @return 0 on success, negative error code on failure
*/
int fs_tag_finish_write(struct fs_file_t *fp, uint8_t version, size_t payload_len);
void fs_wait_for_transfer_complete(void);
/**
* @brief Retrieves information about the firmware slot, such as start address and size

View File

@@ -30,6 +30,14 @@ typedef enum {
CMD_PUT_FILE = 0x20,
CMD_PUT_FW = 0x21,
CMD_GET_FILE = 0x22,
CMD_PUT_TAGS = 0x24,
CMD_GET_TAGS = 0x25,
CMD_PLAY = 0x30,
CMD_STOP = 0x31,
CMD_SET_SETTING = 0x40,
CMD_GET_SETTING = 0x41,
} protocol_cmd_t;
typedef enum {

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
region: flash_primary
# Primary Slot: Start bleibt 0xC000, Größe jetzt 200KB (0x32000)
# Primary Slot: Start bleibt 0xC000, Größe 200KB (0x32000)
mcuboot_primary:
address: 0xC000
size: 0x32000
@@ -26,7 +26,13 @@ mcuboot_secondary:
size: 0x32000
region: flash_primary
# External Flash bleibt unverändert
# NVS storage am Ende des Flashs, 16KB (0x4000)
settings_storage:
address: 0xFC000
size: 0x4000
region: flash_primary
# External Flash
littlefs_storage:
address: 0x0
size: 0x800000

View File

@@ -1,7 +1,7 @@
# --- GPIO & Logging ---
CONFIG_GPIO=y
CONFIG_LOG=y
CONFIG_POLL=y
CONFIG_POLL=n
# --- Power Management (Fix für HAS_PM & Policy) ---
# CONFIG_PM=y
@@ -13,24 +13,25 @@ CONFIG_FLASH_MAP=y
CONFIG_FILE_SYSTEM=y
CONFIG_FILE_SYSTEM_LITTLEFS=y
CONFIG_FILE_SYSTEM_MKFS=y
CONFIG_FS_LITTLEFS_READ_SIZE=64
CONFIG_FS_LITTLEFS_READ_SIZE=256
CONFIG_FS_LITTLEFS_PROG_SIZE=256
CONFIG_FS_LITTLEFS_CACHE_SIZE=512
CONFIG_FS_LITTLEFS_LOOKAHEAD_SIZE=128
CONFIG_FS_LITTLEFS_CACHE_SIZE=4096
CONFIG_FS_LITTLEFS_LOOKAHEAD_SIZE=256
CONFIG_FS_LITTLEFS_BLOCK_CYCLES=512
CONFIG_MAIN_STACK_SIZE=2048
# --- NVS & Settings (für die Speicherung von Konfigurationen) ---
CONFIG_NVS=y
CONFIG_SETTINGS=y
CONFIG_SETTINGS_NVS=y
# --- USB Device & CDC ACM ---
CONFIG_USB_DEVICE_STACK=y
CONFIG_DEPRECATION_TEST=y
CONFIG_USB_DEVICE_MANUFACTURER="Eduard Iten"
CONFIG_USB_DEVICE_PRODUCT="Edis Buzzer"
CONFIG_USB_DEVICE_PID=0x0001
CONFIG_USB_DRIVER_LOG_LEVEL_ERR=y
CONFIG_USB_DEVICE_LOG_LEVEL_ERR=y
CONFIG_USB_DEVICE_LOG_LEVEL_OFF=y
CONFIG_USB_DEVICE_INITIALIZE_AT_BOOT=n
CONFIG_USB_DEVICE_STACK_NEXT=n
CONFIG_USB_DEVICE_STACK_NEXT=y
CONFIG_USBD_CDC_ACM_CLASS=y
CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT=n
CONFIG_USBD_LOG_LEVEL_ERR=y
CONFIG_UDC_DRIVER_LOG_LEVEL_ERR=y
CONFIG_USBD_CDC_ACM_LOG_LEVEL_OFF=y
# --- UART (für USB-CDC) ---
CONFIG_SERIAL=y

View File

@@ -9,6 +9,7 @@
#include <audio.h>
#include <fs.h>
#include <io.h>
#include <settings.h>
#define AUDIO_THREAD_STACK_SIZE 2048
#define AUDIO_THREAD_PRIORITY 5
@@ -47,7 +48,6 @@ K_SEM_DEFINE(audio_ready_sem, 0, 1);
static const struct device *const i2s_dev = DEVICE_DT_GET(I2S_NODE);
static const struct gpio_dt_spec amp_en_dev = GPIO_DT_SPEC_GET(AUDIO_AMP_ENABLE_NODE, gpios);
static volatile int current_volume = 8;
static volatile bool abort_playback = false;
static char next_random_filename[64] = {0};
@@ -57,6 +57,8 @@ static char cached_404_path[] = "/lfs/sys/404";
static struct k_mutex i2s_lock;
static struct k_work audio_stop_work;
static uint32_t last_played_index = 0xFFFFFFFF;
static void audio_stop_work_handler(struct k_work *work)
{
ARG_UNUSED(work);
@@ -89,6 +91,52 @@ void i2s_resume(void)
k_mutex_unlock(&i2s_lock);
}
int get_random_file(char *out_filename, size_t max_len)
{
if (audio_file_count == 0)
{
/* Fallback auf System-Sound, wenn Ordner leer */
strncpy(out_filename, cached_404_path, max_len);
return 0;
}
uint32_t target_index;
/* Random-Index generieren mit optionalem No-Repeat-Schutz */
if (app_settings.play_norepeat && audio_file_count > 1) {
do {
target_index = k_cycle_get_32() % audio_file_count;
} while (target_index == last_played_index);
} else {
target_index = k_cycle_get_32() % audio_file_count;
}
last_played_index = target_index;
struct fs_dir_t dirp;
struct fs_dirent entry;
uint32_t current_index = 0;
fs_dir_t_init(&dirp);
if (fs_pm_opendir(&dirp, AUDIO_PATH) < 0)
return -ENOENT;
while (fs_readdir(&dirp, &entry) == 0 && entry.name[0] != '\0')
{
if (entry.type == FS_DIR_ENTRY_FILE)
{
if (current_index == target_index)
{
snprintf(out_filename, max_len, "%s/%s", AUDIO_PATH, entry.name);
break;
}
current_index++;
}
}
fs_pm_closedir(&dirp);
return 0;
}
void audio_refresh_file_count(void)
{
static struct fs_dir_t dirp;
@@ -112,6 +160,7 @@ void audio_refresh_file_count(void)
fs_pm_closedir(&dirp);
audio_file_count = count;
LOG_INF("Audio cache refreshed: %u files found in %s", count, AUDIO_PATH);
get_random_file(next_random_filename, sizeof(next_random_filename));
}
static void wait_for_i2s_drain(void)
@@ -130,40 +179,6 @@ static void wait_for_i2s_drain(void)
}
}
int get_random_file(char *out_filename, size_t max_len)
{
if (audio_file_count == 0)
{
/* Fallback auf System-Sound, wenn Ordner leer */
strncpy(out_filename, cached_404_path, max_len);
return 0;
}
struct fs_dir_t dirp;
struct fs_dirent entry;
uint32_t target_index = k_cycle_get_32() % audio_file_count;
uint32_t current_index = 0;
fs_dir_t_init(&dirp);
if (fs_pm_opendir(&dirp, AUDIO_PATH) < 0)
return -ENOENT;
while (fs_readdir(&dirp, &entry) == 0 && entry.name[0] != '\0')
{
if (entry.type == FS_DIR_ENTRY_FILE)
{
if (current_index == target_index)
{
snprintf(out_filename, max_len, "%s/%s", AUDIO_PATH, entry.name);
break;
}
current_index++;
}
}
fs_pm_closedir(&dirp);
return 0;
}
void audio_system_ready(void)
{
k_sem_give(&audio_ready_sem);
@@ -250,8 +265,8 @@ void audio_thread(void *arg1, void *arg2, void *arg3)
bool trigger_started = false;
int queued_blocks = 0;
uint8_t factor = MIN(255, current_volume * 0xFF / 100);
LOG_INF("Volume factor: %u (for volume %d%%)", factor, current_volume);
uint8_t factor = MIN(255, app_settings.audio_vol * 0xFF / 100);
LOG_INF("Volume factor: %u (for volume %d%%)", factor, app_settings.audio_vol);
while (!abort_playback)
{

View File

@@ -1,4 +1,5 @@
#include <zephyr/fs/littlefs.h>
#include <zephyr/sys/byteorder.h>
#include <zephyr/drivers/flash.h>
#include <zephyr/storage/flash_map.h>
#include <zephyr/dfu/flash_img.h>
@@ -9,18 +10,23 @@
LOG_MODULE_REGISTER(buzz_fs, LOG_LEVEL_INF);
#define FS_THREAD_STACK_SIZE 2048
#define FS_THREAD_PRIORITY 6
#define FS_MSGQ_MAX_ITEMS 4
#define FS_SLAB_BUF_SIZE 4096
#define TAG_FORMAT_VERSION 0x0001
#define TAG_MAGIC "TAG!"
#define TAG_MAGIC_LEN 4U
#define TAG_LEN_FIELD_LEN 2U
#define TAG_VERSION_LEN 1U
#define TAG_FOOTER_V1_LEN (TAG_VERSION_LEN + TAG_LEN_FIELD_LEN + TAG_MAGIC_LEN)
#define TAG_FORMAT_VERSION 0x01
#define STORAGE_PARTITION_ID FIXED_PARTITION_ID(littlefs_storage)
#define SLOT1_ID FIXED_PARTITION_ID(slot1_partition)
FS_LITTLEFS_DECLARE_DEFAULT_CONFIG(fs_storage_data);
K_MEM_SLAB_DEFINE(file_buffer_slab, FS_SLAB_BUF_SIZE, FS_MSGQ_MAX_ITEMS, 4);
K_MSGQ_DEFINE(fs_msgq, sizeof(fs_msg_t), FS_MSGQ_MAX_ITEMS, 4);
K_SEM_DEFINE(fs_transfer_done_sem, 0, 1);
#define QSPI_FLASH_NODE DT_ALIAS(qspi_flash)
#if !DT_NODE_EXISTS(QSPI_FLASH_NODE)
#error "QSPI Flash alias not defined in devicetree"
@@ -33,6 +39,8 @@ static struct k_mutex flash_pm_lock;
static struct slot_info_t slot1_info;
static struct flash_img_context flash_ctx;
extern struct k_mem_slab file_buffer_slab;
static struct fs_mount_t fs_storage_mnt = {
.type = FS_LITTLEFS,
.fs_data = &fs_storage_data,
@@ -40,6 +48,17 @@ static struct fs_mount_t fs_storage_mnt = {
.mnt_point = "/lfs",
};
typedef enum {
FS_STATE_IDLE,
FS_STATE_RECEIVING
} fs_thread_state_t;
typedef struct __attribute__((packed)) {
uint16_t total_size;
uint16_t version;
uint8_t magic[4];
} tag_footer_t;
int fs_init(void) {
int rc = fs_mount(&fs_storage_mnt);
if (rc < 0) {
@@ -121,10 +140,7 @@ int fs_pm_close(struct fs_file_t *file)
{
LOG_DBG("PM Closing file");
int rc = fs_close(file);
if (rc == 0)
{
fs_pm_flash_suspend();
}
return rc;
}
@@ -144,10 +160,7 @@ int fs_pm_closedir(struct fs_dir_t *dirp)
{
LOG_DBG("PM Closing directory");
int rc = fs_closedir(dirp);
if (rc == 0)
{
fs_pm_flash_suspend();
}
return rc;
}
@@ -196,11 +209,159 @@ int fs_pm_rename(const char *old_path, const char *new_path)
return rc;
}
int fs_pm_rm_recursive(char *path_buf, size_t max_len)
{
struct fs_dirent entry;
struct fs_dir_t dir;
int rc;
fs_pm_flash_resume();
/* 1. Stat prüfen: Ist es eine Datei? */
rc = fs_stat(path_buf, &entry);
if (rc != 0) {
fs_pm_flash_suspend();
return rc;
}
/* Wenn es eine Datei ist, direkt löschen und beenden */
if (entry.type == FS_DIR_ENTRY_FILE) {
rc = fs_unlink(path_buf);
fs_pm_flash_suspend();
return rc;
}
/* 2. Es ist ein Verzeichnis. Schleife bis es leer ist. */
size_t orig_len = strlen(path_buf);
while (1) {
fs_dir_t_init(&dir);
rc = fs_opendir(&dir, path_buf);
if (rc != 0) {
break;
}
bool found_something = false;
/* Genau EINEN löschbaren Eintrag suchen */
while (1) {
rc = fs_readdir(&dir, &entry);
if (rc != 0 || entry.name[0] == '\0') {
break; /* Ende oder Fehler */
}
if (strcmp(entry.name, ".") == 0 || strcmp(entry.name, "..") == 0) {
continue; /* Ignorieren */
}
found_something = true;
break; /* Treffer! Schleife abbrechen. */
}
/* WICHTIG: Das Verzeichnis SOFORT schließen, BEVOR wir rekurieren!
* Damit geben wir das File-Handle (NUM_DIRS) an Zephyr zurück. */
fs_closedir(&dir);
if (!found_something || rc != 0) {
break; /* Verzeichnis ist nun restlos leer */
}
size_t name_len = strlen(entry.name);
if (orig_len + 1 + name_len >= max_len) {
rc = -ENAMETOOLONG;
break;
}
/* Pfad für das gefundene Kindelement bauen */
path_buf[orig_len] = '/';
strcpy(&path_buf[orig_len + 1], entry.name);
/* Rekursiver Aufruf für das Kind */
rc = fs_pm_rm_recursive(path_buf, max_len);
/* Puffer sofort wieder auf unser Verzeichnis zurückschneiden */
path_buf[orig_len] = '\0';
if (rc != 0) {
break; /* Abbruch, falls beim Löschen des Kindes ein Fehler auftrat */
}
}
/* 3. Das nun restlos leere Verzeichnis selbst löschen */
if (rc == 0) {
rc = fs_unlink(path_buf);
}
fs_pm_flash_suspend();
return rc;
}
int fs_pm_mkdir_recursive(char *path)
{
int rc = 0;
struct fs_dirent entry;
char *p = path;
/* Führenden Slash überspringen, falls vorhanden (z. B. bei "/lfs") */
if (*p == '/') {
p++;
}
/* Flash für den gesamten Durchlauf aktivieren */
fs_pm_flash_resume();
while (*p != '\0') {
if (*p == '/') {
*p = '\0'; /* String temporär am aktuellen Slash terminieren */
/* Prüfen, ob dieser Pfadabschnitt bereits existiert */
rc = fs_stat(path, &entry);
if (rc == -ENOENT) {
/* Existiert nicht -> anlegen */
rc = fs_mkdir(path);
if (rc != 0) {
*p = '/'; /* Bei Fehler Slash wiederherstellen und abbrechen */
break;
}
} else if (rc == 0) {
/* Existiert -> prüfen, ob es ein Verzeichnis ist */
if (entry.type != FS_DIR_ENTRY_DIR) {
rc = -ENOTDIR;
*p = '/';
break;
}
} else {
/* Anderer Dateisystemfehler */
*p = '/';
break;
}
*p = '/'; /* Slash für den nächsten Schleifendurchlauf wiederherstellen */
}
p++;
}
/* Letztes Element verarbeiten, falls der Pfad nicht mit '/' endet */
if (rc == 0 && p > path && *(p - 1) != '/') {
rc = fs_stat(path, &entry);
if (rc == -ENOENT) {
rc = fs_mkdir(path);
} else if (rc == 0) {
if (entry.type != FS_DIR_ENTRY_DIR) {
rc = -ENOTDIR;
}
}
}
/* Flash am Ende wieder in den Suspend schicken */
fs_pm_flash_suspend();
return rc;
}
static int fs_get_tag_bounds(struct fs_file_t *fp, off_t file_size,
size_t *audio_limit, size_t *payload_len, bool *has_tag)
{
uint8_t footer[6];
uint16_t tag_len;
tag_footer_t footer;
if (audio_limit == NULL || payload_len == NULL || has_tag == NULL) {
return -EINVAL;
@@ -210,42 +371,41 @@ static int fs_get_tag_bounds(struct fs_file_t *fp, off_t file_size,
*audio_limit = (size_t)file_size;
*payload_len = 0U;
if (file_size < (off_t)TAG_FOOTER_V1_LEN) {
if (file_size < (off_t)sizeof(tag_footer_t)) {
return 0;
}
fs_seek(fp, -(off_t)(TAG_LEN_FIELD_LEN + TAG_MAGIC_LEN), FS_SEEK_END);
if (fs_read(fp, footer, sizeof(footer)) != sizeof(footer)) {
/* Den 8-Byte-Footer direkt in das Struct einlesen */
fs_seek(fp, -(off_t)sizeof(tag_footer_t), FS_SEEK_END);
if (fs_read(fp, &footer, sizeof(tag_footer_t)) != sizeof(tag_footer_t)) {
fs_seek(fp, 0, FS_SEEK_SET);
return -EIO;
}
if (memcmp(&footer[2], TAG_MAGIC, TAG_MAGIC_LEN) != 0) {
/* 1. Signatur prüfen */
if (memcmp(footer.magic, TAG_MAGIC, 4) != 0) {
fs_seek(fp, 0, FS_SEEK_SET);
return 0;
}
tag_len = (uint16_t)footer[0] | ((uint16_t)footer[1] << 8);
if (tag_len > (uint16_t)file_size || tag_len < TAG_FOOTER_V1_LEN) {
fs_seek(fp, 0, FS_SEEK_SET);
return -EBADMSG;
}
uint8_t tag_version = 0;
fs_seek(fp, -(off_t)TAG_FOOTER_V1_LEN, FS_SEEK_END);
if (fs_read(fp, &tag_version, 1) != 1) {
fs_seek(fp, 0, FS_SEEK_SET);
return -EIO;
}
/* 2. Endianness konvertieren */
uint16_t tag_version = sys_le16_to_cpu(footer.version);
uint16_t tag_len = sys_le16_to_cpu(footer.total_size);
/* 3. Version und Größe validieren */
if (tag_version != TAG_FORMAT_VERSION) {
fs_seek(fp, 0, FS_SEEK_SET);
return -ENOTSUP;
}
if (tag_len > (uint16_t)file_size || tag_len < sizeof(tag_footer_t)) {
fs_seek(fp, 0, FS_SEEK_SET);
return -EBADMSG;
}
*has_tag = true;
*audio_limit = (size_t)file_size - tag_len;
*payload_len = tag_len - TAG_FOOTER_V1_LEN;
*payload_len = tag_len - sizeof(tag_footer_t);
fs_seek(fp, 0, FS_SEEK_SET);
return 0;
@@ -321,51 +481,6 @@ ssize_t fs_tag_read_chunk(struct fs_file_t *fp, void *buffer, size_t len)
return fs_read(fp, buffer, len);
}
int fs_tag_open_write(struct fs_file_t *fp)
{
ssize_t audio_limit = fs_get_audio_data_len(fp);
if (audio_limit < 0) {
return (int)audio_limit;
}
fs_seek(fp, audio_limit, FS_SEEK_SET);
return 0;
}
ssize_t fs_tag_write_chunk(struct fs_file_t *fp, const void *buffer, size_t len)
{
return fs_write(fp, buffer, len);
}
int fs_tag_finish_write(struct fs_file_t *fp, uint8_t version, size_t payload_len)
{
if (version != TAG_FORMAT_VERSION) {
return -ENOTSUP;
}
size_t total_footer_len = payload_len + TAG_FOOTER_V1_LEN;
if (total_footer_len > UINT16_MAX) {
return -EFBIG;
}
if (fs_write(fp, &version, 1) != 1) {
return -EIO;
}
uint8_t len_bytes[2];
len_bytes[0] = (uint8_t)(total_footer_len & 0xFFU);
len_bytes[1] = (uint8_t)((total_footer_len >> 8) & 0xFFU);
if (fs_write(fp, len_bytes, sizeof(len_bytes)) != sizeof(len_bytes)) {
return -EIO;
}
if (fs_write(fp, TAG_MAGIC, TAG_MAGIC_LEN) != TAG_MAGIC_LEN) {
return -EIO;
}
off_t current_pos = fs_tell(fp);
return fs_truncate(fp, current_pos);
}
int flash_get_slot_info(slot_info_t *info) {
if (slot1_info.size != 0) {
*info = slot1_info;
@@ -493,3 +608,112 @@ size_t fs_get_internal_flash_page_size(void) {
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 <usb.h>
#include <utils.h>
#include <uart.h>
#include <settings.h>
LOG_MODULE_REGISTER(main, LOG_LEVEL_INF);
@@ -38,6 +40,13 @@ int main(void)
int rc;
rc = app_settings_init();
if (rc < 0)
{
LOG_ERR("Settings initialization failed: %d", rc);
return rc;
}
rc = fs_init();
if (rc < 0)
{
@@ -52,13 +61,21 @@ int main(void)
return rc;
}
rc = usb_cdc_acm_init();
rc = usb_init();
if (rc < 0)
{
LOG_ERR("USB initialization failed: %d", rc);
return rc;
}
rc = uart_init();
if (rc < 0)
{
LOG_ERR("UART initialization failed: %d", rc);
return rc;
}
rc = io_init();
if (rc < 0)
{
@@ -81,6 +98,7 @@ int main(void)
{
LOG_INF("Firmware image already confirmed. No need to confirm again.");
}
while (1)
{
k_sleep(K_FOREVER);

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/logging/log.h>
#include <zephyr/usb/usb_device.h>
#include <zephyr/device.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);
K_SEM_DEFINE(usb_rx_sem, 0, 1);
K_SEM_DEFINE(usb_tx_sem, 0, 1);
K_SEM_DEFINE(dtr_active_sem, 0, 1);
static uint32_t dtr_active = 0U;
#define UART_NODE DT_ALIAS(usb_uart)
const struct device *cdc_dev = DEVICE_DT_GET(UART_NODE);
static volatile bool rx_interrupt_enabled = false;
USBD_DEVICE_DEFINE(cdc_acm_serial,
DEVICE_DT_GET(DT_NODELABEL(zephyr_udc0)),
USB_DEVICE_VID, USB_DEVICE_PID);
RING_BUF_DECLARE(rx_ringbuf, RX_RING_BUF_SIZE);
USBD_DESC_LANG_DEFINE(cdc_acm_lang);
USBD_DESC_MANUFACTURER_DEFINE(cdc_acm_mfr, USB_MANUFACTURER_STRING);
USBD_DESC_PRODUCT_DEFINE(cdc_acm_product, USB_PRODUCT_STRING);
IF_ENABLED(CONFIG_HWINFO, (USBD_DESC_SERIAL_NUMBER_DEFINE(cdc_acm_sn)));
static void cdc_acm_irq_cb(const struct device *dev, void *user_data)
USBD_DESC_CONFIG_DEFINE(fs_cfg_desc, "FS Configuration");
USBD_DESC_CONFIG_DEFINE(hs_cfg_desc, "HS Configuration");
USBD_CONFIGURATION_DEFINE(fs_config, 0U, 125U, &fs_cfg_desc);
USBD_CONFIGURATION_DEFINE(hs_config, 0U, 125U, &hs_cfg_desc);
static void fix_code_triple(struct usbd_context *uds_ctx, enum usbd_speed speed)
{
ARG_UNUSED(user_data);
if (!uart_irq_update(dev)) {
return;
}
if (uart_irq_rx_ready(dev)) {
uint8_t buffer[64];
uint32_t space = ring_buf_space_get(&rx_ringbuf);
if (space == 0) {
/* Backpressure anwenden: Ringpuffer ist voll.
Interrupt deaktivieren, damit Daten im HW-FIFO bleiben
und der USB-Stack den Host drosselt (NAK). */
uart_irq_rx_disable(dev);
if (IS_ENABLED(CONFIG_USBD_CDC_ACM_CLASS) ||
IS_ENABLED(CONFIG_USBD_CDC_ECM_CLASS) ||
IS_ENABLED(CONFIG_USBD_CDC_NCM_CLASS) ||
IS_ENABLED(CONFIG_USBD_MIDI2_CLASS) ||
IS_ENABLED(CONFIG_USBD_AUDIO2_CLASS) ||
IS_ENABLED(CONFIG_USBD_VIDEO_CLASS)) {
usbd_device_set_code_triple(uds_ctx, speed,
USB_BCC_MISCELLANEOUS, 0x02, 0x01);
} else {
int to_read = MIN(sizeof(buffer), space);
int len = uart_fifo_read(dev, buffer, to_read);
if (len > 0) {
ring_buf_put(&rx_ringbuf, buffer, len);
k_sem_give(&usb_rx_sem);
}
}
}
if (uart_irq_tx_ready(dev)) {
uart_irq_tx_disable(dev);
k_sem_give(&usb_tx_sem);
usbd_device_set_code_triple(uds_ctx, speed, 0, 0, 0);
}
}
bool usb_wait_for_data(k_timeout_t timeout)
static void usbd_msg_cb(struct usbd_context *const ctx, const struct usbd_msg *const msg)
{
if (!ring_buf_is_empty(&rx_ringbuf)) {
return true;
int err;
LOG_DBG("USBD message: %s", usbd_msg_type_string(msg->type));
if (usbd_can_detect_vbus(ctx)) {
if (msg->type == USBD_MSG_VBUS_READY) {
err = usbd_enable(ctx);
if (err) {
LOG_ERR("Failed to enable USB device (%d)", err);
}
}
/* Wenn der Puffer leer ist, sicherstellen, dass der RX-Interrupt
aktiviert ist, da sonst keine neuen Daten empfangen werden können. */
if (device_is_ready(cdc_dev)) {
uart_irq_rx_enable(cdc_dev);
if (msg->type == USBD_MSG_VBUS_REMOVED) {
err = usbd_disable(ctx);
if (err) {
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)
{
int ret = ring_buf_get(&rx_ringbuf, c, 1);
return (ret > 0);
}
int usb_read_buffer(uint8_t *buf, size_t max_len)
{
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);
if (msg->dev != NULL) {
(void)uart_line_ctrl_get(msg->dev, UART_LINE_CTRL_RTS, &rts);
(void)uart_line_ctrl_get(msg->dev, UART_LINE_CTRL_DTR, &dtr_active);
(void)uart_line_ctrl_get(msg->dev, UART_LINE_CTRL_DCD, &dcd);
(void)uart_line_ctrl_get(msg->dev, UART_LINE_CTRL_DSR, &dsr);
LOG_DBG("CDC ACM RTS: %u, DTR: %u, DCD: %u, DSR: %u", rts, dtr_active, dcd, dsr);
if (dtr_active) {
k_sem_give(&dtr_active_sem);
}
}
}
}
void usb_write_byte(uint8_t c)
int usb_init(void)
{
if (!device_is_ready(cdc_dev)) {
return;
}
uart_poll_out(cdc_dev, c);
}
int err;
int usb_write_buffer(const uint8_t *buf, size_t len)
{
if (!device_is_ready(cdc_dev))
{
return -ENODEV;
err = usbd_add_descriptor(&cdc_acm_serial, &cdc_acm_lang);
if (err) {
LOG_ERR("Failed to add language descriptor (%d)", err);
return err;
}
size_t written;
while (len > 0)
{
written = uart_fifo_fill(cdc_dev, buf, len);
err = usbd_add_descriptor(&cdc_acm_serial, &cdc_acm_mfr);
if (err) {
LOG_ERR("Failed to add manufacturer descriptor (%d)", err);
return err;
}
len -= written;
buf += written;
err = usbd_add_descriptor(&cdc_acm_serial, &cdc_acm_product);
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)
{
LOG_WRN("USB TX timeout - consumer not reading?");
return -ETIMEDOUT;
}
err = usbd_register_class(&cdc_acm_serial, "cdc_acm_0", USBD_SPEED_HS, 1);
if (err) {
LOG_ERR("Failed to register HS CDC ACM class (%d)", err);
return err;
}
fix_code_triple(&cdc_acm_serial, USBD_SPEED_HS);
}
err = usbd_add_configuration(&cdc_acm_serial, USBD_SPEED_FS, &fs_config);
if (err) {
LOG_ERR("Failed to add FS configuration (%d)", err);
return err;
}
err = usbd_register_class(&cdc_acm_serial, "cdc_acm_0", USBD_SPEED_FS, 1);
if (err) {
LOG_ERR("Failed to register FS CDC ACM class (%d)", err);
return err;
}
fix_code_triple(&cdc_acm_serial, USBD_SPEED_FS);
err = usbd_msg_register_cb(&cdc_acm_serial, usbd_msg_cb);
if (err) {
LOG_ERR("Failed to register USBD callback (%d)", err);
return err;
}
err = usbd_init(&cdc_acm_serial);
if (err) {
LOG_ERR("Failed to initialize USBD (%d)", err);
return err;
}
if (!usbd_can_detect_vbus(&cdc_acm_serial)) {
err = usbd_enable(&cdc_acm_serial);
if (err) {
LOG_ERR("Failed to enable USBD (%d)", err);
return err;
}
}
LOG_INF("USBD CDC ACM initialized");
return 0;
}
void usb_flush_rx(void)
void usb_wait_for_dtr(void)
{
uint8_t dummy;
if (!device_is_ready(cdc_dev)) return;
/* Hardware-FIFO leeren, falls Reste vorhanden */
while (uart_fifo_read(cdc_dev, &dummy, 1) > 0);
/* Ringpuffer und Semaphore zurücksetzen */
ring_buf_reset(&rx_ringbuf);
k_sem_reset(&usb_rx_sem);
k_sem_take(&dtr_active_sem, K_FOREVER);
}
static void usb_status_cb(enum usb_dc_status_code cb_status, const uint8_t *param)
bool usb_dtr_active(void)
{
switch (cb_status) {
case USB_DC_CONNECTED:
/* VBUS wurde vom Zephyr-Stack erkannt */
LOG_DBG("VBUS detected, USB device connected");
break;
case USB_DC_CONFIGURED:
LOG_DBG("USB device configured by host");
io_usb_status(true);
if (device_is_ready(cdc_dev)) {
(void)uart_line_ctrl_set(cdc_dev, UART_LINE_CTRL_DCD, 1);
(void)uart_line_ctrl_set(cdc_dev, UART_LINE_CTRL_DSR, 1);
/* Interrupt-Handler binden und initial aktivieren */
uart_irq_callback_set(cdc_dev, cdc_acm_irq_cb);
uart_irq_rx_enable(cdc_dev);
}
break;
case USB_DC_DISCONNECTED:
/* Kabel wurde gezogen */
LOG_DBG("VBUS removed, USB device disconnected");
if (device_is_ready(cdc_dev)) {
uart_irq_rx_disable(cdc_dev);
}
io_usb_status(false);
break;
case USB_DC_RESET:
LOG_DBG("USB bus reset");
break;
default:
break;
}
}
int usb_cdc_acm_init(void)
{
LOG_DBG("Initializing USB Stack...");
/* Zephyr-Treiber registrieren. Verbraucht keinen Strom ohne VBUS. */
int ret = usb_enable(usb_status_cb);
if (ret != 0) {
LOG_ERR("Failed to enable USB (%d)", ret);
return ret;
}
#if DT_NODE_HAS_STATUS(DT_NODELABEL(cdc_acm_uart0), okay)
const struct device *cdc_dev = DEVICE_DT_GET(DT_NODELABEL(cdc_acm_uart0));
if (!device_is_ready(cdc_dev)) {
LOG_ERR("CDC ACM device not ready");
return -ENODEV;
}
#else
LOG_ERR("CDC ACM UART device not found in devicetree");
return -ENODEV;
#endif
LOG_DBG("USB Stack enabled and waiting for VBUS in hardware");
return 0;
return dtr_active != 0U;
}

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

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
subparsers = parser.add_subparsers(dest="command", help="Verfügbare Befehle")
# Befehl: crc32
crc32_parser = subparsers.add_parser("crc32", help="CRC32-Checksumme einer Datei oder eines Verzeichnisses berechnen")
crc32_parser.add_argument("path", help="Pfad der Datei auf dem Zielsystem")
# Befehl: flash_info
flash_info_parser = subparsers.add_parser("flash_info", help="Informationen über den Flash-Speicher des Controllers abfragen")
@@ -41,6 +45,21 @@ def main():
# Befehl: proto
proto_parser = subparsers.add_parser("proto", help="Protokollversion des Controllers abfragen")
# Befehl: put_file
put_file_parser = subparsers.add_parser("put_file", help="Datei auf das Zielsystem hochladen")
put_file_parser.add_argument("source_path", help="Pfad der Datei auf dem lokalen System")
put_file_parser.add_argument("dest_path", help="Zielpfad auf dem Zielsystem")
put_file_parser.add_argument("-t", "--tags", help="Optionale JSON Tags für den Upload", type=str)
# Befehl: get_tags
get_tags_parser = subparsers.add_parser("get_tags", help="Tags einer Datei anzeigen")
get_tags_parser.add_argument("path", help="Pfad der Datei auf dem Zielsystem")
# Befehl: put_tags
put_tags_parser = subparsers.add_parser("put_tags", help="Tags schreiben")
put_tags_parser.add_argument("path", help="Pfad der Datei auf dem Zielsystem")
put_tags_parser.add_argument("json", help="JSON String (z.B. '{\"json\": {\"t\": \"Titel\"}}')")
put_tags_parser.add_argument("-o", "--overwrite", help="Alle bestehenden JSON-Tags vorher löschen", action="store_true")
# Befehl: rename
rename_parser = subparsers.add_parser("rename", help="Benennen Sie eine Datei oder einen Ordner auf dem Zielsystem um")
rename_parser.add_argument("source_path", help="Aktueller Pfad der Datei/des Ordners auf dem Zielsystem")
@@ -54,6 +73,32 @@ def main():
stat_parser = subparsers.add_parser("stat", help="Informationen zu einer Datei/Ordner")
stat_parser.add_argument("path", help="Pfad auf dem Zielsystem")
# Befehl: put_fw
put_fw_parser = subparsers.add_parser("put_fw", help="Firmware-Image auf den Controller hochladen")
put_fw_parser.add_argument("file_path", help="Pfad zur Firmware-Datei auf dem lokalen System")
# Befehl: confirm_fw
confirm_fw_parser = subparsers.add_parser("confirm_fw", help="Bestätigt ein als 'Testing' markiertes Firmware-Image, damit es beim permanent wird")
# Befehl: reboot
reboot_parser = subparsers.add_parser("reboot", help="Neustart des Controllers")
# Befehl: play
play_parser = subparsers.add_parser("play", help="Startet die Wiedergabe einer Datei")
play_parser.add_argument("path", help="Pfad der Datei auf dem Zielsystem")
play_parser.add_argument("-i", "--interrupt", help="Sofortige Wiedergabe (Interrupt)", action="store_true")
# Befehl: stop
stop_parser = subparsers.add_parser("stop", help="Stoppt die aktuelle Wiedergabe")
# Befehl: set
set_parser = subparsers.add_parser("set", help="System-Einstellung setzen")
set_parser.add_argument("key", help="Schlüssel (z.B. audio/vol)")
set_parser.add_argument("value", help="Wert")
# Befehl: get
get_parser = subparsers.add_parser("get", help="System-Einstellung auslesen")
get_parser.add_argument("key", help="Schlüssel (z.B. audio/vol)")
args = parser.parse_args()
if not args.command:
@@ -103,7 +148,12 @@ def main():
try:
bus.open()
if args.command == "get_file":
if args.command == "crc32":
from core.cmd.crc32 import crc32
cmd = crc32(bus)
result = cmd.get(args.path)
cmd.print(result, args.path)
elif args.command == "get_file":
from core.cmd.get_file import get_file
cmd = get_file(bus)
result = cmd.get(args.source_path, args.dest_path)
@@ -128,6 +178,11 @@ def main():
cmd = proto(bus)
result = cmd.get()
cmd.print(result)
elif args.command == "put_file":
from core.cmd.put_file import put_file
cmd = put_file(bus)
result = cmd.get(args.source_path, args.dest_path)
cmd.print(result)
elif args.command == "rename":
from core.cmd.rename import rename
cmd = rename(bus)
@@ -143,7 +198,56 @@ def main():
cmd = stat(bus)
result = cmd.get(args.path)
cmd.print(result, args.path)
elif args.command == "put_file":
from core.cmd.put_file import put_file
cmd = put_file(bus)
result = cmd.get(args.source_path, args.dest_path, cli_tags_json=args.tags)
cmd.print(result)
elif args.command == "get_tags":
from core.cmd.get_tags import get_tags
cmd = get_tags(bus)
result = cmd.get(args.path)
cmd.print(result, args.path)
elif args.command == "put_tags":
from core.cmd.put_tags import put_tags
cmd = put_tags(bus)
result = cmd.get(args.path, args.json, overwrite=args.overwrite)
cmd.print(result, args.path)
elif args.command == "confirm_fw":
from core.cmd.fw_confirm import fw_confirm
cmd = fw_confirm(bus)
result = cmd.get()
cmd.print(result)
elif args.command == "put_fw":
from core.cmd.put_fw import put_fw
cmd = put_fw(bus)
result = cmd.get(args.file_path)
cmd.print(result)
elif args.command == "reboot":
from core.cmd.reboot import reboot
cmd = reboot(bus)
result = cmd.get()
cmd.print(result)
elif args.command == "play":
from core.cmd.play import play
cmd = play(bus)
result = cmd.get(args.path, interrupt=args.interrupt)
cmd.print(result, args.path, interrupt=args.interrupt)
elif args.command == "stop":
from core.cmd.stop import stop
cmd = stop(bus)
result = cmd.get()
cmd.print(result)
elif args.command == "set":
from core.cmd.set_setting import set_setting
cmd = set_setting(bus)
result = cmd.get(args.key, args.value)
cmd.print(result, args.key, args.value)
elif args.command == "get":
from core.cmd.get_setting import get_setting
cmd = get_setting(bus)
result = cmd.get(args.key)
cmd.print(result, args.key)
finally:
bus.close()

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 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
@@ -21,24 +22,34 @@ class get_file:
source_path_bytes = source_path.encode('utf-8')
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)
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':
return None
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:
with open(p, 'wb') as f:
@@ -55,8 +66,8 @@ class get_file:
'dest_path': dest_path,
'crc32_remote': remote_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):
@@ -74,3 +85,5 @@ class get_file:
if result.get('crc32_device_file') is not None:
console.print(f" • Device CRC: [info]{result['crc32_device_file']:08X}[/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 = {
"min_protocol_version": 1,
"max_protocol_version": 1,
"current_protocol_version": None
"current_protocol_version": None,
}
SYNC_SEQ = b'BUZZ'
@@ -33,56 +33,45 @@ ERRORS = {
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 = {
'request': FRAME_TYPE_REQUEST,
'ack': FRAME_TYPE_ACK,
'response': FRAME_TYPE_RESPONSE,
'error': FRAME_TYPE_ERROR,
'stream_start': FRAME_TYPE_STREAM_START,
'stream_chunk': FRAME_TYPE_STREAM_CHUNK,
'stream_end': FRAME_TYPE_STREAM_END,
'list_start': FRAME_TYPE_LIST_START,
'list_chunk': FRAME_TYPE_LIST_CHUNK,
'list_end': FRAME_TYPE_LIST_END
'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,
}
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 = {
'get_protocol_version': CMD_GET_PROTOCOL_VERSION,
'get_firmware_status': CMD_GET_FIRMWARE_STATUS,
'get_flash_info': CMD_GET_FLASH_INFO,
'list_dir': CMD_LIST_DIR,
'stat': CMD_STAT,
'rm': CMD_RM,
'rename': CMD_RENAME,
'mkdir': CMD_MKDIR,
'crc_32': CMD_CRC_32,
'put_file': CMD_PUT_FILE,
'get_file': CMD_GET_FILE,
'put_fw': CMD_PUT_FW,
'get_protocol_version': 0x00,
'get_firmware_status': 0x01,
'get_flash_info': 0x02,
'confirm_fw': 0x03,
'reboot': 0x04,
'list_dir': 0x10,
'crc_32': 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,
}

View File

@@ -120,6 +120,40 @@ class SerialBus:
full_frame += payload
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):
wait_time = timeout if timeout is not None else self.timeout
@@ -212,42 +246,46 @@ class SerialBus:
err_name = ERRORS.get(err_code, "UNKNOWN")
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."""
is_stream = False
data_chunks = []
start_time = None
while True:
if not self.wait_for_sync(SYNC_SEQ):
raise TimeoutError("Timeout beim Warten auf Sync im Stream-Modus.")
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']:
is_stream = True
data_chunks = []
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
while received_size < size:
chunk_length = min(chunk_size, size - received_size)
try:
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)
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.")
elif ftype == FRAME_TYPES['stream_end']:
end_time = time.time()
if not is_stream: raise ValueError("Ende ohne Start.")
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), 'crc32': crc32}
return {
'data': b''.join(data_chunks),
'crc32': crc32,
'duration': end_time - start_time if start_time and end_time else None
}
# elif ftype == FRAME_TYPES['list_chunk']:
# 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">
import { onMount } from 'svelte';
import { bus } from '../lib/bus/SerialBus';
import { buzzer } from '../lib/buzzerStore';
import { connectToPort, disconnectBuzzer,initSerialListeners } from '../lib/buzzerActions';
import { PlugsIcon, PlugsConnectedIcon } from 'phosphor-svelte';
import { GetProtocolCommand } from '../lib/protocol/commands/GetProtocol';
import { addToast } from '../lib/toastStore';
import {
PlugsIcon,
PlugsConnectedIcon,
CaretDownIcon,
BluetoothIcon,
TrashIcon,
PlusCircleIcon
} from 'phosphor-svelte';
import { slide } from 'svelte/transition';
import { initializeBuzzer } from '../lib/buzzerActions';
const BUZZER_FILTER = [
{ usbVendorId: 0x2fe3, usbProductId: 0x0001 },
];
onMount(() => {
initSerialListeners();
});
const BUZZER_FILTER = [{ usbVendorId: 0x1209, usbProductId: 0xEDED }];
async function handleConnectClick() {
try {
if ($buzzer.connected) {
console.log("Trenne verbindung zum aktuellen Buzzer...");
await disconnectBuzzer();
console.log("Verbindung getrennt");
let showMenu = false;
let menuElement: HTMLElement;
// Schließt das Menü bei Klick außerhalb
function handleOutsideClick(event: MouseEvent) {
if (showMenu && menuElement && !menuElement.contains(event.target as Node)) {
showMenu = false;
}
}
const port = await navigator.serial.requestPort({ filters: BUZZER_FILTER });
console.log("Port ausgewählt, versuche Verbindung.", port.getInfo());
await connectToPort(port);
} catch (e) {
// Verhindert das Error-Logging, wenn der User einfach nur "Abbrechen" klickt
if (e instanceof Error && e.name === 'NotFoundError') {
console.log("Keine Verbindung ausgewählt, Abbruch durch Nutzer.");
async function connectTo(port: SerialPort) {
try {
await bus.connect(port);
// Kurze Pause für die Hardware-Bereitschaft
await new Promise(r => setTimeout(r, 100));
// Logische Initialisierung starten
await initializeBuzzer();
} catch (e: any) {
console.error("Port-Fehler:", e);
}
}
async function handleMainAction() {
if ($buzzer.connected) {
await bus.disconnect();
buzzer.update(s => ({ ...s, connected: false }));
return;
}
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>
<button
on:click={handleConnectClick}
class="flex items-center gap-2 px-4 py-2 rounded-xl transition-all
<div class="relative inline-flex shadow-lg" bind:this={menuElement}>
<button
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
? 'bg-emerald-500/20 text-emerald-400 border border-emerald-500/50'
: 'bg-slate-700 hover:bg-slate-600 text-slate-200 border border-slate-600'}"
>
? 'bg-emerald-600 hover:bg-emerald-500 text-white'
: 'bg-slate-700 hover:bg-slate-600 text-slate-200'}"
>
{#if $buzzer.connected}
<PlugsConnectedIcon size={18} weight="fill" />
<span class="text-xs font-bold uppercase tracking-wider text-emerald-300">Verbunden</span>
<PlugsConnectedIcon size={20} weight="fill" />
<span class="text-sm font-bold uppercase tracking-wide">Trennen</span>
{:else}
<PlugsIcon size={18} weight="bold" />
<span class="text-xs font-bold uppercase tracking-wider">Verbinden</span>
<PlugsIcon size={20} weight="bold" />
<span class="text-sm font-bold uppercase tracking-wide">Verbinden</span>
{/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>
import { buzzer } from '../lib/buzzerStore';
import { CpuIcon } from 'phosphor-svelte';
</script>
<div class="text-[11px] font-mono text-slate-400 space-y-1 relative z-10">
<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">
<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>
Firmware: <span class="text-indigo-300">{$buzzer.version}</span>
</p>
@@ -14,4 +21,5 @@
{$buzzer.connected ? 'Confirmed' : 'Disconnected'}
</span>
</p>
</div>
</div>

View File

@@ -14,7 +14,7 @@
$: isDisconnected = !$buzzer.connected;
</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-full bg-slate-300 transition-all duration-500" style="width: {pMeta}%"></div>

View File

@@ -1,10 +1,31 @@
<script lang="ts">
import { buzzer } from '../lib/buzzerStore';
import { playFile, deleteFile } from '../lib/buzzerActions';
import { MusicNotesIcon, WrenchIcon, PlayIcon, TrashIcon, ArrowsLeftRightIcon, QuestionMarkIcon } from "phosphor-svelte";
import { InfoIcon, 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 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>
<div
@@ -48,17 +69,24 @@
<div class="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity shrink-0 ml-4">
<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"
title="Play Sound"
title="Auf dem Buzzer abspielen"
>
<PlayIcon size={16} weight="fill" />
</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"
title="Delete File"
title="Datei vom Buzzer löschen"
>
<TrashIcon size={16} weight="fill" />
</button>

View File

@@ -1,10 +1,10 @@
<script lang="ts">
import { buzzer } from '../lib/buzzerStore';
import { refreshFileList } from '../lib/buzzerActions';
import FileRow from './FileRow.svelte';
import { ArrowsCounterClockwiseIcon } from 'phosphor-svelte';
import { refreshFileList } from '../lib/buzzerActions';
async function handleRefresh() {
console.log("Aktualisiere Dateiliste...");
await refreshFileList();
}
</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() {
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++) {
export async function initializeBuzzer() {
try {
// console.log("Auto-Connect Versuch mit Port:", port.getInfo());
await connectToPort(port);
return; // Erfolg!
} catch (e) {
if (i < retryDelays.length) {
// console.log(`Reconnect Versuch ${i + 1} fehlgeschlagen, warte ${retryDelays[i]}ms...`);
await delay(retryDelays[i]);
} else {
console.error('Auto-Connect nach Retries endgültig fehlgeschlagen.');
}
}
}
}
}
const version = await new GetProtocolCommand().execute();
/**
* Kernfunktion für den Verbindungsaufbau
*/
export async function connectToPort(port: SerialPort) {
if (isConnecting || get(buzzer).connected) return;
isConnecting = true;
try {
// console.log("Versuche Verbindung mit Port:", port.getInfo());
await port.open({ baudRate: 115200 });
await delay(100);
setActivePort(port);
try {
// Validierung: Antwortet das Teil auf "info"?
const success = await Promise.race([
updateDeviceInfo(port),
new Promise<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");
if (version !== null) {
buzzer.update(s => ({ ...s, connected: true, protocol: version }));
// FIX 1: Flash-Info muss auch beim Start geladen werden!
await refreshFileList();
} catch (validationError) {
addToast("Buzzer-Validierung fehlgeschlagen!", "error");
await disconnectBuzzer();
await refreshFlashInfo();
addToast(`Buzzer bereit (v${version})`, 'success');
return true;
}
} catch (e: any) {
console.error("Initialisierung fehlgeschlagen:", e);
addToast(`Fehler: ${e.message}`, "error");
await bus.disconnect();
buzzer.update(s => ({ ...s, connected: false }));
}
return false;
}
export async function refreshFlashInfo() {
try {
if ('forget' in port) { // Check für Browser-Support
await (port as any).forget();
console.log("Gerät wurde erfolgreich entkoppelt.");
}
} catch (forgetError) {
console.error("Entkoppeln fehlgeschlagen:", forgetError);
}
throw new Error("Device ist kein gültiger Buzzer");
throw validationError; // Fehler an den äußeren Block weitergeben
}
} catch (e) {
setActivePort(null);
// Hier landen wir, wenn der User den Port-Dialog abbricht oder die Validierung fehlschlägt
throw e;
} finally {
isConnecting = false;
}
}
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;
const flashInfo = await new GetFlashInfoCommand().execute();
if (flashInfo) {
const totalSize = (flashInfo.total_size / (1024 * 1024));
const freeSize = (flashInfo.free_size / (1024 * 1024));
const fwSlotSize = (flashInfo.fw_slot_size / 1024);
const maxPathLength = flashInfo.max_path_length;
buzzer.update(s => ({
...s,
version: parts[1],
protocol: parseInt(parts[0]),
storage: { ...s.storage, total: totalMB, available: availableMB }
storage: { total: totalSize, available: freeSize }, // FIX 2: "storage" korrekt geschrieben
fw_slot_size: fwSlotSize,
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
}
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();
}
} // Funktion schließen

View File

@@ -2,15 +2,18 @@ import { writable } from 'svelte/store';
export const buzzer = writable({
connected: false,
version: 'v0.0.0',
protocol: 0,
build: 'unknown',
version: 'v0.0.0',
kernel_version: 'v0.0.0',
storage: {
total: 8.0, // 8 MB Flash laut Spezifikation
total: 8.0,
available: 0.0,
unknown: 8.0,
usedSys: 0.0,
usedAudio: 0.0
usedAudio: 0.0,
unknown: 0.0
},
files: [] as {name: string, size: string, crc32: number, isSystem: boolean, isSynced: boolean}[]
max_path_length: 15,
fw_slot_size: 0,
files: [] as {name: string, size: string, crc32: number | null, isSystem: boolean}[]
});

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 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 />
</div>
<div class="flex-1 flex flex-col overflow-hidden">
<FileStorage client:load />
</div>