From b665cb5deff532f82996820c955a4c6f962930c9 Mon Sep 17 00:00:00 2001 From: Eduard Iten Date: Mon, 2 Mar 2026 00:25:40 +0100 Subject: [PATCH] sync --- buzzer_tool/buzzer.py | 142 ++- buzzer_tool/config.yaml | 6 +- buzzer_tool/config.yaml.example | 4 +- buzzer_tool/core/commands/check.py | 64 +- buzzer_tool/core/commands/confirm.py | 9 +- buzzer_tool/core/commands/fw_put.py | 57 + buzzer_tool/core/commands/get_tag.py | 56 - buzzer_tool/core/commands/info.py | 29 +- buzzer_tool/core/commands/ls.py | 2 +- buzzer_tool/core/commands/mkdir.py | 2 +- buzzer_tool/core/commands/mv.py | 10 + buzzer_tool/core/commands/pull.py | 62 + buzzer_tool/core/commands/put.py | 246 +++- buzzer_tool/core/commands/reboot.py | 9 +- buzzer_tool/core/commands/rm.py | 25 +- buzzer_tool/core/commands/stat.py | 5 + buzzer_tool/core/commands/tags.py | 140 +++ buzzer_tool/core/config.py | 10 +- buzzer_tool/core/connection.py | 722 +++++++++++- buzzer_tool/smoke_test.sh | 165 +++ firmware/Protokoll.md | 303 +++++ firmware/Tags.md | 133 +++ firmware/VERSION | 10 +- firmware/prj.conf | 4 +- firmware/src/audio.c | 2 +- firmware/src/fs.c | 239 ++-- firmware/src/fs.h | 60 +- firmware/src/protocol.c | 1639 ++++++++++++++++---------- firmware/src/protocol.h | 59 +- firmware/src/utils.c | 12 - firmware/src/utils.h | 14 - x | 0 32 files changed, 3287 insertions(+), 953 deletions(-) create mode 100644 buzzer_tool/core/commands/fw_put.py delete mode 100644 buzzer_tool/core/commands/get_tag.py create mode 100644 buzzer_tool/core/commands/mv.py create mode 100644 buzzer_tool/core/commands/pull.py create mode 100644 buzzer_tool/core/commands/stat.py create mode 100644 buzzer_tool/core/commands/tags.py create mode 100755 buzzer_tool/smoke_test.sh create mode 100644 firmware/Protokoll.md create mode 100644 firmware/Tags.md create mode 100644 x diff --git a/buzzer_tool/buzzer.py b/buzzer_tool/buzzer.py index 6145dd9..18c2623 100644 --- a/buzzer_tool/buzzer.py +++ b/buzzer_tool/buzzer.py @@ -1,9 +1,6 @@ # buzzer.py 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, get_tag def main(): parser = argparse.ArgumentParser(description="Edis Buzzer Host Tool") @@ -12,6 +9,7 @@ def main(): parser.add_argument("-p", "--port", type=str, help="Serielle Schnittstelle (z.B. COM15)") parser.add_argument("-b", "--baudrate", type=int, help="Verbindungsgeschwindigkeit") parser.add_argument("-t", "--timeout", type=float, help="Timeout in Sekunden (Standard: 5.0)") + parser.add_argument("--no-auto-info", action="store_true", help="Überspringt den automatischen Info-Call beim Start") # Subkommandos einrichten subparsers = parser.add_subparsers(dest="command", help="Verfügbare Befehle") @@ -26,8 +24,19 @@ def main(): # Befehl: put put_parser = subparsers.add_parser("put", help="Lädt eine oder mehrere Dateien auf den Controller hoch") - put_parser.add_argument("sources", nargs="+", help="Lokale Quelldatei(en) oder Wildcards (z.B. *.raw)") + put_parser.add_argument("sources", nargs="+", help="Lokale Quelldatei(en), Verzeichnisse oder Wildcards (z.B. *.raw)") put_parser.add_argument("target", type=str, help="Zielpfad auf dem Controller (Verzeichnis muss mit '/' enden)") + put_parser.add_argument("-r", "--recursive", action="store_true", help="Verzeichnisse rekursiv hochladen") + + # Befehl: put_many + put_many_parser = subparsers.add_parser("put_many", help="Lädt mehrere Dateien/Verzeichnisse (typisch rekursiv) hoch") + put_many_parser.add_argument("sources", nargs="+", help="Lokale Quelldatei(en), Verzeichnisse oder Wildcards") + put_many_parser.add_argument("target", type=str, help="Zielpfad auf dem Controller") + put_many_parser.add_argument("-r", "--recursive", action="store_true", help="Verzeichnisse rekursiv hochladen (Standard für put_many)") + + # Befehl: fw_put + fw_put_parser = subparsers.add_parser("fw_put", help="Lädt eine Firmware in den Secondary Slot (Test-Upgrade)") + fw_put_parser.add_argument("source", type=str, help="Lokale Firmware-Datei (.bin)") # Befehl: mkdir mkdir_parser = subparsers.add_parser("mkdir", help="Erstellt ein neues Verzeichnis") @@ -38,6 +47,25 @@ def main(): rm_parser.add_argument("path", type=str, help="Pfad der zu löschenden Datei/Ordner") rm_parser.add_argument("-r", "--recursive", action="store_true", help="Ordnerinhalte rekursiv löschen") + # Befehl: stat + stat_parser = subparsers.add_parser("stat", help="Zeigt Typ und Größe einer Datei/eines Verzeichnisses") + stat_parser.add_argument("path", type=str, help="Pfad der Datei/des Verzeichnisses") + + # Befehl: mv + mv_parser = subparsers.add_parser("mv", help="Benennt eine Datei/ein Verzeichnis um oder verschiebt es") + mv_parser.add_argument("source", type=str, help="Alter Pfad") + mv_parser.add_argument("target", type=str, help="Neuer Pfad") + + # Befehl: pull + pull_parser = subparsers.add_parser("pull", help="Lädt eine Datei vom Controller herunter") + pull_parser.add_argument("source", type=str, help="Quellpfad auf dem Controller") + pull_parser.add_argument("target", nargs="?", default=None, help="Optionaler lokaler Zielpfad") + + # Alias: get_file + get_file_parser = subparsers.add_parser("get_file", help="Alias für pull") + get_file_parser.add_argument("source", type=str, help="Quellpfad auf dem Controller") + get_file_parser.add_argument("target", nargs="?", default=None, help="Optionaler lokaler Zielpfad") + # Befehl: play play_parser = subparsers.add_parser("play", help="Spielt eine Datei auf dem Controller ab") play_parser.add_argument("path", type=str, help="Pfad der abzuspielenden Datei (z.B. /lfs/a/neu)") @@ -52,39 +80,58 @@ 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)") + # Befehl: get_tags (neuer Blob/TLV-Parser) + get_tags_parser = subparsers.add_parser("get_tags", help="Holt alle Tags einer Datei als JSON") + get_tags_parser.add_argument("path", type=str, help="Pfad der Datei") + + # Befehl: write_tags (merge/replace per JSON) + write_tags_parser = subparsers.add_parser("write_tags", help="Fügt Tags ein/ersetzt bestehende Tags per JSON") + write_tags_parser.add_argument("path", type=str, help="Pfad der Datei") + write_tags_parser.add_argument("json", type=str, help="JSON-Objekt oder @datei.json") + + # Befehl: remove_tag + remove_tag_parser = subparsers.add_parser("remove_tag", help="Entfernt einen Tag und schreibt den Rest zurück") + remove_tag_parser.add_argument("path", type=str, help="Pfad der Datei") + remove_tag_parser.add_argument("key", type=str, choices=["description", "author", "crc32", "fileformat"], help="Zu entfernender Tag-Key") # Argumente parsen args = parser.parse_args() + from core.config import load_config config = load_config(args) - print("--- Aktuelle Verbindungsparameter ---------------------") + print("--- Aktuelle Verbindungsparameter -------------------------------") print(f"Port: {config.get('port', 'Nicht definiert')}") print(f"Baudrate: {config.get('baudrate')}") print(f"Timeout: {config.get('timeout')}s") - print("-" * 55) + print("-" * 65) if not config.get("port"): print("Abbruch: Es muss ein Port in der config.yaml oder via --port definiert werden.") sys.exit(1) + from core.connection import BuzzerConnection, BuzzerError + try: with BuzzerConnection(config) as conn: - # 1. Immer die Info holen und anzeigen - sys_info = info.execute(conn) - - # Neu: Status auslesen und Farbe zuweisen (Grün für CONFIRMED, Gelb für UNCONFIRMED) - status = sys_info.get("image_status", "UNKNOWN") - status_color = "\033[32m" if status == "CONFIRMED" else "\033[33m" - - # Neu: Die print-Anweisung enthält nun den formatierten Status - print(f"Buzzer Firmware: v{sys_info['app_version']} [{status_color}{status}\033[0m] (Protokoll v{sys_info['protocol_version']})") - print(f"LittleFS Status: {sys_info['used_kb']:.1f} KB / {sys_info['total_kb']:.1f} KB belegt ({sys_info['percent_used']:.1f}%)") - print("-" * 55) + if not args.no_auto_info: + from core.commands import info + sys_info = info.execute(conn) + + status = sys_info.get("image_status", "UNKNOWN") + status_colors = { + "CONFIRMED": "\033[32m", + "TESTING": "\033[33m", + "PENDING": "\033[36m", + } + status_color = status_colors.get(status, "\033[37m") + + print(f"Buzzer Firmware: v{sys_info['app_version']} [{status_color}{status}\033[0m] (Protokoll v{sys_info['protocol_version']})") + print(f"LittleFS Status: {sys_info['used_kb']:.1f} KB / {sys_info['total_kb']:.1f} KB belegt ({sys_info['percent_used']:.1f}%)") + print("-" * 65) + # 2. Spezifisches Kommando ausführen if args.command == "ls": + from core.commands import ls print(f"Inhalt von '{args.path}':\n") tree = ls.get_file_tree(conn, target_path=args.path, recursive=args.recursive) if not tree: @@ -92,31 +139,66 @@ def main(): else: ls.print_tree(tree, path=args.path ) elif args.command == "put": - put.execute(conn, sources=args.sources, target=args.target) + from core.commands import put + put.execute(conn, sources=args.sources, target=args.target, recursive=args.recursive) + elif args.command == "put_many": + from core.commands import put + recursive = True if not args.recursive else args.recursive + put.execute(conn, sources=args.sources, target=args.target, recursive=recursive) + elif args.command == "fw_put": + from core.commands import fw_put + fw_put.execute(conn, source=args.source) elif args.command == "mkdir": + from core.commands import mkdir mkdir.execute(conn, path=args.path) elif args.command == "rm": + from core.commands import rm rm.execute(conn, path=args.path, recursive=args.recursive) + elif args.command == "stat": + from core.commands import stat + stat.execute(conn, path=args.path) + elif args.command == "mv": + from core.commands import mv + mv.execute(conn, source=args.source, target=args.target) + elif args.command == "pull" or args.command == "get_file": + from core.commands import pull + pull.execute(conn, source=args.source, target=args.target) elif args.command == "confirm": + from core.commands import confirm confirm.execute(conn) elif args.command == "reboot": + from core.commands import reboot reboot.execute(conn) elif args.command == "play": + from core.commands import play play.execute(conn, path=args.path) elif args.command == "check": + from core.commands import check CRC32 = check.execute(conn, path=args.path) if CRC32: - print(f"CRC32 von '{args.path}': 0x{CRC32['crc32']:08x}") + size_bytes = CRC32.get("size_bytes") + if isinstance(size_bytes, int) and size_bytes >= 0: + size_kb = size_bytes / 1024.0 + print(f"CRC32 von '{args.path}': 0x{CRC32['crc32']:08x} (Größe: {size_bytes} B / {size_kb:.1f} KB)") + else: + 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 == "get_tags": + from core.commands import tags + tag_map = tags.get_tags(conn, args.path) + print(tag_map) + elif args.command == "write_tags": + from core.commands import tags + updates = tags.parse_tags_json_input(args.json) + result = tags.write_tags(conn, args.path, updates) + print("Aktuelle Tags:") + print(result) + elif args.command == "remove_tag": + from core.commands import tags + result = tags.remove_tag(conn, args.path, args.key) + print("Aktuelle Tags:") + print(result) 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 index e2c4e2b..0a207a6 100644 --- a/buzzer_tool/config.yaml +++ b/buzzer_tool/config.yaml @@ -1,5 +1,5 @@ # config.yaml serial: - port: "COM17" - baudrate: 250000 - timeout: 10 + port: "/dev/cu.usbmodem83401" + baudrate: 2500000 + timeout: 1 diff --git a/buzzer_tool/config.yaml.example b/buzzer_tool/config.yaml.example index 926d21d..4dc466b 100644 --- a/buzzer_tool/config.yaml.example +++ b/buzzer_tool/config.yaml.example @@ -2,4 +2,6 @@ serial: port: "COM17" baudrate: 250000 - timeout: 20 \ No newline at end of file + timeout: 20 + crc_timeout_min_seconds: 2.0 + crc_timeout_ms_per_100kb: 1.5 \ No newline at end of file diff --git a/buzzer_tool/core/commands/check.py b/buzzer_tool/core/commands/check.py index 5af6a7b..a93419c 100644 --- a/buzzer_tool/core/commands/check.py +++ b/buzzer_tool/core/commands/check.py @@ -1,18 +1,60 @@ # core/commands/check.py from core.connection import BuzzerError -def execute(conn, path: str) -> dict: - """Holt die CRC32 einer datei und gibt sie als Int zurück.""" - lines = conn.send_command("check " + path) - if not lines: - raise BuzzerError("Keine Antwort auf 'check' empfangen.") +def _split_parent_and_name(path: str) -> tuple[str, str]: + normalized = path.rstrip("/") + if not normalized or normalized == "/": + raise BuzzerError("Für CHECK wird ein Dateipfad benötigt.") - parts = lines[0].split() - if len(parts) != 1: - raise BuzzerError(f"Unerwartetes Check-Format: {lines[0]}") - - crc32 = int(parts[0], 16) + idx = normalized.rfind("/") + if idx <= 0: + return "/", normalized + + parent = normalized[:idx] + name = normalized[idx + 1:] + if not name: + raise BuzzerError("Ungültiger Dateipfad für CHECK.") + return parent, name + + +def _lookup_file_size_bytes(conn, path: str) -> int | None: + parent, filename = _split_parent_and_name(path) + lines = conn.list_directory(parent) + + for line in lines: + parts = line.split(",", 2) + if len(parts) != 3: + continue + + entry_type, entry_size, entry_name = parts + if entry_type == "F" and entry_name == filename: + try: + return int(entry_size) + except ValueError: + return None + + return None + + +def _estimate_crc_timeout_seconds(conn, size_bytes: int | None) -> float: + min_timeout = float(getattr(conn, "crc_timeout_min_seconds", 2.0)) + ms_per_100kb = float(getattr(conn, "crc_timeout_ms_per_100kb", 1.5)) + + base = max(float(conn.timeout), min_timeout) + if size_bytes is None or size_bytes <= 0: + return base + + blocks_100kb = size_bytes / (100.0 * 1024.0) + extra = blocks_100kb * (ms_per_100kb / 1000.0) + return base + extra + +def execute(conn, path: str) -> dict: + """Holt die CRC32 nur über Audiodaten und passt Timeout für große Dateien an.""" + size_bytes = _lookup_file_size_bytes(conn, path) + timeout = _estimate_crc_timeout_seconds(conn, size_bytes) + crc32 = conn.check_file_crc(path, timeout=timeout) return { - "crc32": crc32 + "crc32": crc32, + "size_bytes": size_bytes, } \ No newline at end of file diff --git a/buzzer_tool/core/commands/confirm.py b/buzzer_tool/core/commands/confirm.py index d688ff6..28d53ac 100644 --- a/buzzer_tool/core/commands/confirm.py +++ b/buzzer_tool/core/commands/confirm.py @@ -1,10 +1,11 @@ -# core/commands/mkdir.py +# core/commands/confirm.py from core.connection import BuzzerError + def execute(conn): - """Confirmt die aktuelle Firmware.""" + """Bestätigt die aktuell laufende Firmware per Binary-Protokoll.""" try: - conn.send_command(f"confirm") - print(f"✅ Firmware erfolgreich bestätigt.") + conn.confirm_firmware() + print("✅ Firmware erfolgreich bestätigt.") except BuzzerError as e: print(f"❌ Fehler beim Bestätigen der Firmware: {e}") \ No newline at end of file diff --git a/buzzer_tool/core/commands/fw_put.py b/buzzer_tool/core/commands/fw_put.py new file mode 100644 index 0000000..5e0a5ed --- /dev/null +++ b/buzzer_tool/core/commands/fw_put.py @@ -0,0 +1,57 @@ +import os +import time +import sys + + +def _estimate_fw_timeout_seconds(conn, total_size: int) -> float: + base = float(getattr(conn, "timeout", 5.0)) + erase_budget = 8.0 + stream_and_write_budget = total_size / (25.0 * 1024.0) + return max(base, erase_budget + stream_and_write_budget) + + +def execute(conn, source: str): + if not os.path.isfile(source): + raise FileNotFoundError(f"Firmware-Datei nicht gefunden: {source}") + + with open(source, "rb") as f: + data = f.read() + + total_size = len(data) + if total_size == 0: + raise ValueError("Firmware-Datei ist leer.") + + print(f"Sende 🧩 Firmware ({total_size / 1024:.1f} KB) -> secondary slot") + fw_timeout = _estimate_fw_timeout_seconds(conn, total_size) + print(f" Timeout fw_put: {fw_timeout:.1f}s") + print(" Phase 1/3: Lösche secondary slot und initialisiere Flash...") + + start_time = time.monotonic() + last_ui_update = start_time + transfer_started = False + + def progress_handler(chunk_len, sent_file, total_file): + nonlocal last_ui_update, transfer_started + if not transfer_started: + transfer_started = True + print(" Phase 2/3: Übertrage Firmware...") + now = time.monotonic() + if now - last_ui_update < 0.2 and sent_file < total_file: + return + last_ui_update = now + + elapsed = now - start_time + speed = (sent_file / 1024.0) / elapsed if elapsed > 0 else 0.0 + perc = (sent_file / total_file) * 100.0 if total_file > 0 else 100.0 + eta_sec = ((total_file - sent_file) / (sent_file / elapsed)) if sent_file > 0 and elapsed > 0 else 0.0 + eta_str = f"{int(eta_sec // 60):02d}:{int(eta_sec % 60):02d}" + + sys.stdout.write( + f"\r \033[90mProg: {perc:3.0f}% | {speed:6.1f} KB/s | ETA: {eta_str}\033[0m" + ) + sys.stdout.flush() + + crc32 = conn.fw_put_data(data, timeout=fw_timeout, progress_callback=progress_handler) + print("\n Phase 3/3: Finalisiere und warte auf Geräte-ACK...") + print(f"\r \033[32mFertig: Firmware übertragen (CRC32: 0x{crc32:08x}).{' ' * 16}\033[0m") + print("ℹ️ Nächste Schritte: reboot -> testen -> confirm") diff --git a/buzzer_tool/core/commands/get_tag.py b/buzzer_tool/core/commands/get_tag.py deleted file mode 100644 index 1b788ce..0000000 --- a/buzzer_tool/core/commands/get_tag.py +++ /dev/null @@ -1,56 +0,0 @@ -# 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/commands/info.py b/buzzer_tool/core/commands/info.py index f925d6f..993edd8 100644 --- a/buzzer_tool/core/commands/info.py +++ b/buzzer_tool/core/commands/info.py @@ -3,24 +3,23 @@ from core.connection import BuzzerError def execute(conn) -> dict: """Holt die Systeminformationen und gibt sie als strukturiertes Dictionary zurück.""" - lines = conn.send_command("info") - if not lines: - raise BuzzerError("Keine Antwort auf 'info' empfangen.") + protocol_version = conn.get_protocol_version() + if protocol_version != 1: + raise BuzzerError(f"Inkompatibles Protokoll: Gerät nutzt v{protocol_version}, Host erwartet v1.") - parts = lines[0].split(';') - # Auf 6 Parameter aktualisiert - if len(parts) != 6: - raise BuzzerError(f"Unerwartetes Info-Format: {lines[0]}") + status_code, app_version = conn.get_firmware_status() + flash = conn.get_flash_status() - protocol_version = int(parts[0]) - if protocol_version != 2: - raise BuzzerError(f"Inkompatibles Protokoll: Gerät nutzt v{protocol_version}, Host erwartet v2.") + f_frsize = flash["block_size"] + f_blocks = flash["total_blocks"] + f_bfree = flash["free_blocks"] - app_version = parts[1] - f_frsize = int(parts[2]) - f_blocks = int(parts[3]) - f_bfree = int(parts[4]) - image_status = parts[5].strip() # CONFIRMED oder UNCONFIRMED + status_map = { + 1: "CONFIRMED", + 2: "TESTING", + 3: "PENDING", + } + image_status = status_map.get(status_code, f"UNKNOWN({status_code})") total_kb = (f_blocks * f_frsize) / 1024 free_kb = (f_bfree * f_frsize) / 1024 diff --git a/buzzer_tool/core/commands/ls.py b/buzzer_tool/core/commands/ls.py index 771495f..cc33d8d 100644 --- a/buzzer_tool/core/commands/ls.py +++ b/buzzer_tool/core/commands/ls.py @@ -11,7 +11,7 @@ def get_file_tree(conn, target_path="/", recursive=False) -> list: cmd_path = target_path.rstrip('/') if target_path != '/' else '/' try: - lines = conn.send_command(f"ls {cmd_path}") + lines = conn.list_directory(cmd_path) except BuzzerError as e: return [{"type": "E", "name": f"Fehler beim Lesen: {e}", "path": target_path}] diff --git a/buzzer_tool/core/commands/mkdir.py b/buzzer_tool/core/commands/mkdir.py index 8608a6b..4873404 100644 --- a/buzzer_tool/core/commands/mkdir.py +++ b/buzzer_tool/core/commands/mkdir.py @@ -4,7 +4,7 @@ from core.connection import BuzzerError def execute(conn, path: str): """Erstellt ein Verzeichnis auf dem Controller.""" try: - conn.send_command(f"mkdir {path}") + conn.mkdir(path) print(f"📁 Verzeichnis '{path}' erfolgreich erstellt.") except BuzzerError as e: print(f"❌ Fehler beim Erstellen von '{path}': {e}") \ No newline at end of file diff --git a/buzzer_tool/core/commands/mv.py b/buzzer_tool/core/commands/mv.py new file mode 100644 index 0000000..4e2b300 --- /dev/null +++ b/buzzer_tool/core/commands/mv.py @@ -0,0 +1,10 @@ +from core.connection import BuzzerError + + +def execute(conn, source: str, target: str): + try: + conn.rename(source, target) + print(f"✅ Umbenannt/Verschoben: '{source}' -> '{target}'") + except BuzzerError as e: + print(f"❌ Fehler beim Umbenennen/Verschieben: {e}") + raise diff --git a/buzzer_tool/core/commands/pull.py b/buzzer_tool/core/commands/pull.py new file mode 100644 index 0000000..7cf0e82 --- /dev/null +++ b/buzzer_tool/core/commands/pull.py @@ -0,0 +1,62 @@ +import os +import posixpath +import time + + +def _resolve_local_target(remote_path: str, target: str | None) -> str: + if target: + return target + + basename = posixpath.basename(remote_path.rstrip("/")) + if not basename: + raise ValueError("Kann keinen lokalen Dateinamen aus dem Remote-Pfad ableiten. Bitte Zielpfad angeben.") + return basename + + +def execute(conn, source: str, target: str | None = None): + local_path = _resolve_local_target(source, target) + + os.makedirs(os.path.dirname(local_path) or ".", exist_ok=True) + + last_print = 0.0 + start_time = time.monotonic() + + def _progress(_chunk_len: int, received: int, expected: int | None): + nonlocal last_print + now = time.monotonic() + if now - last_print < 0.2: + return + last_print = now + + elapsed = max(now - start_time, 1e-6) + speed_kb_s = (received / 1024.0) / elapsed + + if expected is not None and expected > 0: + percent = (received * 100.0) / expected + remaining = max(expected - received, 0) + eta_sec = (remaining / 1024.0) / speed_kb_s if speed_kb_s > 0 else 0.0 + eta_str = f"{int(eta_sec // 60):02d}:{int(eta_sec % 60):02d}" + print( + f"\r⬇️ {received}/{expected} B ({percent:5.1f}%) | {speed_kb_s:6.1f} KB/s | ETA {eta_str}", + end="", + flush=True, + ) + else: + print(f"\r⬇️ {received} B | {speed_kb_s:6.1f} KB/s", end="", flush=True) + + data = conn.get_file_data(source, progress_callback=_progress) + + if len(data) > 0: + print() + + with open(local_path, "wb") as f: + f.write(data) + + total_duration = max(time.monotonic() - start_time, 1e-6) + avg_speed_kb_s = (len(data) / 1024.0) / total_duration + print(f"✅ Heruntergeladen: '{source}' -> '{local_path}' ({len(data)} B, {avg_speed_kb_s:.1f} KB/s)") + return { + "source": source, + "target": local_path, + "size": len(data), + } diff --git a/buzzer_tool/core/commands/put.py b/buzzer_tool/core/commands/put.py index 8d4e6d4..8c171d8 100644 --- a/buzzer_tool/core/commands/put.py +++ b/buzzer_tool/core/commands/put.py @@ -1,61 +1,202 @@ -import os -import zlib import glob -import time +import os import sys +import time + from core.connection import BuzzerError -def get_file_crc32(filepath: str) -> int: - """Berechnet die IEEE CRC32-Prüfsumme einer Datei in Chunks.""" - crc = 0 - with open(filepath, 'rb') as f: - while chunk := f.read(4096): - crc = zlib.crc32(chunk, crc) - return crc & 0xFFFFFFFF +TAG_MAGIC = b"TAG!" +TAG_FOOTER_LEN = 7 +TAG_VERSION_V1 = 0x01 +TAG_TYPE_CRC32 = 0x10 -def execute(conn, sources: list, target: str): - # 1. Globbing auflösen - resolved_files = [f for src in sources for f in glob.glob(src) if os.path.isfile(f)] - - if not resolved_files: + +def _split_audio_and_tag_blob(filepath: str) -> tuple[bytes, bytes | None]: + with open(filepath, "rb") as f: + data = f.read() + + if len(data) < TAG_FOOTER_LEN: + return data, None + + if data[-4:] != TAG_MAGIC: + return data, None + + tag_total_len = int.from_bytes(data[-6:-4], byteorder="little", signed=False) + tag_version = data[-7] + if tag_version != TAG_VERSION_V1: + return data, None + + if tag_total_len < TAG_FOOTER_LEN or tag_total_len > len(data): + return data, None + + audio_end = len(data) - tag_total_len + tag_payload_len = tag_total_len - TAG_FOOTER_LEN + tag_payload = data[audio_end:audio_end + tag_payload_len] + audio_data = data[:audio_end] + return audio_data, tag_payload + + +def _upsert_crc32_tag(tag_blob: bytes | None, crc32: int) -> tuple[bytes, bool]: + crc_payload = int(crc32).to_bytes(4, byteorder="little", signed=False) + crc_tlv = bytes([TAG_TYPE_CRC32, 0x04, 0x00]) + crc_payload + + if not tag_blob: + return crc_tlv, True + + pos = 0 + out = bytearray() + found_crc = False + + while pos < len(tag_blob): + if pos + 3 > len(tag_blob): + return crc_tlv, True + + tag_type = tag_blob[pos] + tag_len = tag_blob[pos + 1] | (tag_blob[pos + 2] << 8) + header = tag_blob[pos:pos + 3] + pos += 3 + + if pos + tag_len > len(tag_blob): + return crc_tlv, True + + value = tag_blob[pos:pos + tag_len] + pos += tag_len + + if tag_type == TAG_TYPE_CRC32: + if not found_crc: + out.extend(crc_tlv) + found_crc = True + continue + + out.extend(header) + out.extend(value) + + if not found_crc: + out.extend(crc_tlv) + + return bytes(out), True + + +def _collect_source_files(sources: list[str], recursive: bool) -> list[dict]: + entries = [] + + for source in sources: + matches = glob.glob(source) + if not matches: + print(f"⚠️ Keine Treffer für Quelle: {source}") + continue + + for match in matches: + if os.path.isfile(match): + entries.append({"local": match, "relative": os.path.basename(match)}) + continue + + if os.path.isdir(match): + if recursive: + for root, _, files in os.walk(match): + for name in sorted(files): + local_path = os.path.join(root, name) + rel = os.path.relpath(local_path, match) + entries.append({"local": local_path, "relative": rel.replace("\\", "/")}) + else: + for name in sorted(os.listdir(match)): + local_path = os.path.join(match, name) + if os.path.isfile(local_path): + entries.append({"local": local_path, "relative": name}) + + return entries + + +def _remote_parent(path: str) -> str: + idx = path.rfind("/") + if idx <= 0: + return "/" + return path[:idx] + + +def _ensure_remote_dir(conn, remote_dir: str) -> None: + if not remote_dir or remote_dir == "/": + return + + current = "" + for part in [p for p in remote_dir.split("/") if p]: + current = f"{current}/{part}" + try: + conn.mkdir(current) + except BuzzerError as e: + msg = str(e) + if "0x11" in msg or "existiert bereits" in msg: + continue + raise + + +def _build_upload_plan(entries: list[dict], target: str) -> list[dict]: + if not entries: + return [] + + needs_dir_semantics = target.endswith("/") or len(entries) > 1 or any("/" in e["relative"] for e in entries) + if not needs_dir_semantics: + return [{"local": entries[0]["local"], "remote": target}] + + base = target.rstrip("/") + if not base: + base = "/" + + plan = [] + for entry in entries: + rel = entry["relative"].lstrip("/") + if base == "/": + remote = f"/{rel}" + else: + remote = f"{base}/{rel}" + plan.append({"local": entry["local"], "remote": remote}) + + return plan + + +def execute(conn, sources: list[str], target: str, recursive: bool = False): + uploads = _build_upload_plan(_collect_source_files(sources, recursive=recursive), target) + if not uploads: print("Keine gültigen Dateien gefunden.") return - total_size_all = sum(os.path.getsize(f) for f in resolved_files) + total_size_all = sum(os.path.getsize(item["local"]) for item in uploads) sent_all = 0 start_time_all = time.monotonic() - is_target_dir = target.endswith('/') + last_ui_update = start_time_all - for filepath in resolved_files: - filename = os.path.basename(filepath) - filesize = os.path.getsize(filepath) - crc32 = get_file_crc32(filepath) - dest_path = f"{target}{filename}" if is_target_dir else target - - print(f"Sende 📄 {filename} ({filesize/1024:.1f} KB) -> {dest_path}") - + for item in uploads: + local_path = item["local"] + remote_path = item["remote"] + filename = os.path.basename(local_path) + + audio_data, tag_blob = _split_audio_and_tag_blob(local_path) + audio_size = len(audio_data) + + _ensure_remote_dir(conn, _remote_parent(remote_path)) + + print(f"Sende 📄 {filename} ({audio_size / 1024:.1f} KB Audio) -> {remote_path}") start_time_file = time.monotonic() - sent_file = 0 - def progress_handler(chunk_len): - nonlocal sent_file, sent_all - sent_file += chunk_len + def progress_handler(chunk_len, sent_file, total_file): + nonlocal sent_all, last_ui_update sent_all += chunk_len - - elapsed = time.monotonic() - start_time_file - speed = (sent_file / 1024) / elapsed if elapsed > 0 else 0 - - # Prozentberechnungen - perc_file = (sent_file / filesize) * 100 - perc_all = (sent_all / total_size_all) * 100 - - # ETA (Basierend auf Gesamtgeschwindigkeit) - elapsed_all = time.monotonic() - start_time_all - avg_speed_all = sent_all / elapsed_all if elapsed_all > 0 else 0 - eta_sec = (total_size_all - sent_all) / avg_speed_all if avg_speed_all > 0 else 0 + + now = time.monotonic() + if now - last_ui_update < 0.2 and sent_file < total_file: + return + last_ui_update = now + + elapsed = now - start_time_file + speed = (sent_file / 1024.0) / elapsed if elapsed > 0 else 0.0 + perc_file = (sent_file / total_file) * 100.0 if total_file > 0 else 100.0 + perc_all = (sent_all / total_size_all) * 100.0 if total_size_all > 0 else 100.0 + + elapsed_all = now - start_time_all + avg_speed_all = sent_all / elapsed_all if elapsed_all > 0 else 0.0 + eta_sec = (total_size_all - sent_all) / avg_speed_all if avg_speed_all > 0 else 0.0 eta_str = f"{int(eta_sec // 60):02d}:{int(eta_sec % 60):02d}" - # Ausgabezeile (\r überschreibt die aktuelle Zeile) sys.stdout.write( f"\r \033[90mProg: {perc_file:3.0f}% | Gesamt: {perc_all:3.0f}% | " f"{speed:6.1f} KB/s | ETA: {eta_str}\033[0m" @@ -63,20 +204,17 @@ def execute(conn, sources: list, target: str): sys.stdout.flush() try: - cmd = f"put {dest_path};{filesize};{crc32}\n" - conn.serial.write(cmd.encode('utf-8')) - conn.serial.flush() - - # Binärtransfer mit unserem Handler - conn.send_binary(filepath, progress_callback=progress_handler) - - # Zeile nach Erfolg abschließen - print(f"\r \033[32mFertig: {filename} übertragen. \033[0m") + audio_crc32 = conn.put_file_data(remote_path, audio_data, progress_callback=progress_handler) + + rewritten_blob, _ = _upsert_crc32_tag(tag_blob, audio_crc32) + conn.set_tag_blob(remote_path, rewritten_blob) + tag_note = " (CRC32-Tag gesetzt)" + + print(f"\r \033[32mFertig: {filename} übertragen{tag_note}.{' ' * 20}\033[0m") except Exception as e: print(f"\n ❌ \033[31mFehler: {e}\033[0m") total_duration = time.monotonic() - start_time_all - total_kb = total_size_all / 1024 - avg_speed = total_kb / total_duration if total_duration > 0 else 0 - + total_kb = total_size_all / 1024.0 + avg_speed = total_kb / total_duration if total_duration > 0 else 0.0 print(f"\nÜbertragung abgeschlossen: {total_kb:.1f} KB in {total_duration:.1f}s ({avg_speed:.1f} KB/s)") \ No newline at end of file diff --git a/buzzer_tool/core/commands/reboot.py b/buzzer_tool/core/commands/reboot.py index fca6a49..b03e5de 100644 --- a/buzzer_tool/core/commands/reboot.py +++ b/buzzer_tool/core/commands/reboot.py @@ -1,10 +1,11 @@ -# core/commands/mkdir.py +# core/commands/reboot.py from core.connection import BuzzerError + def execute(conn): - """Startet den Buzzer neu.""" + """Startet den Buzzer per Binary-Protokoll neu.""" try: - conn.send_command(f"reboot") - print(f"🔄 Buzzer wird neu gestartet.") + conn.reboot_device() + print("🔄 Buzzer wird neu gestartet.") except BuzzerError as e: print(f"❌ Fehler beim Neustarten des Buzzers: {e}") \ No newline at end of file diff --git a/buzzer_tool/core/commands/rm.py b/buzzer_tool/core/commands/rm.py index fb69872..17c9089 100644 --- a/buzzer_tool/core/commands/rm.py +++ b/buzzer_tool/core/commands/rm.py @@ -17,7 +17,7 @@ def _delete_recursive(conn, nodes): def _try_rm(conn, path, is_dir=False): icon = "📁" if is_dir else "📄" try: - conn.send_command(f"rm {path}") + conn.rm(path) print(f" 🗑️ {icon} Gelöscht: {path}") except BuzzerError as e: print(f" ❌ Fehler bei {path}: {e}") @@ -53,21 +53,16 @@ def execute(conn, path: str, recursive: bool = False): # 2. Rekursives Löschen (-r) if recursive: - print(f"Sammle Dateibaum für rekursives Löschen von '{path}'...") - tree = get_file_tree(conn, target_path=path, recursive=True) - - if len(tree) == 1 and tree[0].get("type") == "E": - print(f"❌ Pfad nicht gefunden oder Fehler: {tree[0]['name']}") - return - - if not tree: - print(f"Ordner '{path}' ist bereits leer.") - else: - _delete_recursive(conn, tree) - - # 3. Standard-Löschen (Einzeldatei oder am Ende der Rekursion der leere Ordner) + try: + conn.rm_recursive(path) + print(f"🗑️ '{path}' rekursiv gelöscht.") + except BuzzerError as e: + print(f"❌ Fehler beim rekursiven Löschen von '{path}': {e}") + return + + # 3. Standard-Löschen (Einzeldatei oder leeres Verzeichnis) try: - conn.send_command(f"rm {path}") + conn.rm(path) print(f"🗑️ '{path}' erfolgreich gelöscht.") except BuzzerError as e: print(f"❌ Fehler beim Löschen von '{path}': {e}") \ No newline at end of file diff --git a/buzzer_tool/core/commands/stat.py b/buzzer_tool/core/commands/stat.py new file mode 100644 index 0000000..fb5beb5 --- /dev/null +++ b/buzzer_tool/core/commands/stat.py @@ -0,0 +1,5 @@ +def execute(conn, path: str): + info = conn.stat(path) + entry_type = "Ordner" if info["type"] == "D" else "Datei" + print(f"{path}: {entry_type}, Größe: {info['size']} B") + return info diff --git a/buzzer_tool/core/commands/tags.py b/buzzer_tool/core/commands/tags.py new file mode 100644 index 0000000..f4d654e --- /dev/null +++ b/buzzer_tool/core/commands/tags.py @@ -0,0 +1,140 @@ +import json +from core.connection import BuzzerError + +TAG_TYPE_TO_KEY = { + 0x00: "description", + 0x01: "author", + 0x10: "crc32", + 0x20: "fileformat", +} + +KEY_TO_TAG_TYPE = {v: k for k, v in TAG_TYPE_TO_KEY.items()} +VALID_TAG_KEYS = frozenset(KEY_TO_TAG_TYPE.keys()) + +def _u16le(value: int) -> bytes: + return bytes((value & 0xFF, (value >> 8) & 0xFF)) + + +def _parse_tlv(blob: bytes) -> dict: + tags = {} + pos = 0 + + while pos < len(blob): + if pos + 3 > len(blob): + raise BuzzerError("Ungültiger Tag-Blob: TLV-Header abgeschnitten") + + tag_type = blob[pos] + tag_len = blob[pos + 1] | (blob[pos + 2] << 8) + pos += 3 + + if pos + tag_len > len(blob): + raise BuzzerError("Ungültiger Tag-Blob: TLV-Wert abgeschnitten") + + value = blob[pos:pos + tag_len] + pos += tag_len + + key = TAG_TYPE_TO_KEY.get(tag_type, f"unknown_0x{tag_type:02x}") + + if tag_type in (0x00, 0x01): + tags[key] = value.decode("utf-8", errors="replace") + elif tag_type == 0x10: + if tag_len != 4: + raise BuzzerError("Ungültiger crc32-Tag: len muss 4 sein") + crc32 = int.from_bytes(value, byteorder="little", signed=False) + tags[key] = f"0x{crc32:08x}" + elif tag_type == 0x20: + if tag_len != 5: + raise BuzzerError("Ungültiger fileformat-Tag: len muss 5 sein") + bits = value[0] + samplerate = int.from_bytes(value[1:5], byteorder="little", signed=False) + tags[key] = {"bits_per_sample": bits, "sample_rate": samplerate} + else: + tags[key] = value.hex() + + return tags + + +def _build_tlv(tags: dict) -> bytes: + entries = [] + + if "description" in tags and tags["description"] is not None: + data = str(tags["description"]).encode("utf-8") + entries.append(bytes([KEY_TO_TAG_TYPE["description"]]) + _u16le(len(data)) + data) + + if "author" in tags and tags["author"] is not None: + data = str(tags["author"]).encode("utf-8") + entries.append(bytes([KEY_TO_TAG_TYPE["author"]]) + _u16le(len(data)) + data) + + if "crc32" in tags and tags["crc32"] is not None: + crc_val = tags["crc32"] + if isinstance(crc_val, str): + crc_val = int(crc_val, 16) if crc_val.lower().startswith("0x") else int(crc_val) + data = int(crc_val).to_bytes(4, byteorder="little", signed=False) + entries.append(bytes([KEY_TO_TAG_TYPE["crc32"]]) + _u16le(4) + data) + + if "fileformat" in tags and tags["fileformat"] is not None: + ff = tags["fileformat"] + if not isinstance(ff, dict): + raise BuzzerError("fileformat muss ein Objekt sein: {bits_per_sample, sample_rate}") + bits = int(ff.get("bits_per_sample", 16)) + samplerate = int(ff.get("sample_rate", 16000)) + data = bytes([bits]) + samplerate.to_bytes(4, byteorder="little", signed=False) + entries.append(bytes([KEY_TO_TAG_TYPE["fileformat"]]) + _u16le(5) + data) + + return b"".join(entries) + + +def get_tags(conn, path: str) -> dict: + blob = conn.get_tag_blob(path) + if not blob: + return {} + return _parse_tlv(blob) + + +def write_tags(conn, path: str, tags_update: dict) -> dict: + unknown_keys = [k for k in tags_update.keys() if k not in VALID_TAG_KEYS] + if unknown_keys: + unknown_str = ", ".join(sorted(str(k) for k in unknown_keys)) + valid_str = ", ".join(sorted(VALID_TAG_KEYS)) + raise BuzzerError( + f"Unbekannter Tag-Key in write_tags: {unknown_str}. Erlaubte Keys: {valid_str}" + ) + + current = get_tags(conn, path) + merged = dict(current) + + for key, value in tags_update.items(): + if value is None: + merged.pop(key, None) + else: + merged[key] = value + + blob = _build_tlv(merged) + conn.set_tag_blob(path, blob) + return merged + + +def remove_tag(conn, path: str, key: str) -> dict: + current = get_tags(conn, path) + current.pop(key, None) + blob = _build_tlv(current) + conn.set_tag_blob(path, blob) + return current + + +def parse_tags_json_input(raw: str) -> dict: + text = raw.strip() + if text.startswith("@"): + file_path = text[1:] + with open(file_path, "r", encoding="utf-8") as f: + text = f.read() + + try: + data = json.loads(text) + except json.JSONDecodeError as e: + raise BuzzerError(f"Ungültiges JSON für write_tags: {e}") + + if not isinstance(data, dict): + raise BuzzerError("write_tags erwartet ein JSON-Objekt.") + + return data diff --git a/buzzer_tool/core/config.py b/buzzer_tool/core/config.py index cf3483c..606d1f6 100644 --- a/buzzer_tool/core/config.py +++ b/buzzer_tool/core/config.py @@ -6,7 +6,9 @@ import yaml DEFAULT_CONFIG = { "port": None, "baudrate": 115200, - "timeout": 5.0 + "timeout": 5.0, + "crc_timeout_min_seconds": 2.0, + "crc_timeout_ms_per_100kb": 1.5, } def load_config(cli_args=None): @@ -26,6 +28,12 @@ def load_config(cli_args=None): config["port"] = yaml_data["serial"].get("port", config["port"]) config["baudrate"] = yaml_data["serial"].get("baudrate", config["baudrate"]) config["timeout"] = yaml_data["serial"].get("timeout", config["timeout"]) + config["crc_timeout_min_seconds"] = yaml_data["serial"].get( + "crc_timeout_min_seconds", config["crc_timeout_min_seconds"] + ) + config["crc_timeout_ms_per_100kb"] = yaml_data["serial"].get( + "crc_timeout_ms_per_100kb", config["crc_timeout_ms_per_100kb"] + ) except Exception as e: print(f"Fehler beim Laden der Konfigurationsdatei {yaml_path}: {e}") diff --git a/buzzer_tool/core/connection.py b/buzzer_tool/core/connection.py index 3ffc416..4adfc16 100644 --- a/buzzer_tool/core/connection.py +++ b/buzzer_tool/core/connection.py @@ -1,7 +1,8 @@ # core/connection.py -import serial import time import os +import struct +import binascii PROTOCOL_ERROR_MESSAGES = { 0x01: "Ungültiger Befehl.", @@ -23,6 +24,40 @@ PROTOCOL_ERROR_MESSAGES = { 0x32: "Interner Gerätefehler.", } +SYNC = b"BUZZ" +HEADER_SIZE = 14 +DEFAULT_MAX_PATH_LEN = 32 +FRAME_REQ = 0x01 +FRAME_RESP_ACK = 0x10 +FRAME_RESP_DATA = 0x11 +FRAME_RESP_STREAM_START = 0x12 +FRAME_RESP_STREAM_CHUNK = 0x13 +FRAME_RESP_STREAM_END = 0x14 +FRAME_RESP_ERROR = 0x7F +POLL_SLEEP_SECONDS = 0.002 + +CMD_GET_PROTOCOL_VERSION = 0x00 +CMD_GET_FIRMWARE_STATUS = 0x01 +CMD_GET_FLASH_STATUS = 0x02 +CMD_CONFIRM_FIRMWARE = 0x03 +CMD_REBOOT = 0x04 +CMD_LIST_DIR = 0x10 +CMD_CHECK_FILE_CRC = 0x11 +CMD_MKDIR = 0x12 +CMD_RM = 0x13 +CMD_PUT_FILE_START = 0x14 +CMD_PUT_FILE_CHUNK = 0x15 +CMD_PUT_FILE_END = 0x16 +CMD_PUT_FW_START = 0x17 +CMD_STAT = 0x18 +CMD_RENAME = 0x19 +CMD_RM_R = 0x1A +CMD_GET_FILE = 0x1B +CMD_GET_TAG_BLOB = 0x20 +CMD_SET_TAG_BLOB_START = 0x21 +CMD_SET_TAG_BLOB_CHUNK = 0x22 +CMD_SET_TAG_BLOB_END = 0x23 + class BuzzerError(Exception): pass @@ -31,11 +66,20 @@ class BuzzerConnection: self.port = config.get("port") self.baudrate = config.get("baudrate", 115200) self.timeout = config.get("timeout", 5.0) + self.crc_timeout_min_seconds = float(config.get("crc_timeout_min_seconds", 2.0)) + self.crc_timeout_ms_per_100kb = float(config.get("crc_timeout_ms_per_100kb", 1.5)) self.serial = None + self._sequence = 0 + self._max_path_len = DEFAULT_MAX_PATH_LEN def __enter__(self): if not self.port: raise ValueError("Kein serieller Port konfiguriert.") + + try: + import serial + except ImportError as e: + raise BuzzerError("PySerial ist nicht installiert. Bitte 'pip install -r requirements.txt' ausführen.") from e # write_timeout verhindert endloses Blockieren auf inaktiven Ports self.serial = serial.Serial( @@ -61,6 +105,670 @@ class BuzzerConnection: message = PROTOCOL_ERROR_MESSAGES.get(code, "Unbekannter Fehlercode vom Gerät.") return f"Controller-Fehler {code} (0x{code:02X}): {message}" + def _parse_controller_error_code(self, code: int) -> str: + message = PROTOCOL_ERROR_MESSAGES.get(code, "Unbekannter Fehlercode vom Gerät.") + return f"Controller-Fehler {code} (0x{code:02X}): {message}" + + def _raise_error_from_payload(self, payload: bytes) -> None: + error_code = payload[0] if len(payload) >= 1 else 0x32 + detail = "" + if len(payload) >= 2: + detail_len = payload[1] + if detail_len > 0 and len(payload) >= 2 + detail_len: + detail = payload[2:2 + detail_len].decode("utf-8", errors="replace") + + msg = self._parse_controller_error_code(error_code) + if detail: + msg = f"{msg} Detail: {detail}" + raise BuzzerError(msg) + + def _next_sequence(self) -> int: + seq = self._sequence + self._sequence = (self._sequence + 1) & 0xFFFF + return seq + + def _crc16_ccitt_false(self, data: bytes) -> int: + crc = 0xFFFF + for b in data: + crc ^= b + for _ in range(8): + if crc & 0x0001: + crc = ((crc >> 1) ^ 0x8408) & 0xFFFF + else: + crc = (crc >> 1) & 0xFFFF + return crc + + def _read_exact(self, size: int, timeout: float) -> bytes: + deadline = time.monotonic() + timeout + chunks = bytearray() + while len(chunks) < size: + remaining_time = deadline - time.monotonic() + if remaining_time <= 0: + raise TimeoutError(f"Lese-Timeout beim Warten auf {size} Bytes.") + old_timeout = self.serial.timeout + self.serial.timeout = min(remaining_time, 0.25) + try: + chunk = self.serial.read(size - len(chunks)) + finally: + self.serial.timeout = old_timeout + if chunk: + chunks.extend(chunk) + return bytes(chunks) + + def _build_frame(self, frame_type: int, command_id: int, sequence: int, payload: bytes) -> bytes: + payload = payload or b"" + payload_len = len(payload) + header_no_sync_crc = struct.pack(" None: + try: + self.serial.write(frame) + self.serial.flush() + except Exception as e: + if e.__class__.__name__ == "SerialTimeoutException": + raise TimeoutError(f"Schreib-Timeout am Port {self.port}. Ist das Gerät blockiert?") from e + raise + + def _send_binary_frame_no_wait(self, command_id: int, payload: bytes = b"") -> int: + if self.serial is None: + raise BuzzerError("Serielle Verbindung ist nicht geöffnet.") + sequence = self._next_sequence() + frame = self._build_frame(FRAME_REQ, command_id, sequence, payload) + self._write_frame(frame) + return sequence + + def _read_frame(self, timeout: float = None) -> dict: + eff_timeout = timeout if timeout is not None else self.timeout + deadline = time.monotonic() + eff_timeout + + sync_idx = 0 + while sync_idx < len(SYNC): + remaining = deadline - time.monotonic() + if remaining <= 0: + raise TimeoutError("Timeout beim Warten auf Sync 'BUZZ'.") + b = self._read_exact(1, remaining) + if b[0] == SYNC[sync_idx]: + sync_idx += 1 + else: + sync_idx = 1 if b[0] == SYNC[0] else 0 + + remaining = deadline - time.monotonic() + if remaining <= 0: + raise TimeoutError("Timeout beim Lesen des Frame-Headers.") + rest_header = self._read_exact(HEADER_SIZE - len(SYNC), remaining) + frame_type, command_id, sequence, payload_len, rx_header_crc = struct.unpack(" bytes: + if self.serial is None: + raise BuzzerError("Serielle Verbindung ist nicht geöffnet.") + + eff_timeout = timeout if timeout is not None else self.timeout + self.serial.reset_input_buffer() + + sequence = self._next_sequence() + frame = self._build_frame(FRAME_REQ, command_id, sequence, payload) + + self._write_frame(frame) + + response = self._read_frame(timeout=eff_timeout) + + if response["sequence"] != sequence: + raise BuzzerError( + f"Antwort-Sequenz passt nicht: erwartet {sequence}, erhalten {response['sequence']}" + ) + + if response["command_id"] != command_id: + raise BuzzerError( + f"Antwort-Kommando passt nicht: erwartet 0x{command_id:02X}, erhalten 0x{response['command_id']:02X}" + ) + + if response["frame_type"] == FRAME_RESP_ERROR: + self._raise_error_from_payload(response["payload"]) + + if response["frame_type"] not in (FRAME_RESP_ACK, FRAME_RESP_DATA): + raise BuzzerError(f"Unerwarteter Response-Typ: 0x{response['frame_type']:02X}") + + return response["payload"] + + def get_protocol_version(self, timeout: float = None) -> int: + payload = self.send_binary_command(CMD_GET_PROTOCOL_VERSION, b"", timeout=timeout) + if len(payload) != 2: + raise BuzzerError(f"Ungültige Antwortlänge für GET_PROTOCOL_VERSION: {len(payload)}") + return struct.unpack(" tuple[int, str]: + payload = self.send_binary_command(CMD_GET_FIRMWARE_STATUS, b"", timeout=timeout) + if len(payload) < 2: + raise BuzzerError("Ungültige Antwort für GET_FIRMWARE_STATUS: zu kurz") + + status = payload[0] + version_len = payload[1] + if len(payload) != 2 + version_len: + raise BuzzerError( + f"Ungültige Antwort für GET_FIRMWARE_STATUS: erwartete Länge {2 + version_len}, erhalten {len(payload)}" + ) + version = payload[2:2 + version_len].decode("utf-8", errors="replace") + return status, version + + def get_flash_status(self, timeout: float = None) -> dict: + payload = self.send_binary_command(CMD_GET_FLASH_STATUS, b"", timeout=timeout) + if len(payload) != 16: + raise BuzzerError(f"Ungültige Antwortlänge für GET_FLASH_STATUS: {len(payload)}") + + block_size, total_blocks, free_blocks, path_max_len = struct.unpack(" 0: + self._max_path_len = int(path_max_len) + + return { + "block_size": block_size, + "total_blocks": total_blocks, + "free_blocks": free_blocks, + "path_max_len": path_max_len, + } + + def confirm_firmware(self, timeout: float = None) -> None: + payload = self.send_binary_command(CMD_CONFIRM_FIRMWARE, b"", timeout=timeout) + if len(payload) != 0: + raise BuzzerError(f"Unerwartete Payload für CONFIRM_FIRMWARE: {len(payload)} Bytes") + + def reboot_device(self, timeout: float = None) -> None: + payload = self.send_binary_command(CMD_REBOOT, b"", timeout=timeout) + if len(payload) != 0: + raise BuzzerError(f"Unerwartete Payload für REBOOT: {len(payload)} Bytes") + + def _encode_path_payload(self, path: str) -> bytes: + path_bytes = path.encode("utf-8") + if len(path_bytes) == 0: + raise BuzzerError("Pfad darf nicht leer sein.") + max_path_len = min(self._max_path_len, 255) + if len(path_bytes) > max_path_len: + raise BuzzerError(f"Pfad ist zu lang (max. {max_path_len} Bytes).") + return bytes([len(path_bytes)]) + path_bytes + + def list_directory(self, path: str, timeout: float = None) -> list[str]: + if self.serial is None: + raise BuzzerError("Serielle Verbindung ist nicht geöffnet.") + + eff_timeout = timeout if timeout is not None else self.timeout + self.serial.reset_input_buffer() + + sequence = self._next_sequence() + frame = self._build_frame(FRAME_REQ, CMD_LIST_DIR, sequence, self._encode_path_payload(path)) + + try: + self.serial.write(frame) + self.serial.flush() + except Exception as e: + if e.__class__.__name__ == "SerialTimeoutException": + raise TimeoutError(f"Schreib-Timeout am Port {self.port}. Ist das Gerät blockiert?") from e + raise + + lines = [] + stream_started = False + + while True: + response = self._read_frame(timeout=eff_timeout) + + if response["sequence"] != sequence: + raise BuzzerError( + f"Antwort-Sequenz passt nicht: erwartet {sequence}, erhalten {response['sequence']}" + ) + + if response["command_id"] != CMD_LIST_DIR: + raise BuzzerError( + f"Antwort-Kommando passt nicht: erwartet 0x{CMD_LIST_DIR:02X}, erhalten 0x{response['command_id']:02X}" + ) + + frame_type = response["frame_type"] + payload = response["payload"] + + if frame_type == FRAME_RESP_ERROR: + error_code = payload[0] if len(payload) >= 1 else 0x32 + detail = "" + if len(payload) >= 2: + detail_len = payload[1] + if detail_len > 0 and len(payload) >= 2 + detail_len: + detail = payload[2:2 + detail_len].decode("utf-8", errors="replace") + + msg = self._parse_controller_error_code(error_code) + if detail: + msg = f"{msg} Detail: {detail}" + raise BuzzerError(msg) + + if frame_type == FRAME_RESP_STREAM_START: + stream_started = True + continue + + if frame_type == FRAME_RESP_STREAM_CHUNK: + if len(payload) < 6: + raise BuzzerError("Ungültiger LIST_DIR Chunk: zu kurz") + + entry_type = payload[0] + name_len = payload[1] + if len(payload) != 6 + name_len: + raise BuzzerError("Ungültiger LIST_DIR Chunk: inkonsistente Namenslänge") + + size = struct.unpack(" int: + payload = self.send_binary_command(CMD_CHECK_FILE_CRC, self._encode_path_payload(path), timeout=timeout) + if len(payload) != 4: + raise BuzzerError(f"Ungültige Antwortlänge für CHECK_FILE_CRC: {len(payload)}") + return struct.unpack(" None: + payload = self.send_binary_command(CMD_MKDIR, self._encode_path_payload(path), timeout=timeout) + if len(payload) != 0: + raise BuzzerError(f"Unerwartete Payload für MKDIR: {len(payload)} Bytes") + + def rm(self, path: str, timeout: float = None) -> None: + payload = self.send_binary_command(CMD_RM, self._encode_path_payload(path), timeout=timeout) + if len(payload) != 0: + raise BuzzerError(f"Unerwartete Payload für RM: {len(payload)} Bytes") + + def stat(self, path: str, timeout: float = None) -> dict: + payload = self.send_binary_command(CMD_STAT, self._encode_path_payload(path), timeout=timeout) + if len(payload) != 5: + raise BuzzerError(f"Ungültige Antwortlänge für STAT: {len(payload)}") + + entry_type = payload[0] + if entry_type == 0: + type_char = "F" + elif entry_type == 1: + type_char = "D" + else: + raise BuzzerError(f"Ungültiger STAT entry_type: {entry_type}") + + size = struct.unpack(" None: + old_payload = self._encode_path_payload(old_path) + new_payload = self._encode_path_payload(new_path) + payload = old_payload + new_payload + response = self.send_binary_command(CMD_RENAME, payload, timeout=timeout) + if len(response) != 0: + raise BuzzerError(f"Unerwartete Payload für RENAME: {len(response)} Bytes") + + def rm_recursive(self, path: str, timeout: float = None) -> None: + payload = self.send_binary_command(CMD_RM_R, self._encode_path_payload(path), timeout=timeout) + if len(payload) != 0: + raise BuzzerError(f"Unerwartete Payload für RM_R: {len(payload)} Bytes") + + def get_file_data(self, path: str, timeout: float = None, progress_callback=None) -> bytes: + if self.serial is None: + raise BuzzerError("Serielle Verbindung ist nicht geöffnet.") + + eff_timeout = timeout if timeout is not None else self.timeout + self.serial.reset_input_buffer() + + sequence = self._next_sequence() + frame = self._build_frame(FRAME_REQ, CMD_GET_FILE, sequence, self._encode_path_payload(path)) + self._write_frame(frame) + + start_response = self._read_frame(timeout=eff_timeout) + if start_response["sequence"] != sequence: + raise BuzzerError( + f"Antwort-Sequenz passt nicht: erwartet {sequence}, erhalten {start_response['sequence']}" + ) + if start_response["command_id"] != CMD_GET_FILE: + raise BuzzerError( + f"Antwort-Kommando passt nicht: erwartet 0x{CMD_GET_FILE:02X}, erhalten 0x{start_response['command_id']:02X}" + ) + if start_response["frame_type"] == FRAME_RESP_ERROR: + self._raise_error_from_payload(start_response["payload"]) + if start_response["frame_type"] != FRAME_RESP_DATA or len(start_response["payload"]) != 4: + raise BuzzerError("Ungültige GET_FILE-Startantwort (erwartet DATA mit 4 Byte Länge)") + + expected_len = struct.unpack(" None: + if total_len < 0: + raise BuzzerError("Dateigröße darf nicht negativ sein.") + payload = self._encode_path_payload(path) + struct.pack(" None: + self._send_binary_frame_no_wait(CMD_PUT_FILE_CHUNK, chunk) + + def put_file_end(self, timeout: float = None) -> None: + eff_timeout = timeout if timeout is not None else self.timeout + end_sequence = self._send_binary_frame_no_wait(CMD_PUT_FILE_END, b"") + + deadline = time.monotonic() + eff_timeout + while True: + remaining = deadline - time.monotonic() + if remaining <= 0: + raise TimeoutError("Lese-Timeout beim Warten auf PUT_FILE_END Antwort.") + + response = self._read_frame(timeout=remaining) + + if response["frame_type"] == FRAME_RESP_ERROR and response["command_id"] in ( + CMD_PUT_FILE_START, + CMD_PUT_FILE_CHUNK, + CMD_PUT_FILE_END, + ): + self._raise_error_from_payload(response["payload"]) + + if response["command_id"] != CMD_PUT_FILE_END or response["sequence"] != end_sequence: + continue + + if response["frame_type"] not in (FRAME_RESP_ACK, FRAME_RESP_DATA): + raise BuzzerError(f"Unerwarteter Response-Typ für PUT_FILE_END: 0x{response['frame_type']:02X}") + + if len(response["payload"]) != 0: + raise BuzzerError(f"Unerwartete Payload für PUT_FILE_END: {len(response['payload'])} Bytes") + + return + + def put_file_data( + self, + path: str, + data: bytes, + timeout: float = None, + chunk_size: int = 4096, + progress_callback=None, + ) -> int: + if data is None: + data = b"" + if chunk_size <= 0: + raise BuzzerError("chunk_size muss größer als 0 sein.") + + expected_crc32 = binascii.crc32(data) & 0xFFFFFFFF + if self.serial is None: + raise BuzzerError("Serielle Verbindung ist nicht geöffnet.") + + eff_timeout = timeout if timeout is not None else self.timeout + self.serial.reset_input_buffer() + + sequence = self._next_sequence() + start_payload = self._encode_path_payload(path) + struct.pack(" int: + if data is None: + data = b"" + if chunk_size <= 0: + raise BuzzerError("chunk_size muss größer als 0 sein.") + if len(data) == 0: + raise BuzzerError("Firmware-Datei ist leer.") + + expected_crc32 = binascii.crc32(data) & 0xFFFFFFFF + if self.serial is None: + raise BuzzerError("Serielle Verbindung ist nicht geöffnet.") + + eff_timeout = timeout if timeout is not None else self.timeout + old_write_timeout = self.serial.write_timeout + self.serial.write_timeout = max(float(old_write_timeout or 0.0), float(eff_timeout)) + self.serial.reset_input_buffer() + + try: + sequence = self._next_sequence() + start_payload = struct.pack(" bytes: + if self.serial is None: + raise BuzzerError("Serielle Verbindung ist nicht geöffnet.") + + eff_timeout = timeout if timeout is not None else self.timeout + self.serial.reset_input_buffer() + + sequence = self._next_sequence() + frame = self._build_frame(FRAME_REQ, CMD_GET_TAG_BLOB, sequence, self._encode_path_payload(path)) + + try: + self.serial.write(frame) + self.serial.flush() + except Exception as e: + if e.__class__.__name__ == "SerialTimeoutException": + raise TimeoutError(f"Schreib-Timeout am Port {self.port}. Ist das Gerät blockiert?") from e + raise + + expected_len = None + chunks = bytearray() + + while True: + response = self._read_frame(timeout=eff_timeout) + + if response["sequence"] != sequence: + raise BuzzerError( + f"Antwort-Sequenz passt nicht: erwartet {sequence}, erhalten {response['sequence']}" + ) + + if response["command_id"] != CMD_GET_TAG_BLOB: + raise BuzzerError( + f"Antwort-Kommando passt nicht: erwartet 0x{CMD_GET_TAG_BLOB:02X}, erhalten 0x{response['command_id']:02X}" + ) + + frame_type = response["frame_type"] + payload = response["payload"] + + if frame_type == FRAME_RESP_ERROR: + error_code = payload[0] if len(payload) >= 1 else 0x32 + raise BuzzerError(self._parse_controller_error_code(error_code)) + + if frame_type == FRAME_RESP_STREAM_START: + if len(payload) != 4: + raise BuzzerError("Ungültiger GET_TAG_BLOB START-Frame") + expected_len = struct.unpack(" None: + if blob is None: + blob = b"" + + if len(blob) > 1024: + raise BuzzerError("Tag-Blob ist zu groß (max. 1024 Bytes).") + + path_payload = self._encode_path_payload(path) + start_payload = path_payload + struct.pack(" list: eff_timeout = custom_timeout if custom_timeout is not None else self.timeout self.serial.reset_input_buffer() @@ -68,8 +776,10 @@ class BuzzerConnection: try: self.serial.write(f"{command}\n".encode('utf-8')) self.serial.flush() - except serial.SerialTimeoutException: - raise TimeoutError(f"Schreib-Timeout am Port {self.port}. Ist das Gerät blockiert?") + except Exception as e: + if e.__class__.__name__ == "SerialTimeoutException": + raise TimeoutError(f"Schreib-Timeout am Port {self.port}. Ist das Gerät blockiert?") from e + raise lines = [] start_time = time.monotonic() @@ -91,7 +801,7 @@ class BuzzerConnection: except Exception as e: raise BuzzerError(f"Fehler beim Lesen der Antwort: {e}") else: - time.sleep(0.01) + time.sleep(POLL_SLEEP_SECONDS) raise TimeoutError(f"Lese-Timeout ({eff_timeout}s) beim Warten auf Antwort für: '{command}'") @@ -110,7 +820,7 @@ class BuzzerConnection: break elif line.startswith("ERR"): raise BuzzerError(f"Fehler vor Binärtransfer: {self._parse_controller_error(line)}") - time.sleep(0.01) + time.sleep(POLL_SLEEP_SECONDS) if not ready: raise TimeoutError("Kein READY-Signal vom Controller empfangen.") @@ -151,6 +861,6 @@ class BuzzerConnection: return True elif line.startswith("ERR"): raise BuzzerError(f"Fehler beim Speichern der Binärdatei: {self._parse_controller_error(line)}") - time.sleep(0.01) + time.sleep(POLL_SLEEP_SECONDS) raise TimeoutError("Zeitüberschreitung nach Binärtransfer (kein OK empfangen).") \ No newline at end of file diff --git a/buzzer_tool/smoke_test.sh b/buzzer_tool/smoke_test.sh new file mode 100755 index 0000000..f29c349 --- /dev/null +++ b/buzzer_tool/smoke_test.sh @@ -0,0 +1,165 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)" +CLI="$ROOT_DIR/buzzer_tool/buzzer.py" + +PORT="" +BAUDRATE="" +TIMEOUT="" +REMOTE_BASE="/lfs/smoke" +KEEP_TMP=0 + +usage() { + cat < [-b baudrate] [-t timeout] [--remote-base /lfs/path] [--keep-tmp] + +Beispiel: + $(basename "$0") -p /dev/tty.usbmodem14101 -b 115200 -t 5 +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + -p|--port) + PORT="$2" + shift 2 + ;; + -b|--baudrate) + BAUDRATE="$2" + shift 2 + ;; + -t|--timeout) + TIMEOUT="$2" + shift 2 + ;; + --remote-base) + REMOTE_BASE="$2" + shift 2 + ;; + --keep-tmp) + KEEP_TMP=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unbekanntes Argument: $1" >&2 + usage + exit 1 + ;; + esac +done + +if [[ -z "$PORT" ]]; then + echo "Fehler: --port ist erforderlich" >&2 + usage + exit 1 +fi + +COMMON_ARGS=("-p" "$PORT" "--no-auto-info") +if [[ -n "$BAUDRATE" ]]; then + COMMON_ARGS+=("-b" "$BAUDRATE") +fi +if [[ -n "$TIMEOUT" ]]; then + COMMON_ARGS+=("-t" "$TIMEOUT") +fi + +run_cli() { + python3 "$CLI" "${COMMON_ARGS[@]}" "$@" +} + +TMP_DIR="$(mktemp -d -t buzzer-smoke-XXXXXX)" +cleanup() { + if [[ "$KEEP_TMP" -eq 0 ]]; then + rm -rf "$TMP_DIR" + else + echo "Temporärer Ordner bleibt erhalten: $TMP_DIR" + fi +} +trap cleanup EXIT + +echo "[1/9] Erzeuge sehr kleine Testdateien mit Nullen in $TMP_DIR" +python3 - "$TMP_DIR" <<'PY' +import os +import sys +base = sys.argv[1] +files = { + "z0.bin": 0, + "z1.bin": 1, + "z8.bin": 8, + "z16.bin": 16, + "z64.bin": 64, + "z100k.bin": 102400, +} +for name, size in files.items(): + with open(os.path.join(base, name), "wb") as f: + f.write(b"\x00" * size) +print("Created:", ", ".join(f"{k}:{v}B" for k,v in files.items())) +PY + +echo "[2/9] Bereinige altes Remote-Testverzeichnis (falls vorhanden)" +run_cli rm -r "$REMOTE_BASE" >/dev/null 2>&1 || true + +echo "[3/9] Lege Remote-Verzeichnis an" +run_cli mkdir "$REMOTE_BASE" + +echo "[4/9] Upload der kleinen Null-Dateien" +run_cli put "$TMP_DIR"/*.bin "$REMOTE_BASE/" + +echo "[5/9] Prüfe remote stat" +run_cli stat "$REMOTE_BASE/z0.bin" +run_cli stat "$REMOTE_BASE/z64.bin" + +echo "[6/9] Teste rename/mv" +run_cli mv "$REMOTE_BASE/z16.bin" "$REMOTE_BASE/z16_renamed.bin" +run_cli stat "$REMOTE_BASE/z16_renamed.bin" + +echo "[7/9] Pull + get_file (Alias)" +run_cli pull "$REMOTE_BASE/z1.bin" "$TMP_DIR/pull_z1.bin" +run_cli get_file "$REMOTE_BASE/z100k.bin" "$TMP_DIR/get_file_z100k.bin" + +echo "[8/9] Vergleiche heruntergeladene Dateien" +python3 - "$TMP_DIR" <<'PY' +import os +import sys + +TAG_MAGIC = b"TAG!" +TAG_FOOTER_LEN = 7 + +base = sys.argv[1] + +checks = [ + ("z1.bin", "pull_z1.bin"), + ("z8.bin", "get_file_z8.bin"), +] + +for original_name, pulled_name in checks: + original_path = os.path.join(base, original_name) + pulled_path = os.path.join(base, pulled_name) + + with open(original_path, "rb") as f: + original = f.read() + with open(pulled_path, "rb") as f: + pulled = f.read() + + if len(pulled) < len(original) + TAG_FOOTER_LEN: + raise SystemExit(f"Pulled file zu kurz: {pulled_name}") + + if pulled[:len(original)] != original: + raise SystemExit(f"Audio-Präfix stimmt nicht: {pulled_name}") + + if pulled[-4:] != TAG_MAGIC: + raise SystemExit(f"TAG-Footer fehlt: {pulled_name}") + +print("Vergleich OK (audio + tags)") +PY + +echo "[9/9] Rekursives Löschen + Abschlussliste" +run_cli rm -r "$REMOTE_BASE" +run_cli ls /lfs + +echo "✅ Smoke-Test erfolgreich abgeschlossen" diff --git a/firmware/Protokoll.md b/firmware/Protokoll.md new file mode 100644 index 0000000..27bb4e7 --- /dev/null +++ b/firmware/Protokoll.md @@ -0,0 +1,303 @@ +# Buzzer-Kommunikationsprotokoll (vNext Entwurf) + +Diese Datei beschreibt den geplanten Neuentwurf des Protokolls. Ziel ist ein klar strukturiertes Binärprotokoll mit stabilen Typen, expliziter Endianness und sauberem Streaming für große Daten. + +## 1) Grundregeln + +- Transport: USB CDC (Byte-Stream) +- Multi-Byte-Integer: **Little Endian** +- Integer-Typen im Protokoll: nur feste Breiten (`uint8_t`, `uint16_t`, `uint32_t`, `uint64_t`) +- `bool`: als `uint8_t` (`0` = false, `1` = true) +- Strings: immer `len + bytes`, ohne implizites `\0` +- `unsigned long` wird im Protokoll **nicht** verwendet (plattformabhängig) + +> Hinweis: Obwohl nRF52 und STM32G0 beide Little Endian sind, bleibt Endianness explizit Teil der Spezifikation. + +## 2) Frame-Struktur + +Alle Requests und Responses nutzen denselben Header. + +### 2.1 Header + +| Feld | Typ | Beschreibung | +|---|---|---| +| `sync` | 4 Byte | Immer `BUZZ` (`0x42 0x55 0x5A 0x5A`) | +| `frame_type` | `uint8_t` | Typ des Frames (siehe unten) | +| `command_id` | `uint8_t` | Kommando-ID | +| `sequence` | `uint16_t` | Anfrage-/Antwort-Korrelation | +| `payload_len` | `uint32_t` | Länge von `payload` in Bytes | +| `header_crc16` | `uint16_t` | CRC16 über Header ohne `sync` und ohne `header_crc16` | + +Danach folgen: + +| Feld | Typ | Beschreibung | +|---|---|---| +| `payload` | `byte[payload_len]` | Kommandoabhängige Nutzdaten | +| `payload_crc32` | `uint32_t` | CRC32 über `payload` | + +### 2.2 `frame_type` + +| Wert | Name | Bedeutung | +|---:|---|---| +| `0x01` | `REQ` | Host → Device Anfrage | +| `0x10` | `RESP_ACK` | Erfolgreich, ohne Nutzdaten | +| `0x11` | `RESP_DATA` | Erfolgreich, mit Nutzdaten | +| `0x12` | `RESP_STREAM_START` | Start eines Datenstroms | +| `0x13` | `RESP_STREAM_CHUNK` | Chunk eines Datenstroms | +| `0x14` | `RESP_STREAM_END` | Ende eines Datenstroms | +| `0x7F` | `RESP_ERROR` | Fehlerantwort | + +## 3) Antwortmodell (ACK vs DATA) + +Ja, wir unterscheiden explizit: + +- **`RESP_ACK`**: „OK ohne Payload“ (Host muss nur Erfolg verbuchen) +- **`RESP_DATA`**: „OK mit Payload“ +- **`RESP_ERROR`**: Fehlercode + optionaler Detailtext + +Damit ist Parsing eindeutig und ohne Sonderfälle wie „OK aber vielleicht mit Daten“. + +## 4) Fehlercodes + +Fehlercodes bleiben wie bisher inhaltlich erhalten (`P_ERR_*`), werden aber als Binärwert in `RESP_ERROR` übertragen. + +### 4.1 Fehler-Payload (`RESP_ERROR`) + +| Feld | Typ | Beschreibung | +|---|---|---| +| `error_code` | `uint8_t` | Protokollfehlercode | +| `detail_len` | `uint8_t` | Länge von `detail` | +| `detail` | `byte[detail_len]` | Optionaler Kurztext (ASCII/UTF-8) | + +## 5) Kommandos (erste Aufteilung) + +## 5.1 `0x00` `GET_PROTOCOL_VERSION` + +**Request-Payload:** keine + +**Response:** `RESP_DATA` + +| Feld | Typ | Beschreibung | +|---|---|---| +| `protocol_version` | `uint16_t` | Versionsnummer des Protokolls | + +## 5.2 `0x01` `GET_FIRMWARE_STATUS` + +**Request-Payload:** keine + +**Response:** `RESP_DATA` + +| Feld | Typ | Beschreibung | +|---|---|---| +| `status` | `uint8_t` | `0` = pending, `1` = confirmed | +| `version_len` | `uint8_t` | Länge des Versionsstrings | +| `version` | `byte[version_len]` | Firmware-Version | + +## 5.3 `0x02` `GET_FLASH_STATUS` + +**Request-Payload:** keine + +**Response:** `RESP_DATA` + +| Feld | Typ | Beschreibung | +|---|---|---| +| `block_size` | `uint32_t` | Bytes pro Block | +| `total_blocks` | `uint32_t` | Gesamtblöcke | +| `free_blocks` | `uint32_t` | Freie Blöcke | +| `path_max_len` | `uint32_t` | Maximale erlaubte Pfadlänge in Bytes | + +## 5.4 `0x10` `LIST_DIR` + +**Request-Payload:** + +| Feld | Typ | Beschreibung | +|---|---|---| +| `path_len` | `uint8_t` | Länge von `path` | +| `path` | `byte[path_len]` | Zielverzeichnis, z. B. `/` oder `/lfs/a` | + +`path_len` darf den in `GET_FLASH_STATUS.path_max_len` gemeldeten Wert nicht überschreiten. + +**Response:** `RESP_DATA` + +`LIST_DIR` liefert Einträge als Stream: + +1. `RESP_STREAM_START` +2. `RESP_STREAM_CHUNK` pro Verzeichniseintrag +3. `RESP_STREAM_END` + +### `RESP_STREAM_START` Payload + +| Feld | Typ | Beschreibung | +|---|---|---| +| `entry_count` | `uint32_t` | Anzahl Einträge, `0xFFFFFFFF` = unbekannt | + +### `RESP_STREAM_CHUNK` Payload (ein Eintrag) + +| Feld | Typ | Beschreibung | +|---|---|---| +| `entry_type` | `uint8_t` | `0` = Datei, `1` = Verzeichnis | +| `name_len` | `uint8_t` | Länge von `name` | +| `size` | `uint32_t` | Dateigröße in Bytes (`0` für Verzeichnisse) | +| `name` | `byte[name_len]` | Name ohne führenden Pfad | + +### `RESP_STREAM_END` Payload + +- leer + +**Hinweis:** Rekursion bleibt Host-seitig. Der Host setzt aus angefragtem Basispfad + `name` den vollständigen Pfad zusammen. + +## 5.5 `0x11` `CHECK_FILE_CRC` + +**Request-Payload:** + +| Feld | Typ | Beschreibung | +|---|---|---| +| `path_len` | `uint8_t` | Länge von `path` | +| `path` | `byte[path_len]` | Pfad zur Datei | + +`path_len` darf den in `GET_FLASH_STATUS.path_max_len` gemeldeten Wert nicht überschreiten. + +**Response:** `RESP_DATA` + +| Feld | Typ | Beschreibung | +|---|---|---| +| `crc32` | `uint32_t` | CRC32 (Little Endian) über den Audio-Inhalt | + +**Hinweis:** Die CRC-Berechnung folgt der aktuellen Firmware-Logik für Audio-Dateien (ohne angehängte Tag-Daten). + +## 5.6 `0x12` `MKDIR` + +**Request-Payload:** + +| Feld | Typ | Beschreibung | +|---|---|---| +| `path_len` | `uint8_t` | Länge von `path` | +| `path` | `byte[path_len]` | Zielpfad für neues Verzeichnis | + +`path_len` darf den in `GET_FLASH_STATUS.path_max_len` gemeldeten Wert nicht überschreiten. + +**Response:** `RESP_ACK` + +Bei Erfolg wird eine leere ACK-Antwort gesendet. Bei Fehlern `RESP_ERROR` mit passendem `P_ERR_*`. + +## 5.7 `0x13` `RM` + +**Request-Payload:** + +| Feld | Typ | Beschreibung | +|---|---|---| +| `path_len` | `uint8_t` | Länge von `path` | +| `path` | `byte[path_len]` | Pfad zu Datei oder leerem Verzeichnis | + +`path_len` darf den in `GET_FLASH_STATUS.path_max_len` gemeldeten Wert nicht überschreiten. + +**Response:** `RESP_ACK` + +Bei Erfolg wird eine leere ACK-Antwort gesendet. Bei Fehlern `RESP_ERROR` mit passendem `P_ERR_*`. + +**Hinweis:** Rekursives Löschen bleibt Host-seitig (mehrere `LIST_DIR` + `RM` Aufrufe). + +## 5.8 `0x20` `GET_TAG_BLOB` + +**Request-Payload:** + +| Feld | Typ | Beschreibung | +|---|---|---| +| `path_len` | `uint8_t` | Länge von `path` | +| `path` | `byte[path_len]` | Pfad zur Datei | + +`path_len` darf den in `GET_FLASH_STATUS.path_max_len` gemeldeten Wert nicht überschreiten. + +**Response:** Stream + +1. `RESP_STREAM_START` mit `total_len:uint32_t` +2. `RESP_STREAM_CHUNK` mit rohen Tag-Blob-Bytes +3. `RESP_STREAM_END` (leer) + +Der Blob-Inhalt ist die TLV-Metadatenstruktur aus `Tags.md` (ohne Footer `version+len+TAG!`). + +## 5.9 `0x21` `SET_TAG_BLOB_START` + +Startet eine Tag-Blob-Übertragung. + +**Request-Payload:** + +| Feld | Typ | Beschreibung | +|---|---|---| +| `path_len` | `uint8_t` | Länge von `path` | +| `path` | `byte[path_len]` | Pfad zur Datei | +| `total_len` | `uint16_t` | Gesamtlänge des folgenden Blobs | + +**Response:** `RESP_ACK` + +## 5.10 `0x22` `SET_TAG_BLOB_CHUNK` + +Sendet einen weiteren Chunk des Blobs. + +**Request-Payload:** + +| Feld | Typ | Beschreibung | +|---|---|---| +| `data` | `byte[]` | Chunk-Daten | + +**Response:** `RESP_ACK` + +## 5.11 `0x23` `SET_TAG_BLOB_END` + +Schließt den Upload ab und schreibt den Blob in die Datei (ersetzt bestehende Tags). + +**Request-Payload:** leer + +**Response:** `RESP_ACK` + +Hinweis: Ein leerer Blob (`total_len=0`) ist erlaubt und entspricht "Tags leeren". + +## 6) Große Daten (Dateien, Audio, Firmware) + +Für mehrere MB nicht als einzelnes `RESP_DATA` senden, sondern streamen. + +### 6.1 Warum nicht ein riesiges `payload_len`? + +- Höheres Risiko bei Paketverlust/Timeout +- Mehr RAM-/Pufferdruck auf MCU +- Schlechtere Wiederanlaufstrategie bei Fehlern + +### 6.2 Streaming-Ablauf + +1. `RESP_STREAM_START` mit Metadaten +2. `RESP_STREAM_CHUNK` (mehrfach) +3. `RESP_STREAM_END` mit Gesamt-CRC/Abschlussstatus + +### 6.3 Stream-Metadaten (`RESP_STREAM_START`) + +| Feld | Typ | Beschreibung | +|---|---|---| +| `stream_id` | `uint16_t` | ID des Streams | +| `total_len` | `uint32_t` | Gesamtlänge in Bytes | +| `chunk_size` | `uint16_t` | Ziel-Chunkgröße | + +### 6.4 Chunk-Payload (`RESP_STREAM_CHUNK`) + +| Feld | Typ | Beschreibung | +|---|---|---| +| `stream_id` | `uint16_t` | Zuordnung zum Stream | +| `offset` | `uint32_t` | Byte-Offset im Gesamtstrom | +| `data_len` | `uint16_t` | Länge von `data` | +| `data` | `byte[data_len]` | Nutzdaten | + +`total_len` ist `uint32_t`, damit sind bis 4 GiB abbildbar. Für euer Gerät ist das mehr als ausreichend und trotzdem standardisiert. + +## 7) Offene Punkte für die Implementierung + +- Fixe maximale `chunk_size` (z. B. 256/512/1024 Bytes) +- ACK/NACK auf Chunk-Ebene nötig oder „best effort + Retry auf Kommando-Ebene“? +- Timeout-/Retry-Policy pro Kommando +- Welche Kommandos zuerst in `protocol.c` migriert werden + +## 8) Kurzfazit für den nächsten Schritt + +- Ja, getrennte Kommandos sind sinnvoll. +- Ja, Endianness muss explizit definiert sein (Little Endian). +- Ja, `ACK` und `DATA` sollten als unterschiedliche Frame-Typen geführt werden. +- Für große Dateien: `uint32_t total_len` + Chunk-Streaming statt Einmal-Payload. + diff --git a/firmware/Tags.md b/firmware/Tags.md new file mode 100644 index 0000000..d445b21 --- /dev/null +++ b/firmware/Tags.md @@ -0,0 +1,133 @@ +# Audio-Tags Format + +Dieses Dokument beschreibt das aktuelle Tag-Format für Audiodateien. + +## 1) Position in der Datei + +Die Tags stehen am Dateiende: + +`[audio_data][metadata][tag_version_u8][footer_len_le16]["TAG!"]` + +- `audio_data`: eigentliche Audiodaten +- `metadata`: Folge von Tag-Einträgen +- `tag_version_u8`: 1 Byte Versionsnummer des Tag-Formats +- `footer_len_le16`: 2 Byte, Little Endian +- `"TAG!"`: 4 Byte Magic (`0x54 0x41 0x47 0x21`) + +## 2) Bedeutung von `footer_len_le16` + +`footer_len_le16` ist die **Gesamtlänge des Footers**, also: + +`footer_len = metadata_len + 1 + 2 + 4` + +Damit beginnt `metadata` bei: + +`metadata_start = file_size - footer_len` + +Das passt zur aktuellen Implementierung in der Firmware. + +### Tag-Version + +- `tag_version` ist aktuell `0x01`. +- Der Host darf nur bekannte Versionen interpretieren. +- Bei unbekannter Version: Tag-Block ignorieren oder als "nicht unterstützt" melden. + +## 3) Endianness und Typen + +- Alle Multi-Byte-Werte sind **Little Endian**. +- Tag-Einträge sind TLV-basiert: + - `type`: `uint8_t` + - `len`: `uint16_t` + - `value`: `byte[len]` + +Dadurch können auch unbekannte Typen sauber übersprungen werden. + +## 4) Unterstützte Tag-Typen + +Aktuell definierte Typen: + +- `0x00`: `DESCRIPTION` (Beschreibung des Samples) +- `0x01`: `AUTHOR` +- `0x10`: `CRC32_RAW` +- `0x20`: `FILE_FORMAT` (Info für Host, Player wertet derzeit nicht aus) + +## 5) Value-Format pro Tag + +### 5.1 `0x00` DESCRIPTION + +- `value`: UTF-8-Text +- `len`: Anzahl Bytes des UTF-8-Texts + +### 5.2 `0x01` AUTHOR + +- `value`: UTF-8-Text +- `len`: Anzahl Bytes des UTF-8-Texts + +### 5.3 `0x10` CRC32_RAW + +- `value`: `uint32_t crc32` (4 Byte, Little Endian) +- `len`: **muss 4** sein + +### 5.4 `0x20` FILE_FORMAT + +- `value`: + - `bits_per_sample`: `uint8_t` + - `sample_rate`: `uint32_t` (Little Endian) +- `len`: **muss 5** sein + +Beispielwerte aktuell oft: `bits_per_sample = 16`, `sample_rate = 16000`. + +## 6) Vorkommen je Typ + +Aktueller Stand: **jeder Tag-Typ darf maximal 1x vorkommen**. + +Empfohlene Host-Regel: + +- Falls ein Typ mehrfach vorkommt, letzte Instanz gewinnt (`last-wins`) und ein Warnhinweis wird geloggt. + +## 7) Validierungsregeln (Host) + +Beim Lesen: + +1. Prüfen, ob Datei mindestens 7 Byte hat. +2. Letzte 6 Byte prüfen: `footer_len_le16` + `TAG!`. +3. `footer_len` gegen Dateigröße validieren (`6 <= footer_len <= file_size`). +4. `tag_version` an Position `file_size - 6 - 1` lesen und validieren. +5. Im Metadatenbereich TLV-Einträge lesen, bis Ende erreicht. +6. Für bekannte Typen feste Längen prüfen (`CRC32_RAW=4`, `FILE_FORMAT=5`). +7. Unbekannte Typen über `len` überspringen. + +Beim Schreiben: + +1. Vorhandene Tags entfernen/ersetzen (audio-Ende bestimmen). +2. Neue TLV-Metadaten schreiben. +3. `tag_version_u8` schreiben (`0x01`). +4. `footer_len_le16` schreiben (inkl. 1+2+4). +5. `TAG!` schreiben. +5. Datei auf neue Länge truncaten. + +## 8) Beispiel (hex) + +Beispiel mit: + +- DESCRIPTION = "Kick" +- AUTHOR = "Edi" +- CRC32_RAW = `0x12345678` + +TLV-Daten: + +- `00 04 00 4B 69 63 6B` +- `01 03 00 45 64 69` +- `10 04 00 78 56 34 12` + +`metadata_len = 7 + 6 + 7 = 20 (0x0014)` + +`footer_len = 20 + 1 + 2 + 4 = 27 (0x001B)` + +Footer-Ende: + +- `01 1B 00 54 41 47 21` + +## 9) Hinweis zur aktuellen Firmware + +Die Firmware verarbeitet Tag-Payload direkt binär (Chunk-Streaming über das Protokoll). Das dateiinterne Format entspricht direkt diesem Dokument. diff --git a/firmware/VERSION b/firmware/VERSION index ab553f1..9d7380e 100644 --- a/firmware/VERSION +++ b/firmware/VERSION @@ -1,5 +1,9 @@ VERSION_MAJOR = 0 -VERSION_MINOR = 1 -PATCHLEVEL = 14 +VERSION_MINOR = 2 +PATCHLEVEL = 19 VERSION_TWEAK = 0 -EXTRAVERSION = debug \ No newline at end of file +#if (IS_ENABLED(CONFIG_LOG)) +EXTRAVERSION = debug +#else +EXTRAVERSION = 0 +#endif \ No newline at end of file diff --git a/firmware/prj.conf b/firmware/prj.conf index 03b1eab..d7df214 100644 --- a/firmware/prj.conf +++ b/firmware/prj.conf @@ -29,6 +29,7 @@ CONFIG_USB_DEVICE_PRODUCT="Edi's Buzzer" CONFIG_USB_DEVICE_PID=0x0001 CONFIG_USB_DRIVER_LOG_LEVEL_ERR=y CONFIG_USB_DEVICE_LOG_LEVEL_ERR=y +CONFIG_USB_DEVICE_LOG_LEVEL_OFF=y CONFIG_USB_DEVICE_INITIALIZE_AT_BOOT=n CONFIG_USB_DEVICE_STACK_NEXT=n @@ -47,13 +48,14 @@ CONFIG_FLASH_MAP=y CONFIG_STREAM_FLASH=y CONFIG_IMG_MANAGER=y CONFIG_MCUBOOT_IMG_MANAGER=y +CONFIG_MCUBOOT_UTIL_LOG_LEVEL_ERR=y # --- HWINFO und CRC --- CONFIG_HWINFO=y CONFIG_CRC=y # --- Debugging & Sicherheit --- -CONFIG_ASSERT=y +CONFIG_ASSERT=n CONFIG_HW_STACK_PROTECTION=y CONFIG_RESET_ON_FATAL_ERROR=y diff --git a/firmware/src/audio.c b/firmware/src/audio.c index 7913a29..378f6d1 100644 --- a/firmware/src/audio.c +++ b/firmware/src/audio.c @@ -19,7 +19,7 @@ #define AUDIO_WORD_WIDTH 16 #define AUDIO_SAMPLE_RATE 16000 -LOG_MODULE_REGISTER(audio, LOG_LEVEL_DBG); +LOG_MODULE_REGISTER(audio, LOG_LEVEL_INF); /* Dauer eines Blocks in ms (4096 Bytes / (16kHz * 2 Kanäle * 2 Bytes)) = 64 ms */ #define BLOCK_DURATION_MS ((AUDIO_BLOCK_SIZE * 1000) / (AUDIO_SAMPLE_RATE * 2 * (AUDIO_WORD_WIDTH / 8))) diff --git a/firmware/src/fs.c b/firmware/src/fs.c index d089e3d..3354ca3 100644 --- a/firmware/src/fs.c +++ b/firmware/src/fs.c @@ -5,9 +5,15 @@ #include #include -#include -LOG_MODULE_REGISTER(buzz_fs, LOG_LEVEL_DBG); +LOG_MODULE_REGISTER(buzz_fs, LOG_LEVEL_INF); + +#define TAG_MAGIC "TAG!" +#define TAG_MAGIC_LEN 4U +#define TAG_LEN_FIELD_LEN 2U +#define TAG_VERSION_LEN 1U +#define TAG_FOOTER_V1_LEN (TAG_VERSION_LEN + TAG_LEN_FIELD_LEN + TAG_MAGIC_LEN) +#define TAG_FORMAT_VERSION 0x01 #define STORAGE_PARTITION_ID FIXED_PARTITION_ID(littlefs_storage) #define SLOT1_ID FIXED_PARTITION_ID(slot1_partition) @@ -162,6 +168,15 @@ int fs_pm_statvfs(const char *path, struct fs_statvfs *stat) return rc; } +int fs_pm_stat(const char *path, struct fs_dirent *entry) +{ + LOG_DBG("PM Getting stat for '%s'", path); + fs_pm_flash_resume(); + int rc = fs_stat(path, entry); + fs_pm_flash_suspend(); + return rc; +} + int fs_pm_mkdir(const char *path) { LOG_DBG("PM Creating directory '%s'", path); @@ -171,28 +186,91 @@ int fs_pm_mkdir(const char *path) return rc; } -ssize_t fs_get_audio_data_len(struct fs_file_t *fp) { +int fs_pm_rename(const char *old_path, const char *new_path) +{ + LOG_DBG("PM Renaming '%s' to '%s'", old_path, new_path); + fs_pm_flash_resume(); + int rc = fs_rename(old_path, new_path); + fs_pm_flash_suspend(); + return rc; +} + +static int fs_get_tag_bounds(struct fs_file_t *fp, off_t file_size, + size_t *audio_limit, size_t *payload_len, bool *has_tag) +{ uint8_t footer[6]; + uint16_t tag_len; + + if (audio_limit == NULL || payload_len == NULL || has_tag == NULL) { + return -EINVAL; + } + + *has_tag = false; + *audio_limit = (size_t)file_size; + *payload_len = 0U; + + if (file_size < (off_t)TAG_FOOTER_V1_LEN) { + return 0; + } + + fs_seek(fp, -(off_t)(TAG_LEN_FIELD_LEN + TAG_MAGIC_LEN), FS_SEEK_END); + if (fs_read(fp, footer, sizeof(footer)) != sizeof(footer)) { + fs_seek(fp, 0, FS_SEEK_SET); + return -EIO; + } + + if (memcmp(&footer[2], TAG_MAGIC, TAG_MAGIC_LEN) != 0) { + fs_seek(fp, 0, FS_SEEK_SET); + return 0; + } + + tag_len = (uint16_t)footer[0] | ((uint16_t)footer[1] << 8); + if (tag_len > (uint16_t)file_size || tag_len < TAG_FOOTER_V1_LEN) { + fs_seek(fp, 0, FS_SEEK_SET); + return -EBADMSG; + } + + uint8_t tag_version = 0; + fs_seek(fp, -(off_t)TAG_FOOTER_V1_LEN, FS_SEEK_END); + if (fs_read(fp, &tag_version, 1) != 1) { + fs_seek(fp, 0, FS_SEEK_SET); + return -EIO; + } + + if (tag_version != TAG_FORMAT_VERSION) { + fs_seek(fp, 0, FS_SEEK_SET); + return -ENOTSUP; + } + + *has_tag = true; + *audio_limit = (size_t)file_size - tag_len; + *payload_len = tag_len - TAG_FOOTER_V1_LEN; + + fs_seek(fp, 0, FS_SEEK_SET); + return 0; +} + +ssize_t fs_get_audio_data_len(struct fs_file_t *fp) { off_t file_size; + size_t audio_limit = 0U; + size_t payload_len = 0U; + bool has_tag = false; fs_seek(fp, 0, FS_SEEK_END); file_size = fs_tell(fp); - fs_seek(fp, 0, FS_SEEK_SET); - if (file_size < 6) return file_size; - - fs_seek(fp, -6, FS_SEEK_END); - if (fs_read(fp, footer, 6) == 6) { - if (memcmp(&footer[2], "TAG!", 4) == 0) { - uint16_t tag_len = footer[0] | (footer[1] << 8); - if (tag_len <= file_size) { - fs_seek(fp, 0, FS_SEEK_SET); - return file_size - tag_len; - } - } + if (file_size < 0) { + fs_seek(fp, 0, FS_SEEK_SET); + return -EIO; } + + if (fs_get_tag_bounds(fp, file_size, &audio_limit, &payload_len, &has_tag) < 0) { + fs_seek(fp, 0, FS_SEEK_SET); + return -EIO; + } + fs_seek(fp, 0, FS_SEEK_SET); - return file_size; + return has_tag ? (ssize_t)audio_limit : file_size; } ssize_t fs_read_audio(struct fs_file_t *fp, void *buffer, size_t len, size_t audio_limit) { @@ -206,88 +284,85 @@ ssize_t fs_read_audio(struct fs_file_t *fp, void *buffer, size_t len, size_t aud return fs_read(fp, buffer, to_read); } -int fs_write_hex_tag(struct fs_file_t *fp, const char *hex_str) { - size_t hex_len = strlen(hex_str); - - // Ein Hex-String muss eine gerade Anzahl an Zeichen haben - if (hex_len % 2 != 0) return -EINVAL; - - size_t payload_len = hex_len / 2; - uint16_t total_footer_len = (uint16_t)(payload_len + 6); - - // 1. Audio-Ende bestimmen und dorthin seeken - size_t audio_limit = fs_get_audio_data_len(fp); - fs_seek(fp, audio_limit, FS_SEEK_SET); - - // 2. Payload Byte für Byte konvertieren und schreiben - for (size_t i = 0; i < hex_len; i += 2) { - int high = hex2int(hex_str[i]); - int low = hex2int(hex_str[i+1]); - - if (high < 0 || low < 0) return -EINVAL; // Ungültiges Hex-Zeichen - - uint8_t byte = (uint8_t)((high << 4) | low); - fs_write(fp, &byte, 1); - } - - // 3. Die 2 Bytes Länge schreiben (Little Endian) - uint8_t len_bytes[2]; - len_bytes[0] = (uint8_t)(total_footer_len & 0xFF); - len_bytes[1] = (uint8_t)((total_footer_len >> 8) & 0xFF); - fs_write(fp, len_bytes, 2); - - // 4. Magic Bytes schreiben - fs_write(fp, "TAG!", 4); - - // 5. Datei am aktuellen Punkt abschneiden - off_t current_pos = fs_tell(fp); - return fs_truncate(fp, current_pos); -} - -int fs_read_hex_tag(struct fs_file_t *fp, char *hex_str, size_t hex_str_size) { - if (hex_str == NULL || hex_str_size == 0) { +int fs_tag_open_read(struct fs_file_t *fp, uint8_t *version, size_t *payload_len) +{ + if (fp == NULL || version == NULL || payload_len == NULL) { return -EINVAL; } - hex_str[0] = '\0'; - - // Dateigröße ermitteln fs_seek(fp, 0, FS_SEEK_END); off_t file_size = fs_tell(fp); + size_t audio_limit = 0U; + size_t payload_size = 0U; + bool has_tag = false; - // Audio-Limit finden (Anfang des Payloads) - size_t audio_limit = fs_get_audio_data_len(fp); - - // Prüfen, ob überhaupt ein Tag existiert (audio_limit < file_size) - if (audio_limit >= file_size) { - // Kein Tag vorhanden -> leerer String - return 0; + if (file_size < 0) { + return -EIO; } - // Die Payload-Länge ist: Gesamtgröße - Audio - 6 Bytes (Länge + Magic) - size_t payload_len = file_size - audio_limit - 6; - - if ((payload_len * 2U) + 1U > hex_str_size) { - return -ENOMEM; // Nicht genug Platz im Zielpuffer + int rc = fs_get_tag_bounds(fp, file_size, &audio_limit, &payload_size, &has_tag); + if (rc < 0) { + return rc; + } + + if (!has_tag) { + return -ENOENT; } - // Zum Anfang des Payloads springen fs_seek(fp, audio_limit, FS_SEEK_SET); + *version = TAG_FORMAT_VERSION; + *payload_len = payload_size; + return 0; +} - uint8_t byte; - for (size_t i = 0; i < payload_len; i++) { - if (fs_read(fp, &byte, 1) != 1) { - return -EIO; - } +ssize_t fs_tag_read_chunk(struct fs_file_t *fp, void *buffer, size_t len) +{ + return fs_read(fp, buffer, len); +} - // Jedes Byte als zwei Hex-Zeichen in den Zielpuffer schreiben - hex_str[i * 2] = int2hex(byte >> 4); - hex_str[i * 2 + 1] = int2hex(byte & 0x0F); +int fs_tag_open_write(struct fs_file_t *fp) +{ + ssize_t audio_limit = fs_get_audio_data_len(fp); + if (audio_limit < 0) { + return (int)audio_limit; + } + fs_seek(fp, audio_limit, FS_SEEK_SET); + return 0; +} + +ssize_t fs_tag_write_chunk(struct fs_file_t *fp, const void *buffer, size_t len) +{ + return fs_write(fp, buffer, len); +} + +int fs_tag_finish_write(struct fs_file_t *fp, uint8_t version, size_t payload_len) +{ + if (version != TAG_FORMAT_VERSION) { + return -ENOTSUP; } - hex_str[payload_len * 2] = '\0'; + size_t total_footer_len = payload_len + TAG_FOOTER_V1_LEN; + if (total_footer_len > UINT16_MAX) { + return -EFBIG; + } - return 0; + if (fs_write(fp, &version, 1) != 1) { + return -EIO; + } + + uint8_t len_bytes[2]; + len_bytes[0] = (uint8_t)(total_footer_len & 0xFFU); + len_bytes[1] = (uint8_t)((total_footer_len >> 8) & 0xFFU); + if (fs_write(fp, len_bytes, sizeof(len_bytes)) != sizeof(len_bytes)) { + return -EIO; + } + + if (fs_write(fp, TAG_MAGIC, TAG_MAGIC_LEN) != TAG_MAGIC_LEN) { + return -EIO; + } + + off_t current_pos = fs_tell(fp); + return fs_truncate(fp, current_pos); } int flash_get_slot_info(slot_info_t *info) { diff --git a/firmware/src/fs.h b/firmware/src/fs.h index ebcda92..936f933 100644 --- a/firmware/src/fs.h +++ b/firmware/src/fs.h @@ -74,6 +74,15 @@ int fs_pm_unlink(const char *path); */ int fs_pm_statvfs(const char *path, struct fs_statvfs *stat); +/** + * @brief Wrapper around fs_stat that handles power management for the flash + * Resumes the flash before stat and suspends it afterwards + * @param path Path to file or directory + * @param entry Pointer to fs_dirent structure to receive metadata + * @return 0 on success, negative error code on failure + */ +int fs_pm_stat(const char *path, struct fs_dirent *entry); + /** * @brief Wrapper around fs_mkdir that handles power management for the flash * Resumes the flash before creating the directory and suspends it afterwards @@ -82,6 +91,15 @@ int fs_pm_statvfs(const char *path, struct fs_statvfs *stat); */ int fs_pm_mkdir(const char *path); +/** + * @brief Wrapper around fs_rename that handles power management for the flash + * Resumes the flash before renaming and suspends it afterwards + * @param old_path Current path of the file or directory + * @param new_path New path for the file or directory + * @return 0 on success, negative error code on failure + */ +int fs_pm_rename(const char *old_path, const char *new_path); + /** * @brief Gets the length of the audio data in a file, accounting for any metadata tags * @param fp Pointer to an open fs_file_t structure representing the audio file @@ -100,21 +118,47 @@ int fs_get_audio_data_len(struct fs_file_t *fp); int fs_read_audio(struct fs_file_t *fp, void *buffer, size_t len, size_t audio_limit); /** - * @brief Writes a hexadecimal string as a metadata tag at the end of an audio file + * @brief Positions file pointer at start of tag payload if tags exist. * @param fp Pointer to an open fs_file_t structure representing the audio file - * @param hex_str Null-terminated string containing hexadecimal characters (0-9, a-f, A-F) - * @return 0 on success, negative error code on failure + * @param version Pointer to receive tag format version + * @param payload_len Pointer to receive tag payload length in bytes + * @return 0 on success, -ENOENT if no tags exist, negative error code on failure */ -int fs_write_hex_tag(struct fs_file_t *fp, const char *hex_str); +int fs_tag_open_read(struct fs_file_t *fp, uint8_t *version, size_t *payload_len); /** - * @brief Reads a hexadecimal string from a metadata tag at the end of an audio file + * @brief Reads a chunk from current tag payload position. + * @param fp Pointer to an open fs_file_t positioned in tag payload + * @param buffer Destination buffer + * @param len Maximum bytes to read + * @return Number of bytes read, 0 at payload end, negative error code on failure + */ +ssize_t fs_tag_read_chunk(struct fs_file_t *fp, void *buffer, size_t len); + +/** + * @brief Positions file pointer for tag payload overwrite at end of audio data. * @param fp Pointer to an open fs_file_t structure representing the audio file - * @param hex_str Buffer to be filled with the hexadecimal string (must be large enough to hold the data) - * @param hex_str_size Size of the hex_str buffer * @return 0 on success, negative error code on failure */ -int fs_read_hex_tag(struct fs_file_t *fp, char *hex_str, size_t hex_str_size); +int fs_tag_open_write(struct fs_file_t *fp); + +/** + * @brief Writes a raw tag payload chunk. + * @param fp Pointer to an open fs_file_t positioned for tag payload write + * @param buffer Source buffer + * @param len Number of bytes to write + * @return Number of bytes written, negative error code on failure + */ +ssize_t fs_tag_write_chunk(struct fs_file_t *fp, const void *buffer, size_t len); + +/** + * @brief Finalizes tags by appending version + footer and truncating file. + * @param fp Pointer to an open fs_file_t structure representing the audio file + * @param version Tag format version to write + * @param payload_len Tag payload length in bytes + * @return 0 on success, negative error code on failure + */ +int fs_tag_finish_write(struct fs_file_t *fp, uint8_t version, size_t payload_len); /** * @brief Retrieves information about the firmware slot, such as start address and size diff --git a/firmware/src/protocol.c b/firmware/src/protocol.c index 44285bd..5094f7e 100644 --- a/firmware/src/protocol.c +++ b/firmware/src/protocol.c @@ -6,45 +6,126 @@ #include #include #include +#include #include -#include #include #include #include +#include -#define PROTOCOL_VERSION 2 +#define PROTOCOL_VERSION 1 LOG_MODULE_REGISTER(protocol, LOG_LEVEL_INF); -#define PROTOCOL_STACK_SIZE 2048 +#define PROTOCOL_STACK_SIZE 3072 #define PROTOCOL_PRIORITY 6 -#define BUFFER_SIZE 256 +#define HEADER_SIZE 14U +#define HEADER_NO_SYNC_CRC_SIZE 8U +#define MAX_RX_PAYLOAD_SIZE 256U +#define MAX_TX_PAYLOAD_SIZE 1024U +#define PROTOCOL_RC_STREAMING_DONE 1 +#define LIST_ENTRY_TYPE_FILE 0U +#define LIST_ENTRY_TYPE_DIR 1U +#define STAT_ENTRY_TYPE_FILE 0U +#define STAT_ENTRY_TYPE_DIR 1U +#define TAG_UPLOAD_VERSION 0x01U +#define FW_STATUS_CONFIRMED 1U +#define FW_STATUS_TESTING 2U +#define FW_STATUS_PENDING 3U -static uint8_t buffer[BUFFER_SIZE]; -static volatile uint32_t rx_index = 0; +static const uint8_t sync_pattern[4] = { 'B', 'U', 'Z', 'Z' }; -static protocol_state_t current_protocol_state = PS_WAITING_FOR_COMMAND; -static protocol_cmd_t current_command = CMD_INVALID; +static protocol_state_t rx_state = PS_WAIT_SYNC; +static uint8_t sync_index = 0; -void send_ok() +static uint8_t header_buf[HEADER_SIZE]; +static size_t header_index = 0; + +static uint8_t payload_buf[MAX_RX_PAYLOAD_SIZE]; +static uint32_t payload_index = 0; + +static uint8_t payload_crc_buf[4]; +static uint8_t payload_crc_index = 0; + +static uint8_t current_frame_type; +static uint8_t current_command_id; +static uint16_t current_sequence; +static uint32_t current_payload_len; +static uint8_t tx_payload[MAX_TX_PAYLOAD_SIZE]; +static uint8_t tx_header_buf[HEADER_SIZE]; +static uint8_t tx_header_crc_input[HEADER_NO_SYNC_CRC_SIZE]; +static uint8_t tx_payload_crc_le[4]; +static bool tag_upload_active; +static size_t tag_upload_expected_len; +static size_t tag_upload_received_len; +static struct fs_file_t tag_upload_file; +static bool tag_upload_file_open; + +static void protocol_tag_upload_reset(void) { - const char *response = "OK\n"; - LOG_DBG("Sending response: OK"); - usb_write_buffer((const uint8_t *)response, strlen(response)); + if (tag_upload_file_open) { + fs_pm_close(&tag_upload_file); + } + tag_upload_file_open = false; + tag_upload_active = false; + tag_upload_expected_len = 0U; + tag_upload_received_len = 0U; +} + +static void protocol_reset_rx_state(void) +{ + rx_state = PS_WAIT_SYNC; + sync_index = 0; + header_index = 0; + payload_index = 0; + payload_crc_index = 0; + current_frame_type = 0; + current_command_id = 0; + current_sequence = 0; + current_payload_len = 0; +} + +static void protocol_send_frame(uint8_t frame_type, uint8_t command_id, uint16_t sequence, + const uint8_t *payload, uint32_t payload_len) +{ + memcpy(&tx_header_buf[0], sync_pattern, sizeof(sync_pattern)); + tx_header_buf[4] = frame_type; + tx_header_buf[5] = command_id; + sys_put_le16(sequence, &tx_header_buf[6]); + sys_put_le32(payload_len, &tx_header_buf[8]); + + memcpy(tx_header_crc_input, &tx_header_buf[4], HEADER_NO_SYNC_CRC_SIZE); + uint16_t header_crc = crc16_ccitt(0xFFFFU, tx_header_crc_input, sizeof(tx_header_crc_input)); + sys_put_le16(header_crc, &tx_header_buf[12]); + + usb_write_buffer(tx_header_buf, sizeof(tx_header_buf)); + + if (payload_len > 0U && payload != NULL) { + usb_write_buffer(payload, payload_len); + } + + uint32_t payload_crc = crc32_ieee_update(0U, payload, payload_len); + sys_put_le32(payload_crc, tx_payload_crc_le); + usb_write_buffer(tx_payload_crc_le, sizeof(tx_payload_crc_le)); +} + +static void protocol_send_error(uint8_t command_id, uint16_t sequence, protocol_error_t error_code) +{ + tx_payload[0] = (uint8_t)error_code; + tx_payload[1] = 0U; + protocol_send_frame(FRAME_RESP_ERROR, command_id, sequence, tx_payload, 2U); } static protocol_error_t protocol_map_error(int32_t rc) { - if (rc == 0) - { + if (rc == 0) { return P_ERR_NONE; } - int32_t err = rc < 0 ? -rc : rc; + int32_t err = (rc < 0) ? -rc : rc; - switch (err) - { + switch (err) { case ENOENT: return P_ERR_FILE_NOT_FOUND; case EEXIST: @@ -57,6 +138,7 @@ static protocol_error_t protocol_map_error(int32_t rc) case EPERM: return P_ERR_ACCESS_DENIED; case ENOSPC: + case ENOMEM: return P_ERR_NO_SPACE; case EFBIG: return P_ERR_FILE_TOO_LARGE; @@ -84,696 +166,1025 @@ static protocol_error_t protocol_map_error(int32_t rc) } } -void send_error(protocol_error_t error_code) +static int protocol_extract_path(const uint8_t *rx_payload, uint32_t rx_len, char *path, size_t path_size) { - char response[32]; - snprintf(response, sizeof(response), "ERR %d\n", error_code); - LOG_DBG("Sending response: ERR %d", error_code); - usb_write_buffer((const uint8_t *)response, strlen(response)); + if (rx_payload == NULL || path == NULL || path_size == 0U) { + return -EINVAL; + } + + if (rx_len < 1U) { + return -EINVAL; + } + + uint8_t path_len = rx_payload[0]; + if (path_len == 0U) { + return -EINVAL; + } + + if ((uint32_t)path_len != (rx_len - 1U)) { + return -EINVAL; + } + + if ((size_t)path_len > PROTOCOL_MAX_PATH_LEN) { + return -EMSGSIZE; + } + + if ((size_t)path_len >= path_size) { + return -EMSGSIZE; + } + + memcpy(path, &rx_payload[1], path_len); + path[path_len] = '\0'; + return 0; } -int cmd_ls(const char *path) +static int protocol_extract_two_paths(const uint8_t *rx_payload, uint32_t rx_len, + char *old_path, size_t old_path_size, + char *new_path, size_t new_path_size) +{ + if (rx_payload == NULL || old_path == NULL || new_path == NULL || + old_path_size == 0U || new_path_size == 0U) { + return -EINVAL; + } + + if (rx_len < 3U) { + return -EINVAL; + } + + uint8_t old_len = rx_payload[0]; + if (old_len == 0U || old_len > PROTOCOL_MAX_PATH_LEN) { + return -EINVAL; + } + if (rx_len < (uint32_t)(1U + old_len + 1U)) { + return -EINVAL; + } + + uint8_t new_len = rx_payload[1U + old_len]; + if (new_len == 0U || new_len > PROTOCOL_MAX_PATH_LEN) { + return -EINVAL; + } + + if (rx_len != (uint32_t)(1U + old_len + 1U + new_len)) { + return -EINVAL; + } + + if ((size_t)old_len >= old_path_size || (size_t)new_len >= new_path_size) { + return -EMSGSIZE; + } + + memcpy(old_path, &rx_payload[1], old_len); + old_path[old_len] = '\0'; + memcpy(new_path, &rx_payload[1U + old_len + 1U], new_len); + new_path[new_len] = '\0'; + return 0; +} + +static int protocol_join_path(char *out_path, size_t out_path_size, + const char *base_path, const char *name) +{ + if (out_path == NULL || out_path_size == 0U || base_path == NULL || name == NULL) { + return -EINVAL; + } + + size_t base_len = strlen(base_path); + size_t name_len = strlen(name); + + if (base_len == 1U && base_path[0] == '/') { + if ((1U + name_len + 1U) > out_path_size) { + return -EMSGSIZE; + } + out_path[0] = '/'; + memcpy(&out_path[1], name, name_len); + out_path[1U + name_len] = '\0'; + return 0; + } + + bool has_trailing_slash = (base_len > 1U && base_path[base_len - 1U] == '/'); + size_t separator_len = has_trailing_slash ? 0U : 1U; + + if ((base_len + separator_len + name_len + 1U) > out_path_size) { + return -EMSGSIZE; + } + + memcpy(out_path, base_path, base_len); + size_t offset = base_len; + if (!has_trailing_slash) { + out_path[offset++] = '/'; + } + memcpy(&out_path[offset], name, name_len); + out_path[offset + name_len] = '\0'; + return 0; +} + +static int protocol_remove_recursive_impl(const char *path) { struct fs_dir_t dirp; struct fs_dirent entry; - const char *ls_path = (path == NULL || path[0] == '\0') ? "/" : path; fs_dir_t_init(&dirp); - if (fs_pm_opendir(&dirp, ls_path) < 0) - { - LOG_ERR("Failed to open directory '%s'", ls_path); - return -ENOENT; + int rc = fs_pm_opendir(&dirp, path); + if (rc == -ENOTDIR) { + return fs_pm_unlink(path); + } + if (rc != 0) { + return rc; } - char tx_buffer[300]; - while (fs_readdir(&dirp, &entry) == 0 && entry.name[0] != '\0') - { - snprintf(tx_buffer, sizeof(tx_buffer), "%s,%u,%s\n", entry.type == FS_DIR_ENTRY_FILE ? "F" : "D", entry.size, entry.name); - usb_write_buffer((const uint8_t *)tx_buffer, strlen(tx_buffer)); + while ((rc = fs_readdir(&dirp, &entry)) == 0 && entry.name[0] != '\0') { + char child_path[PROTOCOL_MAX_PATH_LEN + 1U]; + rc = protocol_join_path(child_path, sizeof(child_path), path, entry.name); + if (rc != 0) { + (void)fs_pm_closedir(&dirp); + return rc; + } + + if (entry.type == FS_DIR_ENTRY_DIR) { + rc = protocol_remove_recursive_impl(child_path); + } else { + rc = fs_pm_unlink(child_path); + } + + if (rc != 0) { + (void)fs_pm_closedir(&dirp); + return rc; + } + } + + int close_rc = fs_pm_closedir(&dirp); + if (rc != 0) { + return rc; + } + if (close_rc != 0) { + return close_rc; + } + + return fs_pm_unlink(path); +} + +static int protocol_handle_get_protocol_version(uint8_t *tx_payload, uint32_t *tx_len) +{ + sys_put_le16(PROTOCOL_VERSION, tx_payload); + *tx_len = 2U; + return 0; +} + +static int protocol_handle_get_firmware_status(uint8_t *tx_payload, uint32_t *tx_len) +{ + size_t version_len = strlen(APP_VERSION_STRING); + if (version_len > 255U) { + return -EOVERFLOW; + } + + size_t required = 2U + version_len; + if (required > MAX_TX_PAYLOAD_SIZE) { + return -ENOMEM; + } + + int swap_type = mcuboot_swap_type(); + if (swap_type < 0) { + return swap_type; + } + + uint8_t fw_status = FW_STATUS_CONFIRMED; + if (!boot_is_img_confirmed()) { + fw_status = FW_STATUS_TESTING; + } else if (swap_type == BOOT_SWAP_TYPE_TEST || swap_type == BOOT_SWAP_TYPE_PERM) { + fw_status = FW_STATUS_PENDING; + } + + tx_payload[0] = fw_status; + tx_payload[1] = (uint8_t)version_len; + memcpy(&tx_payload[2], APP_VERSION_STRING, version_len); + + *tx_len = (uint32_t)required; + return 0; +} + +static int protocol_handle_confirm_firmware(void) +{ + return boot_write_img_confirmed(); +} + +static int protocol_handle_get_flash_status(uint8_t *tx_payload, uint32_t *tx_len) +{ + struct fs_statvfs stat; + int rc = fs_pm_statvfs("/lfs", &stat); + if (rc != 0) { + return rc; + } + + sys_put_le32((uint32_t)stat.f_frsize, &tx_payload[0]); + sys_put_le32((uint32_t)stat.f_blocks, &tx_payload[4]); + sys_put_le32((uint32_t)stat.f_bfree, &tx_payload[8]); + sys_put_le32((uint32_t)PROTOCOL_MAX_PATH_LEN, &tx_payload[12]); + *tx_len = 16U; + return 0; +} + +static int protocol_handle_list_dir(const uint8_t *rx_payload, uint32_t rx_len) +{ + char path[PROTOCOL_MAX_PATH_LEN + 1U]; + int rc = protocol_extract_path(rx_payload, rx_len, path, sizeof(path)); + if (rc != 0) { + return rc; + } + + struct fs_dir_t dirp; + struct fs_dirent entry; + fs_dir_t_init(&dirp); + + rc = fs_pm_opendir(&dirp, path); + if (rc != 0) { + return rc; + } + + sys_put_le32(0xFFFFFFFFU, tx_payload); + protocol_send_frame(FRAME_RESP_STREAM_START, current_command_id, current_sequence, + tx_payload, 4U); + + while ((rc = fs_readdir(&dirp, &entry)) == 0 && entry.name[0] != '\0') { + size_t name_len = strlen(entry.name); + if (name_len > 255U) { + fs_pm_closedir(&dirp); + return -EMSGSIZE; + } + + tx_payload[0] = (entry.type == FS_DIR_ENTRY_FILE) ? LIST_ENTRY_TYPE_FILE : LIST_ENTRY_TYPE_DIR; + tx_payload[1] = (uint8_t)name_len; + + uint32_t size_u32 = (entry.size > UINT32_MAX) ? UINT32_MAX : (uint32_t)entry.size; + sys_put_le32(size_u32, &tx_payload[2]); + memcpy(&tx_payload[6], entry.name, name_len); + + protocol_send_frame(FRAME_RESP_STREAM_CHUNK, current_command_id, current_sequence, + tx_payload, (uint32_t)(6U + name_len)); } fs_pm_closedir(&dirp); - return 0; -} -int cmd_info() -{ - char info[112]; - struct fs_statvfs stat; - int rc = fs_pm_statvfs("/lfs", &stat); - if (rc) - { - LOG_ERR("Failed to get filesystem stats: %d", rc); + if (rc < 0) { return rc; } - snprintf(info, sizeof(info), "%u;%s;%lu;%lu;%lu;%s\n", PROTOCOL_VERSION, APP_VERSION_STRING, stat.f_frsize, stat.f_blocks, stat.f_bfree, boot_is_img_confirmed() ? "CONFIRMED" : "UNCONFIRMED"); - usb_write_buffer((const uint8_t *)info, strlen(info)); + + protocol_send_frame(FRAME_RESP_STREAM_END, current_command_id, current_sequence, NULL, 0U); + return PROTOCOL_RC_STREAMING_DONE; +} + +static int protocol_handle_check_file_crc(const uint8_t *rx_payload, uint32_t rx_len, uint8_t *out_payload, uint32_t *out_len) +{ + char path[PROTOCOL_MAX_PATH_LEN + 1U]; + int rc = protocol_extract_path(rx_payload, rx_len, path, sizeof(path)); + if (rc != 0) { + return rc; + } + + struct fs_file_t file; + fs_file_t_init(&file); + rc = fs_pm_open(&file, path, FS_O_READ); + if (rc != 0) { + return rc; + } + + ssize_t audio_len = fs_get_audio_data_len(&file); + if (audio_len < 0) { + fs_pm_close(&file); + return (int)audio_len; + } + + uint32_t crc32 = 0U; + ssize_t read; + while ((read = fs_read_audio(&file, payload_buf, sizeof(payload_buf), (size_t)audio_len)) > 0) { + crc32 = crc32_ieee_update(crc32, payload_buf, (size_t)read); + } + + fs_pm_close(&file); + + if (read < 0) { + return (int)read; + } + + sys_put_le32(crc32, out_payload); + *out_len = 4U; return 0; } -int cmd_put_binary_file(const char *filename, ssize_t filesize, uint32_t expected_crc32) +static int protocol_handle_mkdir(const uint8_t *rx_payload, uint32_t rx_len) { - int rc; - ssize_t bytes_written = 0; - uint32_t running_crc32 = 0; - uint32_t retry_count = 0; - size_t accumulated = 0; + char path[PROTOCOL_MAX_PATH_LEN + 1U]; + int rc = protocol_extract_path(rx_payload, rx_len, path, sizeof(path)); + if (rc != 0) { + return rc; + } + + return fs_pm_mkdir(path); +} + +static int protocol_handle_rm(const uint8_t *rx_payload, uint32_t rx_len) +{ + char path[PROTOCOL_MAX_PATH_LEN + 1U]; + int rc = protocol_extract_path(rx_payload, rx_len, path, sizeof(path)); + if (rc != 0) { + return rc; + } + + return fs_pm_unlink(path); +} + +static int protocol_handle_stat(const uint8_t *rx_payload, uint32_t rx_len, uint8_t *out_payload, uint32_t *out_len) +{ + char path[PROTOCOL_MAX_PATH_LEN + 1U]; + int rc = protocol_extract_path(rx_payload, rx_len, path, sizeof(path)); + if (rc != 0) { + return rc; + } + + struct fs_dirent entry; + rc = fs_pm_stat(path, &entry); + if (rc != 0) { + return rc; + } + + out_payload[0] = (entry.type == FS_DIR_ENTRY_DIR) ? STAT_ENTRY_TYPE_DIR : STAT_ENTRY_TYPE_FILE; + uint32_t size_u32 = (entry.size > UINT32_MAX) ? UINT32_MAX : (uint32_t)entry.size; + sys_put_le32(size_u32, &out_payload[1]); + *out_len = 5U; + return 0; +} + +static int protocol_handle_rename(const uint8_t *rx_payload, uint32_t rx_len) +{ + char old_path[PROTOCOL_MAX_PATH_LEN + 1U]; + char new_path[PROTOCOL_MAX_PATH_LEN + 1U]; + int rc = protocol_extract_two_paths(rx_payload, rx_len, + old_path, sizeof(old_path), + new_path, sizeof(new_path)); + if (rc != 0) { + return rc; + } + + return fs_pm_rename(old_path, new_path); +} + +static int protocol_handle_rm_recursive(const uint8_t *rx_payload, uint32_t rx_len) +{ + char path[PROTOCOL_MAX_PATH_LEN + 1U]; + int rc = protocol_extract_path(rx_payload, rx_len, path, sizeof(path)); + if (rc != 0) { + return rc; + } + + if (strcmp(path, "/") == 0) { + return -EACCES; + } + + return protocol_remove_recursive_impl(path); +} + +static int protocol_handle_get_file(const uint8_t *rx_payload, uint32_t rx_len) +{ + char path[PROTOCOL_MAX_PATH_LEN + 1U]; + int rc = protocol_extract_path(rx_payload, rx_len, path, sizeof(path)); + if (rc != 0) { + return rc; + } + + struct fs_dirent entry; + rc = fs_pm_stat(path, &entry); + if (rc != 0) { + return rc; + } + if (entry.type == FS_DIR_ENTRY_DIR) { + return -EISDIR; + } + struct fs_file_t file; - bool firmware_update = false; - - if (strcmp(filename, "update.bin") == 0 || - strcmp(filename, "firmware.bin") == 0 || - strcmp(filename, "update") == 0 || - strcmp(filename, "firmware") == 0) - { - firmware_update = true; - LOG_INF("Firmware update requested with file '%s'", filename); + fs_file_t_init(&file); + rc = fs_pm_open(&file, path, FS_O_READ); + if (rc != 0) { + return rc; } - if (firmware_update) - { - slot_info_t slot1_info; - rc = flash_get_slot_info(&slot1_info); - if (rc < 0) - { - LOG_ERR("Failed to get slot 1 info: %d", rc); - return rc; - } - if (filesize > slot1_info.size) - { - LOG_ERR("File size %zd exceeds slot 1 size %zu", filesize, slot1_info.size); - return -EFBIG; - } - flash_init_firmware_upload(); - } - else - { - fs_file_t_init(&file); - fs_pm_unlink(filename); - LOG_DBG("Opening file '%s' for writing (expected size: %zd bytes, expected CRC32: 0x%08x)", filename, filesize, expected_crc32); - rc = fs_pm_open(&file, filename, FS_O_CREATE | FS_O_WRITE); - if (rc < 0) - { - LOG_ERR("Failed to open file '%s' for writing: %d", filename, rc); - return rc; - } - } - usb_write_buffer((const uint8_t *)"READY\n", 6); + uint32_t total_size = (entry.size > UINT32_MAX) ? UINT32_MAX : (uint32_t)entry.size; + sys_put_le32(total_size, tx_payload); + protocol_send_frame(FRAME_RESP_DATA, current_command_id, current_sequence, + tx_payload, 4U); - uint32_t start = k_uptime_get_32(); - while (bytes_written < filesize) - { - /* Nur so viel lesen, wie in den restlichen Puffer passt (oder bis zum Dateiende) */ - size_t remaining_file = filesize - bytes_written - accumulated; - size_t to_read = MIN(sizeof(buffer) - accumulated, remaining_file); + uint32_t running_crc = 0U; + size_t bytes_sent = 0U; - ssize_t read = usb_read_buffer(buffer + accumulated, to_read); - - if (read < 0) - { - LOG_ERR("Error reading from USB: %d", read); - if (firmware_update) - { - } - else - { - fs_pm_close(&file); - } + while (bytes_sent < total_size) { + size_t want = MIN((size_t)512U, (size_t)(total_size - bytes_sent)); + ssize_t read = fs_read(&file, tx_payload, want); + if (read < 0) { + fs_pm_close(&file); return (int)read; } - else if (read == 0) - { - if (retry_count >= 10) - { - LOG_ERR("No data received from USB after multiple attempts"); - if (firmware_update) - { - } - else - { - fs_pm_close(&file); - } + if (read == 0) { + fs_pm_close(&file); + return -EIO; + } + + usb_write_buffer(tx_payload, (size_t)read); + running_crc = crc32_ieee_update(running_crc, tx_payload, (size_t)read); + bytes_sent += (size_t)read; + } + + rc = fs_pm_close(&file); + if (rc != 0) { + return rc; + } + + sys_put_le32(running_crc, tx_payload); + protocol_send_frame(FRAME_RESP_DATA, current_command_id, current_sequence, + tx_payload, 4U); + return PROTOCOL_RC_STREAMING_DONE; +} + +static int protocol_handle_put_file_start(const uint8_t *rx_payload, uint32_t rx_len) +{ + if (rx_len < 9U) { + return -EINVAL; + } + + uint8_t path_len = rx_payload[0]; + if (path_len == 0U || path_len > PROTOCOL_MAX_PATH_LEN) { + return -EINVAL; + } + + if (rx_len != (uint32_t)(1U + path_len + 8U)) { + return -EINVAL; + } + + char path[PROTOCOL_MAX_PATH_LEN + 1U]; + memcpy(path, &rx_payload[1], path_len); + path[path_len] = '\0'; + + size_t expected_len = (size_t)sys_get_le32(&rx_payload[1U + path_len]); + uint32_t expected_crc = sys_get_le32(&rx_payload[1U + path_len + 4U]); + + struct fs_file_t file; + fs_file_t_init(&file); + int rc = fs_pm_open(&file, path, FS_O_CREATE | FS_O_WRITE | FS_O_TRUNC); + if (rc != 0) { + return rc; + } + + size_t bytes_written = 0U; + size_t accumulated = 0U; + uint32_t running_crc = 0U; + uint32_t retry_count = 0U; + + while (bytes_written < expected_len) { + size_t remaining_file = expected_len - bytes_written - accumulated; + size_t to_read = MIN(sizeof(payload_buf) - accumulated, remaining_file); + + ssize_t read = usb_read_buffer(payload_buf + accumulated, to_read); + if (read < 0) { + fs_pm_close(&file); + (void)fs_pm_unlink(path); + return (int)read; + } + + if (read == 0) { + if (retry_count >= 10U) { + fs_pm_close(&file); + (void)fs_pm_unlink(path); return -ETIMEDOUT; } usb_resume_rx(); - if ((bytes_written + accumulated) == 0) - { - usb_wait_for_data(K_SECONDS(1)); - } - else - { - usb_wait_for_data(K_MSEC(100)); + if ((bytes_written + accumulated) == 0U) { + (void)usb_wait_for_data(K_SECONDS(1)); + } else { + (void)usb_wait_for_data(K_MSEC(100)); } retry_count++; continue; } - /* Wir haben Daten bekommen: Zähler hochsetzen und USB weiterlauschen lassen */ - accumulated += read; - retry_count = 0; + accumulated += (size_t)read; + retry_count = 0U; usb_resume_rx(); - /* SCHREIBEN: Erst auf den Flash schreiben, wenn der Puffer voll ist (4096 Bytes) - ODER wenn wir das Ende der Datei erreicht haben. */ - if (accumulated == sizeof(buffer) || (bytes_written + accumulated) == filesize) - { - if (firmware_update) - { - int rc = flash_write_firmware_block(buffer, accumulated, (bytes_written + accumulated) == filesize); - if (rc < 0) - { - LOG_ERR("Error writing to flash: %d", rc); - return rc; - } + if (accumulated == sizeof(payload_buf) || (bytes_written + accumulated) == expected_len) { + ssize_t written = fs_write(&file, payload_buf, accumulated); + if (written < 0) { + fs_pm_close(&file); + (void)fs_pm_unlink(path); + return (int)written; } - else - { - ssize_t written = fs_write(&file, buffer, accumulated); - if (written < 0) - { - LOG_ERR("Error writing to file '%s': %d", filename, (int)written); - fs_pm_close(&file); - return (int)written; - } + + if ((size_t)written != accumulated) { + fs_pm_close(&file); + (void)fs_pm_unlink(path); + return -EIO; } - /* CRC erst nach dem erfolgreichen Block-Schreiben berechnen */ - running_crc32 = crc32_ieee_update(running_crc32, buffer, accumulated); + + running_crc = crc32_ieee_update(running_crc, payload_buf, accumulated); bytes_written += accumulated; - - /* Puffer für die nächste Runde leeren */ - accumulated = 0; + accumulated = 0U; } } - uint32_t duration = k_uptime_get_32() - start; - uint32_t kb_per_s = (filesize * 1000) / (duration * 1024 + 1); - LOG_DBG("Received file '%s' (%zd bytes) in %u ms (%u kb/s), CRC32: 0x%08x", filename, filesize, duration, kb_per_s, running_crc32); - if (firmware_update) - { - int rc; - rc = boot_request_upgrade(BOOT_UPGRADE_TEST); - if (rc < 0) - { - LOG_ERR("Failed to request firmware upgrade: %d", rc); - return rc; - } - send_ok(); - LOG_INF("Firmware upgrade requested, rebooting into bootloader..."); - reboot_with_status(REBOOT_STATUS_FIRMWARE_UPDATE); - } - else - { - fs_pm_close(&file); - LOG_DBG("Closed file '%s' after writing", filename); + rc = fs_pm_close(&file); + if (rc != 0) { + (void)fs_pm_unlink(path); + return rc; } - if (running_crc32 != expected_crc32) - { - LOG_ERR("CRC32 mismatch for file '%s': expected 0x%08x, got 0x%08x", filename, expected_crc32, running_crc32); + if (running_crc != expected_crc) { + (void)fs_pm_unlink(path); return -EBADMSG; } - LOG_DBG("File '%s' received successfully with matching CRC32", filename); + return 0; } -int cmd_mkdir(const char *path) +static int protocol_handle_put_fw_start(const uint8_t *rx_payload, uint32_t rx_len) { - int rc = fs_pm_mkdir(path); - if (rc < 0) - { - LOG_ERR("Failed to create directory '%s': %d", path, rc); + if (rx_len != 8U) { + return -EINVAL; } - LOG_DBG("Directory '%s' created successfully", path); + + size_t expected_len = (size_t)sys_get_le32(&rx_payload[0]); + uint32_t expected_crc = sys_get_le32(&rx_payload[4]); + + slot_info_t slot1_info; + int rc = flash_get_slot_info(&slot1_info); + if (rc < 0) { + return rc; + } + + if (expected_len > slot1_info.size) { + return -EFBIG; + } + + rc = flash_init_firmware_upload(); + if (rc < 0) { + return rc; + } + + size_t bytes_written = 0U; + size_t accumulated = 0U; + uint32_t running_crc = 0U; + uint32_t retry_count = 0U; + + while (bytes_written < expected_len) { + size_t remaining_file = expected_len - bytes_written - accumulated; + size_t to_read = MIN(sizeof(payload_buf) - accumulated, remaining_file); + + ssize_t read = usb_read_buffer(payload_buf + accumulated, to_read); + if (read < 0) { + return (int)read; + } + + if (read == 0) { + if (retry_count >= 10U) { + return -ETIMEDOUT; + } + + usb_resume_rx(); + if ((bytes_written + accumulated) == 0U) { + (void)usb_wait_for_data(K_SECONDS(1)); + } else { + (void)usb_wait_for_data(K_MSEC(100)); + } + retry_count++; + continue; + } + + accumulated += (size_t)read; + retry_count = 0U; + usb_resume_rx(); + + if (accumulated == sizeof(payload_buf) || (bytes_written + accumulated) == expected_len) { + bool is_last = ((bytes_written + accumulated) == expected_len); + rc = flash_write_firmware_block(payload_buf, accumulated, is_last); + if (rc < 0) { + return rc; + } + + running_crc = crc32_ieee_update(running_crc, payload_buf, accumulated); + bytes_written += accumulated; + accumulated = 0U; + } + } + + if (running_crc != expected_crc) { + return -EBADMSG; + } + + rc = boot_request_upgrade(BOOT_UPGRADE_TEST); + if (rc < 0) { + return rc; + } + + return 0; +} + +static int protocol_handle_put_file_chunk(const uint8_t *rx_payload, uint32_t rx_len) +{ + ARG_UNUSED(rx_payload); + ARG_UNUSED(rx_len); + return -ENOTSUP; +} + +static int protocol_handle_put_file_end(void) +{ + return -ENOTSUP; +} + +static int protocol_handle_get_tag_blob(const uint8_t *rx_payload, uint32_t rx_len) +{ + char path[PROTOCOL_MAX_PATH_LEN + 1U]; + int rc = protocol_extract_path(rx_payload, rx_len, path, sizeof(path)); + if (rc != 0) { + return rc; + } + + struct fs_file_t file; + fs_file_t_init(&file); + rc = fs_pm_open(&file, path, FS_O_READ); + if (rc != 0) { + return rc; + } + + uint8_t tag_version = 0U; + size_t blob_len = 0U; + rc = fs_tag_open_read(&file, &tag_version, &blob_len); + if (rc == -ENOENT) { + blob_len = 0U; + } else if (rc != 0) { + fs_pm_close(&file); + return rc; + } + + sys_put_le32((uint32_t)blob_len, tx_payload); + protocol_send_frame(FRAME_RESP_STREAM_START, current_command_id, current_sequence, + tx_payload, 4U); + + if (blob_len > 0U) { + size_t remaining = blob_len; + + while (remaining > 0U) { + size_t want = MIN((size_t)192U, remaining); + ssize_t read = fs_tag_read_chunk(&file, tx_payload, want); + if (read < 0) { + fs_pm_close(&file); + return (int)read; + } + if (read == 0) { + fs_pm_close(&file); + return -EIO; + } + + protocol_send_frame(FRAME_RESP_STREAM_CHUNK, current_command_id, current_sequence, + tx_payload, (uint32_t)read); + remaining -= (size_t)read; + } + } + + fs_pm_close(&file); + + protocol_send_frame(FRAME_RESP_STREAM_END, current_command_id, current_sequence, NULL, 0U); + return PROTOCOL_RC_STREAMING_DONE; +} + +static int protocol_handle_set_tag_blob_start(const uint8_t *rx_payload, uint32_t rx_len) +{ + if (rx_len < 3U) { + return -EINVAL; + } + + uint8_t path_len = rx_payload[0]; + if (path_len == 0U || path_len > PROTOCOL_MAX_PATH_LEN) { + return -EINVAL; + } + + if (rx_len != (uint32_t)(1U + path_len + 2U)) { + return -EINVAL; + } + + if (tag_upload_active) { + protocol_tag_upload_reset(); + } + + size_t total_len = (size_t)sys_get_le16(&rx_payload[1U + path_len]); + + char path[PROTOCOL_MAX_PATH_LEN + 1U]; + memcpy(path, &rx_payload[1], path_len); + path[path_len] = '\0'; + + fs_file_t_init(&tag_upload_file); + int rc = fs_pm_open(&tag_upload_file, path, FS_O_READ | FS_O_WRITE); + if (rc != 0) { + return rc; + } + + rc = fs_tag_open_write(&tag_upload_file); + if (rc != 0) { + fs_pm_close(&tag_upload_file); + return rc; + } + + tag_upload_file_open = true; + tag_upload_expected_len = total_len; + tag_upload_received_len = 0U; + tag_upload_active = true; + return 0; +} + +static int protocol_handle_set_tag_blob_chunk(const uint8_t *rx_payload, uint32_t rx_len) +{ + if (!tag_upload_active) { + return -EINVAL; + } + + if ((tag_upload_received_len + rx_len) > tag_upload_expected_len) { + protocol_tag_upload_reset(); + return -EINVAL; + } + + if (rx_len > 0U) { + ssize_t written = fs_tag_write_chunk(&tag_upload_file, rx_payload, rx_len); + if (written < 0) { + protocol_tag_upload_reset(); + return (int)written; + } + if ((uint32_t)written != rx_len) { + protocol_tag_upload_reset(); + return -EIO; + } + tag_upload_received_len += rx_len; + } + + return 0; +} + +static int protocol_handle_set_tag_blob_end(void) +{ + if (!tag_upload_active) { + return -EINVAL; + } + + if (tag_upload_received_len != tag_upload_expected_len) { + protocol_tag_upload_reset(); + return -EINVAL; + } + + int rc = fs_tag_finish_write(&tag_upload_file, TAG_UPLOAD_VERSION, tag_upload_expected_len); + protocol_tag_upload_reset(); return rc; } -int cmd_rm(const char *path) +static void protocol_dispatch_request(void) { - int rc = fs_pm_unlink(path); - if (rc < 0) - { - LOG_ERR("Failed to remove '%s': %d", path, rc); + if (current_frame_type != FRAME_REQ) { + protocol_send_error(current_command_id, current_sequence, P_ERR_INVALID_PARAMETERS); + return; } - LOG_DBG("'%s' removed successfully", 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); + uint32_t tx_len = 0U; + int rc = 0; - 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()) - { - int rc = boot_write_img_confirmed(); - if (rc < 0) - { - LOG_ERR("Failed to confirm firmware: %d", rc); - return rc; - } - LOG_INF("Firmware confirmed successfully"); - send_ok(); - audio_play("/lfs/sys/confirm"); - } - else - { - LOG_INF("Firmware is already confirmed, no action taken"); - } - return 0; -} - -int cmd_reboot_device() -{ - LOG_INF("Rebooting device as requested by host..."); - send_ok(); - reboot_with_status(REBOOT_STATUS_NORMAL); - return 0; // Dieser Code wird nie erreicht, aber wir geben ihn der Vollständigkeit halber zurück -} - -void cmd_play(const char *filename) -{ - LOG_DBG("Play command received with filename: '%s'", filename); - audio_stop(); - audio_play(filename); -} - -int cmd_check(const char *param) -{ - LOG_DBG("Check command received with parameter: '%s'", param); - 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("Check failed: file '%s' not found", param); - return -ENOENT; - } - uint32_t crc32 = 0; - uint32_t start_time = k_uptime_get_32(); - uint8_t buffer[256]; - ssize_t read; - 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); - } - fs_pm_close(&file); - if (read < 0) - { - LOG_ERR("Check failed: error reading file '%s': %d", param, (int)read); - return (int)read; - } - 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), "0x%08x\n", crc32); - usb_write_buffer((const uint8_t *)response, strlen(response)); - return 0; -} - -void execute_current_command(void) -{ - int rc; - switch (current_command) - { - case CMD_LS: - LOG_DBG("Executing LS command with parameters: '%s'", buffer); - rc = cmd_ls((char *)buffer); - if (rc == 0) - { - send_ok(); - } - else - { - send_error(protocol_map_error(rc)); + switch (current_command_id) { + case CMD_GET_PROTOCOL_VERSION: + if (current_payload_len != 0U) { + protocol_send_error(current_command_id, current_sequence, P_ERR_INVALID_PARAMETERS); + return; } + rc = protocol_handle_get_protocol_version(tx_payload, &tx_len); break; - case CMD_INFO: - if (buffer[0] != '\0') - { - LOG_WRN("INFO command received with unexpected parameters: '%s'", buffer); - } - LOG_DBG("Executing INFO command"); - rc = cmd_info(); - if (rc == 0) - { - send_ok(); - } - else - { - send_error(protocol_map_error(rc)); - } - break; - case CMD_PUT_BINARY_FILE: - char filename[128]; - ssize_t filesize; - uint32_t crc32; - rc = sscanf((char *)buffer, "%127[^;];%zd;%i", filename, &filesize, &crc32); - if (rc != 3) - { - LOG_ERR("Invalid parameters for PUT_BINARY_FILE command (got %d): '%s'", rc, buffer); - send_error(P_ERR_INVALID_PARAMETERS); - break; - } - LOG_DBG("Executing PUT_BINARY_FILE command filename: '%s', filesize: %zd, crc32: 0x%08x", filename, filesize, crc32); - rc = cmd_put_binary_file(filename, filesize, crc32); - if (rc == 0) - { - send_ok(); - audio_refresh_file_count(); // Nach erfolgreichem Upload die Anzahl der verfügbaren Audiodateien aktualisieren - } - else - { - usb_flush_rx(); - send_error(protocol_map_error(rc)); + case CMD_GET_FIRMWARE_STATUS: + if (current_payload_len != 0U) { + protocol_send_error(current_command_id, current_sequence, P_ERR_INVALID_PARAMETERS); + return; } + rc = protocol_handle_get_firmware_status(tx_payload, &tx_len); break; - case CMD_MKDIR: - LOG_DBG("Executing MKDIR command with parameters: '%s'", buffer); - rc = cmd_mkdir((char *)buffer); - if (rc == 0) - { - send_ok(); - } - else - { - send_error(protocol_map_error(rc)); + + case CMD_GET_FLASH_STATUS: + if (current_payload_len != 0U) { + protocol_send_error(current_command_id, current_sequence, P_ERR_INVALID_PARAMETERS); + return; } + rc = protocol_handle_get_flash_status(tx_payload, &tx_len); break; - case CMD_RM: - LOG_DBG("Executing RM command with parameters: '%s'", buffer); - rc = cmd_rm((char *)buffer); - if (rc == 0) - { - send_ok(); - audio_refresh_file_count(); // Nach erfolgreichem Löschen die Anzahl der verfügbaren Audiodateien aktualisieren - } - else - { - send_error(protocol_map_error(rc)); + + case CMD_CONFIRM_FIRMWARE: + if (current_payload_len != 0U) { + protocol_send_error(current_command_id, current_sequence, P_ERR_INVALID_PARAMETERS); + return; } + rc = protocol_handle_confirm_firmware(); + tx_len = 0U; break; - case CMD_CONFIRM: - LOG_DBG("Executing CONFIRM command"); - rc = cmd_confirm_firmware(); - if (rc != 0) - { - send_error(protocol_map_error(rc)); - break; - } - send_ok(); - break; + case CMD_REBOOT: - LOG_DBG("Executing REBOOT command"); - rc = cmd_reboot_device(); - if (rc != 0) - { - send_error(protocol_map_error(rc)); + if (current_payload_len != 0U) { + protocol_send_error(current_command_id, current_sequence, P_ERR_INVALID_PARAMETERS); + return; } + protocol_send_frame(FRAME_RESP_ACK, current_command_id, current_sequence, NULL, 0U); + k_sleep(K_MSEC(20)); + reboot_with_status(REBOOT_STATUS_NORMAL); + return; + + case CMD_LIST_DIR: + rc = protocol_handle_list_dir(payload_buf, current_payload_len); break; - case CMD_PLAY: - LOG_DBG("Executing PLAY command"); - cmd_play((char *)buffer); - send_ok(); + + case CMD_CHECK_FILE_CRC: + rc = protocol_handle_check_file_crc(payload_buf, current_payload_len, tx_payload, &tx_len); break; - case CMD_CHECK: - LOG_DBG("Executing CHECK command"); - rc = cmd_check((char *)buffer); - if (rc == 0) - { - send_ok(); - } - else - { - send_error(protocol_map_error(rc)); - } + + case CMD_MKDIR: + rc = protocol_handle_mkdir(payload_buf, current_payload_len); + tx_len = 0U; 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)); - } + + case CMD_RM: + rc = protocol_handle_rm(payload_buf, current_payload_len); + tx_len = 0U; 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)); - } + + case CMD_STAT: + rc = protocol_handle_stat(payload_buf, current_payload_len, tx_payload, &tx_len); break; + + case CMD_RENAME: + rc = protocol_handle_rename(payload_buf, current_payload_len); + tx_len = 0U; + break; + + case CMD_RM_R: + rc = protocol_handle_rm_recursive(payload_buf, current_payload_len); + tx_len = 0U; + break; + + case CMD_GET_FILE: + rc = protocol_handle_get_file(payload_buf, current_payload_len); + break; + + case CMD_PUT_FILE_START: + rc = protocol_handle_put_file_start(payload_buf, current_payload_len); + tx_len = 0U; + break; + + case CMD_PUT_FILE_CHUNK: + rc = protocol_handle_put_file_chunk(payload_buf, current_payload_len); + if (rc != 0) { + protocol_send_error(current_command_id, current_sequence, protocol_map_error(rc)); + } + return; + + case CMD_PUT_FILE_END: + rc = protocol_handle_put_file_end(); + tx_len = 0U; + break; + + case CMD_PUT_FW_START: + rc = protocol_handle_put_fw_start(payload_buf, current_payload_len); + tx_len = 0U; + break; + + case CMD_GET_TAG_BLOB: + rc = protocol_handle_get_tag_blob(payload_buf, current_payload_len); + break; + + case CMD_SET_TAG_BLOB_START: + rc = protocol_handle_set_tag_blob_start(payload_buf, current_payload_len); + tx_len = 0U; + break; + + case CMD_SET_TAG_BLOB_CHUNK: + rc = protocol_handle_set_tag_blob_chunk(payload_buf, current_payload_len); + tx_len = 0U; + break; + + case CMD_SET_TAG_BLOB_END: + rc = protocol_handle_set_tag_blob_end(); + tx_len = 0U; + break; + default: - LOG_ERR("No execution logic for command %d", current_command); - send_error(P_ERR_NOT_SUPPORTED); + protocol_send_error(current_command_id, current_sequence, P_ERR_INVALID_COMMAND); + return; + } + + if (rc == PROTOCOL_RC_STREAMING_DONE) { + return; + } + + if (rc != 0) { + protocol_send_error(current_command_id, current_sequence, protocol_map_error(rc)); + return; + } + + if (tx_len == 0U) { + protocol_send_frame(FRAME_RESP_ACK, current_command_id, current_sequence, NULL, 0U); + } else { + protocol_send_frame(FRAME_RESP_DATA, current_command_id, current_sequence, tx_payload, tx_len); + } +} + +static void protocol_process_rx_byte(uint8_t byte) +{ + switch (rx_state) { + case PS_WAIT_SYNC: + if (byte == sync_pattern[sync_index]) { + sync_index++; + if (sync_index == sizeof(sync_pattern)) { + memcpy(header_buf, sync_pattern, sizeof(sync_pattern)); + header_index = sizeof(sync_pattern); + rx_state = PS_READ_HEADER; + LOG_DBG("Sync pattern detected, switching to header reception"); + } + } else { + sync_index = (byte == sync_pattern[0]) ? 1U : 0U; + } break; - } -} -protocol_state_t waiting_for_command(uint8_t byte) -{ - if (byte < 'a' || byte > 'z') - { - LOG_DBG("Ignoring non-command byte: 0x%02x", byte); // Nur aktivieren, wenn nötig! - rx_index = 0; - return PS_WAITING_FOR_COMMAND; - } - buffer[rx_index++] = byte; - return PS_READING_COMMAND; -} + case PS_READ_HEADER: + header_buf[header_index++] = byte; + if (header_index == HEADER_SIZE) { + uint16_t rx_header_crc = sys_get_le16(&header_buf[12]); + uint16_t calc_header_crc = crc16_ccitt(0xFFFFU, &header_buf[4], HEADER_NO_SYNC_CRC_SIZE); -protocol_state_t reading_command(uint8_t byte) -{ - if (byte == ' ' || byte == '\n' || byte == '\r') - { - buffer[rx_index] = '\0'; - rx_index = 0; + if (rx_header_crc != calc_header_crc) { + LOG_WRN("Invalid header CRC: got 0x%04x expected 0x%04x", rx_header_crc, calc_header_crc); + protocol_reset_rx_state(); + break; + } - if (strcmp((char *)buffer, "ls") == 0) - { - LOG_DBG("Received LS command"); - current_command = CMD_LS; - } - else if (strcmp((char *)buffer, "info") == 0) - { - LOG_DBG("Received INFO command"); - current_command = CMD_INFO; - } - else if (strcmp((char *)buffer, "put") == 0) - { - LOG_DBG("Received PUT_BINARY_FILE command"); - current_command = CMD_PUT_BINARY_FILE; - } - else if (strcmp((char *)buffer, "mkdir") == 0) - { - LOG_DBG("Received MKDIR command"); - current_command = CMD_MKDIR; - } - else if (strcmp((char *)buffer, "rm") == 0) - { - LOG_DBG("Received RM command"); - current_command = CMD_RM; - } - else if (strcmp((char *)buffer, "confirm") == 0) - { - LOG_DBG("Received CONFIRM command"); - current_command = CMD_CONFIRM; - } - else if (strcmp((char *)buffer, "reboot") == 0) - { - LOG_DBG("Received REBOOT command"); - current_command = CMD_REBOOT; - } - else if (strcmp((char *)buffer, "play") == 0) - { - LOG_DBG("Received PLAY command"); - current_command = CMD_PLAY; - } - else if (strcmp((char *)buffer, "check") == 0) - { - 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 - { - LOG_DBG("Unknown command: %s", buffer); - current_command = CMD_INVALID; - send_error(P_ERR_INVALID_COMMAND); - if (byte != '\n' && byte != '\r') - return PS_WAITING_FOR_END_OF_LINE; - return PS_WAITING_FOR_COMMAND; - } + current_frame_type = header_buf[4]; + current_command_id = header_buf[5]; + current_sequence = sys_get_le16(&header_buf[6]); + current_payload_len = sys_get_le32(&header_buf[8]); - if (byte == ' ') - { - rx_index = 0; - return PS_READING_PARAMETERS; - } - else - { - buffer[0] = '\0'; - rx_index = 0; - execute_current_command(); - return PS_WAITING_FOR_COMMAND; - } - } - else - { - if (rx_index < BUFFER_SIZE - 1) - { - buffer[rx_index++] = byte; - } - else - { - send_error(P_ERR_COMMAND_TOO_LONG); - return PS_WAITING_FOR_END_OF_LINE; - } - } - return PS_READING_COMMAND; -} + if (current_payload_len > MAX_RX_PAYLOAD_SIZE) { + protocol_send_error(current_command_id, current_sequence, P_ERR_COMMAND_TOO_LONG); + protocol_reset_rx_state(); + LOG_ERR("Payload length %u exceeds maximum of %u", current_payload_len, MAX_RX_PAYLOAD_SIZE); + break; + } -protocol_state_t reading_parameters(uint8_t byte) -{ - if (byte == '\n' || byte == '\r') - { - buffer[rx_index] = '\0'; - rx_index = 0; - execute_current_command(); - return PS_WAITING_FOR_COMMAND; - } - else - { - buffer[rx_index++] = byte; - if (rx_index >= BUFFER_SIZE) - { - rx_index = 0; - send_error(P_ERR_COMMAND_TOO_LONG); - return PS_WAITING_FOR_COMMAND; + payload_index = 0; + payload_crc_index = 0; + rx_state = (current_payload_len == 0U) ? PS_READ_PAYLOAD_CRC : PS_READ_PAYLOAD; } - return PS_READING_PARAMETERS; - } -} + break; -protocol_state_t waiting_for_end_of_line(uint8_t byte) -{ - if (byte == '\n' || byte == '\r') - { - return PS_WAITING_FOR_COMMAND; - } - else - { - return PS_WAITING_FOR_END_OF_LINE; + case PS_READ_PAYLOAD: + payload_buf[payload_index++] = byte; + if (payload_index == current_payload_len) { + rx_state = PS_READ_PAYLOAD_CRC; + } + break; + + case PS_READ_PAYLOAD_CRC: + payload_crc_buf[payload_crc_index++] = byte; + if (payload_crc_index == sizeof(payload_crc_buf)) { + uint32_t rx_crc = sys_get_le32(payload_crc_buf); + uint32_t calc_crc = crc32_ieee_update(0U, payload_buf, current_payload_len); + + if (rx_crc != calc_crc) { + LOG_WRN("Invalid payload CRC: got 0x%08x expected 0x%08x", rx_crc, calc_crc); + protocol_send_error(current_command_id, current_sequence, P_ERR_CRC_MISMATCH); + protocol_reset_rx_state(); + break; + } + + protocol_dispatch_request(); + protocol_reset_rx_state(); + } + break; + + default: + protocol_reset_rx_state(); + break; } } void protocol_thread_entry(void *p1, void *p2, void *p3) { - uint8_t rx_byte; + ARG_UNUSED(p1); + ARG_UNUSED(p2); + ARG_UNUSED(p3); - LOG_DBG("Protocol thread started, waiting for data..."); + LOG_INF("Protocol thread started"); + + protocol_reset_rx_state(); - while (1) - { - /* 1. Thread schläft, bis der USB-Interrupt triggert */ - if (usb_wait_for_data(K_FOREVER)) - { + while (1) { + if (!usb_wait_for_data(K_FOREVER)) { + continue; + } - while (usb_read_char(&rx_byte) > 0) - { - switch (current_protocol_state) - { - case PS_WAITING_FOR_COMMAND: - current_protocol_state = waiting_for_command(rx_byte); - break; - case PS_READING_COMMAND: - current_protocol_state = reading_command(rx_byte); - break; - case PS_READING_PARAMETERS: - current_protocol_state = reading_parameters(rx_byte); - break; - case PS_WAITING_FOR_END_OF_LINE: - current_protocol_state = waiting_for_end_of_line(rx_byte); - break; - default: - LOG_ERR("Invalid protocol state: %d", current_protocol_state); - current_protocol_state = PS_WAITING_FOR_COMMAND; - break; - } - } - - usb_resume_rx(); + uint8_t rx_byte; + while (usb_read_char(&rx_byte) > 0) { + protocol_process_rx_byte(rx_byte); } } } diff --git a/firmware/src/protocol.h b/firmware/src/protocol.h index 602f927..acc3684 100644 --- a/firmware/src/protocol.h +++ b/firmware/src/protocol.h @@ -1,29 +1,53 @@ #ifndef PROTOCOL_H #define PROTOCOL_H +#include +#include + +#define PROTOCOL_MAX_PATH_LEN 32U + typedef enum { - PS_WAITING_FOR_COMMAND, - PS_READING_COMMAND, - PS_READING_PARAMETERS, - PS_WAITING_FOR_END_OF_LINE, + PS_WAIT_SYNC = 0, + PS_READ_HEADER, + PS_READ_PAYLOAD, + PS_READ_PAYLOAD_CRC, } protocol_state_t; typedef enum { CMD_INVALID = 0, - CMD_INFO, - CMD_LS, - CMD_PUT_BINARY_FILE, - CMD_MKDIR, - CMD_RM, - CMD_CONFIRM, - CMD_REBOOT, - CMD_PLAY, - CMD_SET_TAG, - CMD_GET_TAG, - CMD_CHECK, - /* Weitere Kommandos folgen hier */ + CMD_GET_PROTOCOL_VERSION = 0x00, + CMD_GET_FIRMWARE_STATUS = 0x01, + CMD_GET_FLASH_STATUS = 0x02, + CMD_CONFIRM_FIRMWARE = 0x03, + CMD_REBOOT = 0x04, + CMD_LIST_DIR = 0x10, + CMD_CHECK_FILE_CRC = 0x11, + CMD_MKDIR = 0x12, + CMD_RM = 0x13, + CMD_PUT_FILE_START = 0x14, + CMD_PUT_FILE_CHUNK = 0x15, + CMD_PUT_FILE_END = 0x16, + CMD_PUT_FW_START = 0x17, + CMD_STAT = 0x18, + CMD_RENAME = 0x19, + CMD_RM_R = 0x1A, + CMD_GET_FILE = 0x1B, + CMD_GET_TAG_BLOB = 0x20, + CMD_SET_TAG_BLOB_START = 0x21, + CMD_SET_TAG_BLOB_CHUNK = 0x22, + CMD_SET_TAG_BLOB_END = 0x23, } protocol_cmd_t; +typedef enum { + FRAME_REQ = 0x01, + FRAME_RESP_ACK = 0x10, + FRAME_RESP_DATA = 0x11, + FRAME_RESP_STREAM_START = 0x12, + FRAME_RESP_STREAM_CHUNK = 0x13, + FRAME_RESP_STREAM_END = 0x14, + FRAME_RESP_ERROR = 0x7F, +} protocol_frame_type_t; + typedef enum { P_ERR_NONE = 0x00, P_ERR_INVALID_COMMAND = 0x01, @@ -47,4 +71,7 @@ typedef enum { P_ERR_BUSY = 0x31, P_ERR_INTERNAL = 0x32, } protocol_error_t; + +void protocol_thread_entry(void *p1, void *p2, void *p3); + #endif // PROTOCOL_H \ No newline at end of file diff --git a/firmware/src/utils.c b/firmware/src/utils.c index 011703f..ccd75b4 100644 --- a/firmware/src/utils.c +++ b/firmware/src/utils.c @@ -51,15 +51,3 @@ uint8_t get_reboot_status(void) } return status; } - -int hex2int(char c) { - if (c >= '0' && c <= '9') return c - '0'; - if (c >= 'a' && c <= 'f') return c - 'a' + 10; - if (c >= 'A' && c <= 'F') return c - 'A' + 10; - return -1; // Fehlerhaftes Zeichen -} - -char int2hex(uint8_t i) { - if (i < 10) return '0' + i; - return 'a' + (i - 10); -} \ No newline at end of file diff --git a/firmware/src/utils.h b/firmware/src/utils.h index bb1fe27..dce07aa 100644 --- a/firmware/src/utils.h +++ b/firmware/src/utils.h @@ -22,18 +22,4 @@ void reboot_with_status(uint8_t status_code); */ uint8_t get_reboot_status(); -/** - * @brief Converts a hexadecimal character to its integer value. - * @param c The hexadecimal character (0-9, a-f, A-F) to convert. - * @return The integer value of the hexadecimal character, or -1 if the character is not a valid hexadecimal digit. - */ -int hex2int(char c); - -/** - * @brief Converts an integer value to its hexadecimal character representation. - * @param i The integer value to convert (0-15). - * @return The hexadecimal character representation of the integer value. - */ -char int2hex(uint8_t i); - #endif // UTILS_H diff --git a/x b/x new file mode 100644 index 0000000..e69de29