From 844e8e06ca3a85ffa94b2198f2afa817043f6bb8 Mon Sep 17 00:00:00 2001 From: Eduard Iten Date: Sun, 1 Mar 2026 11:11:56 +0100 Subject: [PATCH] sync --- buzzer_tool/buzzer.py | 14 +- buzzer_tool/config.yaml | 5 + buzzer_tool/core/commands/check.py | 7 +- buzzer_tool/core/commands/get_tag.py | 56 +++++ buzzer_tool/core/util.py | 25 ++ firmware/pm_static.yml | 12 +- firmware/src/audio.c | 4 +- firmware/src/fs.c | 125 +++++++++- firmware/src/fs.h | 34 +++ firmware/src/protocol.c | 95 +++++++- firmware/src/protocol.h | 2 + firmware/src/utils.c | 12 + firmware/src/utils.h | 17 +- webpage/src/components/ConnectButton.svelte | 21 +- webpage/src/components/ToastContainer.svelte | 36 +++ webpage/src/lib/buzzerActions.ts | 236 ++++++++++++++----- webpage/src/pages/index.astro | 2 +- 17 files changed, 624 insertions(+), 79 deletions(-) create mode 100644 buzzer_tool/config.yaml create mode 100644 buzzer_tool/core/commands/get_tag.py create mode 100644 buzzer_tool/core/util.py diff --git a/buzzer_tool/buzzer.py b/buzzer_tool/buzzer.py index a43de5b..6145dd9 100644 --- a/buzzer_tool/buzzer.py +++ b/buzzer_tool/buzzer.py @@ -3,7 +3,7 @@ import argparse import sys from core.config import load_config from core.connection import BuzzerConnection, BuzzerError -from core.commands import info, ls, put, mkdir, rm, confirm, reboot, play, check +from core.commands import info, ls, put, mkdir, rm, confirm, reboot, play, check, get_tag def main(): parser = argparse.ArgumentParser(description="Edis Buzzer Host Tool") @@ -52,6 +52,10 @@ def main(): # Befehl: reboot reboot_parser = subparsers.add_parser("reboot", help="Startet den Buzzer neu") + # Befehl: get_tag + get_tag_parser = subparsers.add_parser("get_tag", help="Holt die Tags einer Datei") + get_tag_parser.add_argument("path", type=str, help="Pfad der Datei (z.B. /lfs/a/neu)") + # Argumente parsen args = parser.parse_args() config = load_config(args) @@ -105,6 +109,14 @@ def main(): print(f"CRC32 von '{args.path}': 0x{CRC32['crc32']:08x}") else: print(f"Fehler: Keine CRC32-Information für '{args.path}' erhalten.") + elif args.command == "get_tag": + tags = get_tag.execute(conn, path=args.path) + if tags: + print(f"Tags von '{args.path}':") + for key, value in tags.items(): + print(f" {key}: {value}") + else: + print(f"Fehler: Keine Tags für '{args.path}' erhalten.") elif args.command == "info" or args.command is None: # Wurde kein Befehl oder explizit 'info' angegeben, sind wir hier schon fertig pass diff --git a/buzzer_tool/config.yaml b/buzzer_tool/config.yaml new file mode 100644 index 0000000..e2c4e2b --- /dev/null +++ b/buzzer_tool/config.yaml @@ -0,0 +1,5 @@ +# config.yaml +serial: + port: "COM17" + baudrate: 250000 + timeout: 10 diff --git a/buzzer_tool/core/commands/check.py b/buzzer_tool/core/commands/check.py index e529570..5af6a7b 100644 --- a/buzzer_tool/core/commands/check.py +++ b/buzzer_tool/core/commands/check.py @@ -8,13 +8,10 @@ def execute(conn, path: str) -> dict: raise BuzzerError("Keine Antwort auf 'check' empfangen.") parts = lines[0].split() - if len(parts) != 3 or parts[0] != "CRC32": + if len(parts) != 1: raise BuzzerError(f"Unerwartetes Check-Format: {lines[0]}") - - if parts[1] != path: - raise BuzzerError(f"Unerwarteter Pfad in Check-Antwort: {parts[1]} (erwartet: {path})") - crc32 = int(parts[2], 16) + crc32 = int(parts[0], 16) return { "crc32": crc32 diff --git a/buzzer_tool/core/commands/get_tag.py b/buzzer_tool/core/commands/get_tag.py new file mode 100644 index 0000000..1b788ce --- /dev/null +++ b/buzzer_tool/core/commands/get_tag.py @@ -0,0 +1,56 @@ +# core/commands/get_tag.py +from core.connection import BuzzerError +from core.util import hex_to_bytearray + +def execute(conn, path: str) -> dict: + """Holt Tags einer Datei und gibt sie als strukturiertes Dictionary zurück.""" + lines = conn.send_command("get_tag " + path) + if not lines: + raise BuzzerError("Keine Antwort auf 'get_tag' empfangen.") + + parts = lines[0].split() + if len(parts) != 1: + raise BuzzerError(f"Unerwartetes get_tag-Format: {lines[0]}") + + data = hex_to_bytearray(parts[0]) + if data is None: + raise BuzzerError("Ungültiger Hex-String in get_tag-Antwort.") + + pos = 0 + tags = {} + while pos < len(data): + tag_type = data[pos] + pos += 1 + if pos >= len(data): + raise BuzzerError(f"Unerwartetes Ende des Hex-Strings bei get_tag (Tag-Typ erwartet). Position: {pos}/{len(data)}") + + match tag_type: + case 0x01: # Kommentar + length = data[pos] + pos += 1 + if pos + length > len(data): + raise BuzzerError(f"Unerwartetes Ende des Hex-Strings bei get_tag (Kommentar erwartet). Position: {pos}") + comment = data[pos:pos+length].decode('utf-8') + pos += length + tags["comment"] = comment + + case 0x02: # Author + length = data[pos] + pos += 1 + if pos + length > len(data): + raise BuzzerError(f"Unerwartetes Ende des Hex-Strings bei get_tag (Author erwartet). Position: {pos}") + author = data[pos:pos+length].decode('utf-8') + pos += length + tags["author"] = author + + case 0x10: # CRC32 + if pos + 4 > len(data): + raise BuzzerError(f"Unerwartetes Ende des Hex-Strings bei get_tag (CRC32 erwartet). Position: {pos}") + crc32 = int.from_bytes(data[pos:pos+4], byteorder='big') + pos += 4 + tags["crc32"] = hex(crc32) + + case _: # Default / Unbekannter Tag + tags[f"unknown_0x{tag_type:02x}"] = tag_value_raw.hex() + + return tags \ No newline at end of file diff --git a/buzzer_tool/core/util.py b/buzzer_tool/core/util.py new file mode 100644 index 0000000..a36726b --- /dev/null +++ b/buzzer_tool/core/util.py @@ -0,0 +1,25 @@ +def hex_to_bytearray(hex_string): + """ + Wandelt einen Hex-String (z.B. "deadbeef") in ein bytearray um. + Entfernt vorher Leerzeichen und prüft auf Gültigkeit. + """ + try: + # Whitespace entfernen (falls vorhanden) + clean_hex = hex_string.strip().replace(" ", "") + + # Konvertierung + return bytearray.fromhex(clean_hex) + + except ValueError as e: + print(f"Fehler bei der Konvertierung: {e}") + return None + +def string_to_hexstring(text): + """ + Wandelt einen String in einen UTF-8-kodierten Hex-String um. + """ + # 1. String zu UTF-8 Bytes + utf8_bytes = text.encode('utf-8') + + # 2. Bytes zu Hex-String + return utf8_bytes.hex() \ No newline at end of file diff --git a/firmware/pm_static.yml b/firmware/pm_static.yml index 20b8a1c..66455d3 100644 --- a/firmware/pm_static.yml +++ b/firmware/pm_static.yml @@ -3,9 +3,10 @@ mcuboot: size: 0xC000 region: flash_primary +# Primary Slot: Start bleibt 0xC000, Größe jetzt 200KB (0x32000) mcuboot_primary: address: 0xC000 - size: 0x25000 + size: 0x32000 region: flash_primary mcuboot_pad: @@ -13,16 +14,19 @@ mcuboot_pad: size: 0x200 region: flash_primary +# Die App startet nach dem Padding des Primary Slots app: address: 0xC200 - size: 0x24E00 + size: 0x31E00 # (0x32000 - 0x200) region: flash_primary +# Secondary Slot: Startet jetzt bei 0xC000 + 0x32000 = 0x3E000 mcuboot_secondary: - address: 0x31000 - size: 0x25000 + address: 0x3E000 + size: 0x32000 region: flash_primary +# External Flash bleibt unverändert littlefs_storage: address: 0x0 size: 0x800000 diff --git a/firmware/src/audio.c b/firmware/src/audio.c index aa08818..7913a29 100644 --- a/firmware/src/audio.c +++ b/firmware/src/audio.c @@ -243,6 +243,8 @@ void audio_thread(void *arg1, void *arg2, void *arg3) continue; } + ssize_t file_size = fs_get_audio_data_len(&file); + LOG_INF("Playing: %s", filename); io_status(true); @@ -269,7 +271,7 @@ void audio_thread(void *arg1, void *arg2, void *arg3) break; } - ssize_t bytes_read = fs_read(&file, block, AUDIO_BLOCK_SIZE / 2); + ssize_t bytes_read = fs_read_audio(&file, block, AUDIO_BLOCK_SIZE / 2, file_size); if (bytes_read <= 0) { diff --git a/firmware/src/fs.c b/firmware/src/fs.c index 6d9943e..d089e3d 100644 --- a/firmware/src/fs.c +++ b/firmware/src/fs.c @@ -3,8 +3,11 @@ #include #include #include + #include -LOG_MODULE_REGISTER(buzz_fs, LOG_LEVEL_INF); +#include + +LOG_MODULE_REGISTER(buzz_fs, LOG_LEVEL_DBG); #define STORAGE_PARTITION_ID FIXED_PARTITION_ID(littlefs_storage) #define SLOT1_ID FIXED_PARTITION_ID(slot1_partition) @@ -167,6 +170,126 @@ int fs_pm_mkdir(const char *path) fs_pm_flash_suspend(); return rc; } + +ssize_t fs_get_audio_data_len(struct fs_file_t *fp) { + uint8_t footer[6]; + off_t file_size; + + fs_seek(fp, 0, FS_SEEK_END); + file_size = fs_tell(fp); + + fs_seek(fp, 0, FS_SEEK_SET); + if (file_size < 6) return file_size; + + fs_seek(fp, -6, FS_SEEK_END); + if (fs_read(fp, footer, 6) == 6) { + if (memcmp(&footer[2], "TAG!", 4) == 0) { + uint16_t tag_len = footer[0] | (footer[1] << 8); + if (tag_len <= file_size) { + fs_seek(fp, 0, FS_SEEK_SET); + return file_size - tag_len; + } + } + } + fs_seek(fp, 0, FS_SEEK_SET); + return file_size; +} + +ssize_t fs_read_audio(struct fs_file_t *fp, void *buffer, size_t len, size_t audio_limit) { + off_t current_pos = fs_tell(fp); + if (current_pos >= audio_limit) { + return 0; // "Virtuelles" Dateiende erreicht + } + + size_t remaining = audio_limit - current_pos; + size_t to_read = (len < remaining) ? len : remaining; + return fs_read(fp, buffer, to_read); +} + +int fs_write_hex_tag(struct fs_file_t *fp, const char *hex_str) { + size_t hex_len = strlen(hex_str); + + // Ein Hex-String muss eine gerade Anzahl an Zeichen haben + if (hex_len % 2 != 0) return -EINVAL; + + size_t payload_len = hex_len / 2; + uint16_t total_footer_len = (uint16_t)(payload_len + 6); + + // 1. Audio-Ende bestimmen und dorthin seeken + size_t audio_limit = fs_get_audio_data_len(fp); + fs_seek(fp, audio_limit, FS_SEEK_SET); + + // 2. Payload Byte für Byte konvertieren und schreiben + for (size_t i = 0; i < hex_len; i += 2) { + int high = hex2int(hex_str[i]); + int low = hex2int(hex_str[i+1]); + + if (high < 0 || low < 0) return -EINVAL; // Ungültiges Hex-Zeichen + + uint8_t byte = (uint8_t)((high << 4) | low); + fs_write(fp, &byte, 1); + } + + // 3. Die 2 Bytes Länge schreiben (Little Endian) + uint8_t len_bytes[2]; + len_bytes[0] = (uint8_t)(total_footer_len & 0xFF); + len_bytes[1] = (uint8_t)((total_footer_len >> 8) & 0xFF); + fs_write(fp, len_bytes, 2); + + // 4. Magic Bytes schreiben + fs_write(fp, "TAG!", 4); + + // 5. Datei am aktuellen Punkt abschneiden + off_t current_pos = fs_tell(fp); + return fs_truncate(fp, current_pos); +} + +int fs_read_hex_tag(struct fs_file_t *fp, char *hex_str, size_t hex_str_size) { + if (hex_str == NULL || hex_str_size == 0) { + return -EINVAL; + } + + hex_str[0] = '\0'; + + // Dateigröße ermitteln + fs_seek(fp, 0, FS_SEEK_END); + off_t file_size = fs_tell(fp); + + // Audio-Limit finden (Anfang des Payloads) + size_t audio_limit = fs_get_audio_data_len(fp); + + // Prüfen, ob überhaupt ein Tag existiert (audio_limit < file_size) + if (audio_limit >= file_size) { + // Kein Tag vorhanden -> leerer String + return 0; + } + + // Die Payload-Länge ist: Gesamtgröße - Audio - 6 Bytes (Länge + Magic) + size_t payload_len = file_size - audio_limit - 6; + + if ((payload_len * 2U) + 1U > hex_str_size) { + return -ENOMEM; // Nicht genug Platz im Zielpuffer + } + + // Zum Anfang des Payloads springen + fs_seek(fp, audio_limit, FS_SEEK_SET); + + uint8_t byte; + for (size_t i = 0; i < payload_len; i++) { + if (fs_read(fp, &byte, 1) != 1) { + return -EIO; + } + + // Jedes Byte als zwei Hex-Zeichen in den Zielpuffer schreiben + hex_str[i * 2] = int2hex(byte >> 4); + hex_str[i * 2 + 1] = int2hex(byte & 0x0F); + } + + hex_str[payload_len * 2] = '\0'; + + return 0; +} + int flash_get_slot_info(slot_info_t *info) { if (slot1_info.size != 0) { *info = slot1_info; diff --git a/firmware/src/fs.h b/firmware/src/fs.h index fc4272d..ebcda92 100644 --- a/firmware/src/fs.h +++ b/firmware/src/fs.h @@ -82,6 +82,40 @@ int fs_pm_statvfs(const char *path, struct fs_statvfs *stat); */ int fs_pm_mkdir(const char *path); +/** + * @brief Gets the length of the audio data in a file, accounting for any metadata tags + * @param fp Pointer to an open fs_file_t structure representing the audio file + * @return Length of the audio data in bytes, or negative error code on failure + */ +int fs_get_audio_data_len(struct fs_file_t *fp); + +/** + * @brief Reads audio data from a file, ensuring that it does not read past the audio data limit + * @param fp Pointer to an open fs_file_t structure representing the audio file + * @param buffer Pointer to the buffer to read data into + * @param len Maximum number of bytes to read + * @param audio_limit Maximum byte offset for audio data (e.g. file size minus metadata) + * @return Number of bytes read, or negative error code on failure + */ +int fs_read_audio(struct fs_file_t *fp, void *buffer, size_t len, size_t audio_limit); + +/** + * @brief Writes a hexadecimal string as a metadata tag at the end of an audio file + * @param fp Pointer to an open fs_file_t structure representing the audio file + * @param hex_str Null-terminated string containing hexadecimal characters (0-9, a-f, A-F) + * @return 0 on success, negative error code on failure + */ +int fs_write_hex_tag(struct fs_file_t *fp, const char *hex_str); + +/** + * @brief Reads a hexadecimal string from a metadata tag at the end of an audio file + * @param fp Pointer to an open fs_file_t structure representing the audio file + * @param hex_str Buffer to be filled with the hexadecimal string (must be large enough to hold the data) + * @param hex_str_size Size of the hex_str buffer + * @return 0 on success, negative error code on failure + */ +int fs_read_hex_tag(struct fs_file_t *fp, char *hex_str, size_t hex_str_size); + /** * @brief Retrieves information about the firmware slot, such as start address and size * @param info Pointer to slot_info_t structure to be filled with slot information diff --git a/firmware/src/protocol.c b/firmware/src/protocol.c index b619e9b..44285bd 100644 --- a/firmware/src/protocol.c +++ b/firmware/src/protocol.c @@ -15,7 +15,7 @@ #define PROTOCOL_VERSION 2 -LOG_MODULE_REGISTER(protocol, LOG_LEVEL_DBG); +LOG_MODULE_REGISTER(protocol, LOG_LEVEL_INF); #define PROTOCOL_STACK_SIZE 2048 #define PROTOCOL_PRIORITY 6 @@ -319,6 +319,63 @@ int cmd_rm(const char *path) return rc; } +int cmd_set_tag(const char *param) +{ + LOG_DBG("SET_TAG command received with parameter: '%s'", param); + if (param == NULL || param[0] == '\0') + { + LOG_ERR("SET_TAG command requires a non-empty parameter"); + return -EINVAL; + } + uint8_t tag_buffer[256]; + uint8_t filename[64]; + int rc = sscanf(param, "%63[^;];%255[^\n]", filename, tag_buffer); + if (rc != 2) { + LOG_ERR("Invalid parameters for SET_TAG command (got %d): '%s'", rc, param); + return -EINVAL; + } + struct fs_file_t file; + fs_file_t_init(&file); + rc = fs_pm_open(&file, (const char *)filename, FS_O_READ | FS_O_WRITE); + + if (rc < 0) { + LOG_ERR("Failed to open file '%s' for SET_TAG: %d", filename, rc); + return rc; + } + rc = fs_write_hex_tag(&file, (const char *)tag_buffer); + fs_pm_close(&file); + if (rc < 0) { + LOG_ERR("Failed to write tag to file '%s': %d", filename, rc); + return rc; + } + LOG_DBG("Tag written successfully to file '%s'", filename); + return 0; +} + +int cmd_get_tag(const char *param) +{ + LOG_DBG("GET_TAG command received"); + struct fs_file_t file; + fs_file_t_init(&file); + int rc = fs_pm_open(&file, param, FS_O_READ); + if (rc < 0) { + LOG_ERR("Failed to open file '%s' for GET_TAG: %d", param, rc); + return rc; + } + uint8_t tag_buffer[256]; + rc = fs_read_hex_tag(&file, tag_buffer, sizeof(tag_buffer)); + fs_pm_close(&file); + if (rc < 0) { + LOG_ERR("Failed to read tag from file '%s': %d", param, rc); + return rc; + } + LOG_DBG("Tag read successfully from file '%s': '%s'", param, tag_buffer); + LOG_HEXDUMP_DBG(tag_buffer, strlen((char *)tag_buffer), "Tag content"); + usb_write_buffer(tag_buffer, strlen((char *)tag_buffer)); + usb_write_char('\n'); + return 0; +} + int cmd_confirm_firmware() { if (!boot_is_img_confirmed()) @@ -370,7 +427,8 @@ int cmd_check(const char *param) uint32_t start_time = k_uptime_get_32(); uint8_t buffer[256]; ssize_t read; - while ((read = fs_read(&file, buffer, sizeof(buffer))) > 0) + ssize_t file_size = fs_get_audio_data_len(&file); + while ((read = fs_read_audio(&file, buffer, sizeof(buffer), file_size)) > 0) { crc32 = crc32_ieee_update(crc32, buffer, read); } @@ -383,7 +441,7 @@ int cmd_check(const char *param) uint32_t duration = k_uptime_get_32() - start_time; LOG_DBG("Check successful: file '%s' has CRC32 0x%08x, check took %u ms", param, crc32, duration); char response[64]; - snprintf(response, sizeof(response), "CRC32 %s 0x%08x\n", param, crc32); + snprintf(response, sizeof(response), "0x%08x\n", crc32); usb_write_buffer((const uint8_t *)response, strlen(response)); return 0; } @@ -506,6 +564,28 @@ void execute_current_command(void) send_error(protocol_map_error(rc)); } break; + case CMD_SET_TAG: + LOG_DBG("Executing SET_TAG command"); + rc = cmd_set_tag((char *)buffer); + if (rc == 0) + { + send_ok(); + } + else + { + send_error(protocol_map_error(rc)); + } + break; + case CMD_GET_TAG: + LOG_DBG("Executing GET_TAG command"); + rc = cmd_get_tag((char *)buffer); + if (rc == 0) { + send_ok(); + } + else { + send_error(protocol_map_error(rc)); + } + break; default: LOG_ERR("No execution logic for command %d", current_command); send_error(P_ERR_NOT_SUPPORTED); @@ -576,6 +656,15 @@ protocol_state_t reading_command(uint8_t byte) { LOG_DBG("Received CHECK command"); current_command = CMD_CHECK; + } else if (strcmp((char *)buffer, "gett") == 0) + { + LOG_DBG("Received GETT command"); + current_command = CMD_GET_TAG; + } + else if (strcmp((char *)buffer, "sett") == 0) + { + LOG_DBG("Received SETT command"); + current_command = CMD_SET_TAG; } else { diff --git a/firmware/src/protocol.h b/firmware/src/protocol.h index d66e4f1..602f927 100644 --- a/firmware/src/protocol.h +++ b/firmware/src/protocol.h @@ -18,6 +18,8 @@ typedef enum { CMD_CONFIRM, CMD_REBOOT, CMD_PLAY, + CMD_SET_TAG, + CMD_GET_TAG, CMD_CHECK, /* Weitere Kommandos folgen hier */ } protocol_cmd_t; diff --git a/firmware/src/utils.c b/firmware/src/utils.c index 3d0ce71..011703f 100644 --- a/firmware/src/utils.c +++ b/firmware/src/utils.c @@ -50,4 +50,16 @@ uint8_t get_reboot_status(void) printk("Reboot status detected: 0x%02x\n", status); } return status; +} + +int hex2int(char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; // Fehlerhaftes Zeichen +} + +char int2hex(uint8_t i) { + if (i < 10) return '0' + i; + return 'a' + (i - 10); } \ No newline at end of file diff --git a/firmware/src/utils.h b/firmware/src/utils.h index 2bc2354..bb1fe27 100644 --- a/firmware/src/utils.h +++ b/firmware/src/utils.h @@ -21,4 +21,19 @@ void reboot_with_status(uint8_t status_code); * @return The reboot status code set before the last reboot, or 0 if no status was set. */ uint8_t get_reboot_status(); -#endif // UTILS_H \ No newline at end of file + +/** + * @brief Converts a hexadecimal character to its integer value. + * @param c The hexadecimal character (0-9, a-f, A-F) to convert. + * @return The integer value of the hexadecimal character, or -1 if the character is not a valid hexadecimal digit. + */ +int hex2int(char c); + +/** + * @brief Converts an integer value to its hexadecimal character representation. + * @param i The integer value to convert (0-15). + * @return The hexadecimal character representation of the integer value. + */ +char int2hex(uint8_t i); + +#endif // UTILS_H diff --git a/webpage/src/components/ConnectButton.svelte b/webpage/src/components/ConnectButton.svelte index 1c266f0..9377c69 100644 --- a/webpage/src/components/ConnectButton.svelte +++ b/webpage/src/components/ConnectButton.svelte @@ -1,20 +1,33 @@ + +
+ {#each $toasts as toast (toast.id)} +
+
+ {#if toast.type === 'success'}{/if} + {#if toast.type === 'error'}{/if} + {#if toast.type === 'warning'}{/if} + {#if toast.type === 'info'}{/if} +
+ +
+ {toast.message} +
+ + +
+ {/each} +
\ No newline at end of file diff --git a/webpage/src/lib/buzzerActions.ts b/webpage/src/lib/buzzerActions.ts index c68d52b..64c515c 100644 --- a/webpage/src/lib/buzzerActions.ts +++ b/webpage/src/lib/buzzerActions.ts @@ -3,12 +3,13 @@ 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; + key?: string; }; class SerialQueue { @@ -16,6 +17,9 @@ class SerialQueue { private isProcessing = false; private port: SerialPort | null = null; + private reader: ReadableStreamDefaultReader | null = null; + private writer: WritableStreamDefaultWriter | null = null; + setPort(port: SerialPort | null) { this.port = port; } async add(command: string, priority = 1, key?: string): Promise { @@ -43,38 +47,81 @@ class SerialQueue { try { const encoder = new TextEncoder(); const decoder = new TextDecoder(); - const writer = this.port.writable.getWriter(); - const reader = this.port.readable.getReader(); - await writer.write(encoder.encode(task.command + "\n")); - writer.releaseLock(); + // 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) { - const { value, done } = await reader.read(); + // 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; // Auf ERR achten! + if (raw.includes("OK") || raw.includes("ERR")) break; } - - reader.releaseLock(); - const lines = raw.split('\n').map(l => l.trim()).filter(l => l); - // AUTOMATISCHE FEHLERERKENNUNG + 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); - } + if (errorLine) addToast(`Gerätefehler: ${errorLine}`, 'error', 5000); task.resolve(lines.filter(l => l !== "OK" && !l.startsWith("ERR") && !l.startsWith(task.command))); } catch (e) { - addToast("Kommunikationsfehler (Serial Port)", "error"); - console.error(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(); @@ -87,7 +134,7 @@ export function initSerialListeners() { // 1. Wenn ein bereits gekoppeltes Gerät eingesteckt wird navigator.serial.addEventListener('connect', (event) => { - console.log('Neues Gerät erkannt, starte Auto-Connect...'); + // console.log('Neues Gerät erkannt, starte Auto-Connect...'); autoConnect(); }); @@ -103,14 +150,23 @@ export async function autoConnect() { const ports = await navigator.serial.getPorts(); if (ports.length > 0) { - // Wir nehmen den ersten verfügbaren Port const port = ports[0]; - console.log('Gekoppeltes Gerät gefunden, verbinde...'); - - try { - await connectToPort(port); - } catch (e) { - console.warn('Auto-Connect fehlgeschlagen:', e); + const retryDelays = [100, 500]; + + // Erster Versuch + 2 Retries = max 3 Versuche + for (let i = 0; i <= retryDelays.length; i++) { + try { + // console.log("Auto-Connect Versuch mit Port:", port.getInfo()); + await connectToPort(port); + return; // Erfolg! + } catch (e) { + if (i < retryDelays.length) { + // console.log(`Reconnect Versuch ${i + 1} fehlgeschlagen, warte ${retryDelays[i]}ms...`); + await delay(retryDelays[i]); + } else { + console.error('Auto-Connect nach Retries endgültig fehlgeschlagen.'); + } + } } } } @@ -123,29 +179,49 @@ export async function connectToPort(port: SerialPort) { isConnecting = true; try { + // console.log("Versuche Verbindung mit Port:", port.getInfo()); await port.open({ baudRate: 115200 }); - - port.addEventListener('disconnect', () => { - addToast("Buzzer-Verbindung verloren!", "warning"); - handleDisconnect(); - }); - + await delay(100); setActivePort(port); - buzzer.update(s => ({ ...s, connected: true })); - - // Kleiner Timeout, damit die UI Zeit zum Hydrieren hat - setTimeout(() => { + + try { + // Validierung: Antwortet das Teil auf "info"? + const success = await Promise.race([ + updateDeviceInfo(port), + new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), 1500)) + ]); + + if (!success) throw new Error("Kein Buzzer"); + + port.addEventListener('disconnect', () => { + addToast("Buzzer-Verbindung verloren!", "warning"); + handleDisconnect(); + }); + + buzzer.update(s => ({ ...s, connected: true })); addToast("Buzzer erfolgreich verbunden", "success"); - }, 100); - - await updateDeviceInfo(port); - await refreshFileList(); - } catch (e) { - console.error("Connect error:", e); - // Nur Toasten, wenn es kein "Already Open" Fehler ist - if (!(e instanceof Error && e.name === 'InvalidStateError')) { - addToast("Verbindung fehlgeschlagen", "error"); + + await refreshFileList(); + } catch (validationError) { + addToast("Buzzer-Validierung fehlgeschlagen!", "error"); + await disconnectBuzzer(); + + try { + if ('forget' in port) { // Check für Browser-Support + await (port as any).forget(); + console.log("Gerät wurde erfolgreich entkoppelt."); + } + } catch (forgetError) { + console.error("Entkoppeln fehlgeschlagen:", forgetError); + } + + throw new Error("Device ist kein gültiger Buzzer"); + throw validationError; // Fehler an den äußeren Block weitergeben } + } catch (e) { + setActivePort(null); + // Hier landen wir, wenn der User den Port-Dialog abbricht oder die Validierung fehlschlägt + throw e; } finally { isConnecting = false; } @@ -153,8 +229,8 @@ export async function connectToPort(port: SerialPort) { function handleDisconnect() { setActivePort(null); - buzzer.update(s => ({ - ...s, + buzzer.update(s => ({ + ...s, connected: false, files: [] // Liste leeren, da Gerät weg })); @@ -166,9 +242,12 @@ export function setActivePort(port: SerialPort | null) { queue.setPort(port); } -// Diese Funktion fehlte im letzten Block! -export async function updateDeviceInfo(port: SerialPort) { - // Wir nutzen hier direkt die Queue für Konsistenz +export async function disconnectBuzzer() { + await queue.close(); + handleDisconnect(); +} + +export async function updateDeviceInfo(port: SerialPort): Promise { const lines = await queue.add("info", 1); if (lines.length > 0) { const parts = lines[0].split(';'); @@ -177,8 +256,9 @@ export async function updateDeviceInfo(port: SerialPort) { const totalPages = parseInt(parts[3]); const availablePages = parseInt(parts[4]); - const totalMB = (totalPages * pageSize) / (1024 * 1024); - const availableMB = (availablePages * pageSize) / (1024 * 1024); + // MB Berechnung mit dem korrekten Divisor (1024 * 1024) + const totalMB = (totalPages * pageSize) / 1048576; + const availableMB = (availablePages * pageSize) / 1048576; buzzer.update(s => ({ ...s, @@ -186,32 +266,72 @@ export async function updateDeviceInfo(port: SerialPort) { protocol: parseInt(parts[0]), storage: { ...s.storage, total: totalMB, available: availableMB } })); + return true; // Validierung erfolgreich } } + return false; // Keine gültigen Daten erhalten } export async function refreshFileList() { + let totalSystemBytes = 0; + let totalAudioBytes = 0; + + // 1. System-Größe abfragen (nur Summieren, keine Liste speichern) + const syslines = await queue.add("ls /lfs/sys", 1, 'ls'); + syslines.forEach(line => { + const parts = line.split(','); + if (parts.length >= 2) { + totalSystemBytes += parseInt(parts[1]); + } + }); + + // 2. Audio-Files abfragen und Liste für das UI erstellen const lines = await queue.add("ls /lfs/a", 1, 'ls'); const audioFiles = lines.map(line => { const parts = line.split(','); if (parts.length < 3) return null; - const [type, size, name] = parts; - return { - name, - size: (parseInt(size) / 1024).toFixed(1) + " KB", - crc32: 0, - isSystem: false + 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[]; - buzzer.update(s => ({ ...s, files: audioFiles })); + // 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 (!file.crc32) { + 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]+)/); diff --git a/webpage/src/pages/index.astro b/webpage/src/pages/index.astro index 0164e11..bb22865 100644 --- a/webpage/src/pages/index.astro +++ b/webpage/src/pages/index.astro @@ -8,7 +8,7 @@ import ConnectButton from "../components/ConnectButton.svelte"; import SerialWarning from "../components/SerialWarning.svelte"; import FileStorage from "../components/FileStorage.svelte"; import DeviceInfo from "../components/DeviceInfo.svelte"; -import ToastContainer from "../components/ToastContainer.svelte"; +import ToastContainer from '../components/ToastContainer.svelte'; import type { loadRenderers } from "astro:container"; ---