diff --git a/buzzer_tool/buzzer.py b/buzzer_tool/buzzer.py deleted file mode 100644 index 18c2623..0000000 --- a/buzzer_tool/buzzer.py +++ /dev/null @@ -1,217 +0,0 @@ -# buzzer.py -import argparse -import sys - -def main(): - parser = argparse.ArgumentParser(description="Edis Buzzer Host Tool") - - # Globale Argumente (gelten für alle Befehle) - 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") - - # Befehl: info (expliziter Aufruf, obwohl es ohnehin immer angezeigt wird) - subparsers.add_parser("info", help="Zeigt nur die Systeminformationen an") - - # Befehl: ls - ls_parser = subparsers.add_parser("ls", help="Listet Dateien und Verzeichnisse auf") - ls_parser.add_argument("path", nargs="?", default="/", help="Zielpfad (Standard: /)") - ls_parser.add_argument("-r", "--recursive", action="store_true", help="Rekursiv auflisten") - - # 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), 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") - mkdir_parser.add_argument("path", type=str, help="Pfad des neuen Verzeichnisses (z.B. /lfs/a/neu)") - - # Befehl: rm - rm_parser = subparsers.add_parser("rm", help="Löscht eine Datei oder ein Verzeichnis") - 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)") - - # Befehl: check - check_parser = subparsers.add_parser("check", help="Holt die CRC32 einer Datei und zeigt sie an") - check_parser.add_argument("path", type=str, help="Pfad der zu prüfenden Datei (z.B. /lfs/a/neu)") - - # Befehl: confirm - confirm_parser = subparsers.add_parser("confirm", help="Bestätigt die aktuell laufende Firmware") - - # Befehl: reboot - reboot_parser = subparsers.add_parser("reboot", help="Startet den Buzzer 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(f"Port: {config.get('port', 'Nicht definiert')}") - print(f"Baudrate: {config.get('baudrate')}") - print(f"Timeout: {config.get('timeout')}s") - 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: - 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: - print(" (Leer)") - else: - ls.print_tree(tree, path=args.path ) - elif args.command == "put": - 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: - 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_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 - - except TimeoutError as e: - print(f"Fehler: {e}") - sys.exit(1) - except BuzzerError as e: - print(f"Buzzer hat die Aktion abgelehnt: {e}") - sys.exit(1) - except Exception as e: - print(f"Verbindungsfehler auf {config.get('port')}: {e}") - sys.exit(1) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/buzzer_tool/config.yaml b/buzzer_tool/config.yaml deleted file mode 100644 index 0a207a6..0000000 --- a/buzzer_tool/config.yaml +++ /dev/null @@ -1,5 +0,0 @@ -# config.yaml -serial: - port: "/dev/cu.usbmodem83401" - baudrate: 2500000 - timeout: 1 diff --git a/buzzer_tool/config.yaml.example b/buzzer_tool/config.yaml.example deleted file mode 100644 index 4dc466b..0000000 --- a/buzzer_tool/config.yaml.example +++ /dev/null @@ -1,7 +0,0 @@ -# config.yaml -serial: - port: "COM17" - baudrate: 250000 - 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 deleted file mode 100644 index a93419c..0000000 --- a/buzzer_tool/core/commands/check.py +++ /dev/null @@ -1,60 +0,0 @@ -# core/commands/check.py -from core.connection import BuzzerError - -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.") - - 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, - "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 deleted file mode 100644 index 28d53ac..0000000 --- a/buzzer_tool/core/commands/confirm.py +++ /dev/null @@ -1,11 +0,0 @@ -# core/commands/confirm.py -from core.connection import BuzzerError - - -def execute(conn): - """Bestätigt die aktuell laufende Firmware per Binary-Protokoll.""" - try: - 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 deleted file mode 100644 index 5e0a5ed..0000000 --- a/buzzer_tool/core/commands/fw_put.py +++ /dev/null @@ -1,57 +0,0 @@ -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/info.py b/buzzer_tool/core/commands/info.py deleted file mode 100644 index 993edd8..0000000 --- a/buzzer_tool/core/commands/info.py +++ /dev/null @@ -1,37 +0,0 @@ -# core/commands/info.py -from core.connection import BuzzerError - -def execute(conn) -> dict: - """Holt die Systeminformationen und gibt sie als strukturiertes Dictionary zurück.""" - protocol_version = conn.get_protocol_version() - if protocol_version != 1: - raise BuzzerError(f"Inkompatibles Protokoll: Gerät nutzt v{protocol_version}, Host erwartet v1.") - - status_code, app_version = conn.get_firmware_status() - flash = conn.get_flash_status() - - f_frsize = flash["block_size"] - f_blocks = flash["total_blocks"] - f_bfree = flash["free_blocks"] - - 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 - used_kb = total_kb - free_kb - percent_used = (used_kb / total_kb) * 100 if total_kb > 0 else 0 - - return { - "protocol_version": protocol_version, - "app_version": app_version, - "total_kb": total_kb, - "free_kb": free_kb, - "used_kb": used_kb, - "percent_used": percent_used, - "image_status": image_status - } \ No newline at end of file diff --git a/buzzer_tool/core/commands/ls.py b/buzzer_tool/core/commands/ls.py deleted file mode 100644 index cc33d8d..0000000 --- a/buzzer_tool/core/commands/ls.py +++ /dev/null @@ -1,84 +0,0 @@ -# core/commands/ls.py -from core.connection import BuzzerError - -def get_file_tree(conn, target_path="/", recursive=False) -> list: - """ - Liest das Dateisystem aus und gibt eine hierarchische Baumstruktur zurück. - """ - if not target_path.endswith('/'): - target_path += '/' - - cmd_path = target_path.rstrip('/') if target_path != '/' else '/' - - try: - lines = conn.list_directory(cmd_path) - except BuzzerError as e: - return [{"type": "E", "name": f"Fehler beim Lesen: {e}", "path": target_path}] - - nodes = [] - if not lines: - return nodes - - for line in lines: - parts = line.split(',', 2) - if len(parts) != 3: - continue - - entry_type, entry_size, entry_name = parts - node = { - "type": entry_type, - "name": entry_name, - "path": f"{target_path}{entry_name}" - } - - if entry_type == 'D': - if recursive: - # Rekursiver Aufruf auf dem Host für Unterverzeichnisse - node["children"] = get_file_tree(conn, f"{target_path}{entry_name}/", recursive=True) - else: - node["children"] = [] - elif entry_type == 'F': - node["size"] = int(entry_size) - - nodes.append(node) - - return nodes - -def print_tree(nodes, prefix="", path=""): - """ - Gibt die Baumstruktur optisch formatiert auf der Konsole aus. - """ - if path: - if path == "/": - display_path = "💾 "+"/ (Root)" - else: - display_path = "📁 " + path - print(f"{prefix}{display_path}") - for i, node in enumerate(nodes): - is_last = (i == len(nodes) - 1) - connector = " └─" if is_last else " ├─" - - if node["type"] == 'D': - print(f"{prefix}{connector}📁 {node['name']}") - extension = " " if is_last else " │ " - if "children" in node and node["children"]: - print_tree(node["children"], prefix + extension) - elif node["type"] == 'F': - size_kb = node["size"] / 1024 - # \033[90m macht den Text dunkelgrau, \033[0m setzt die Farbe zurück - print(f"{prefix}{connector}📄 {node['name']} \033[90m({size_kb:.1f} KB)\033[0m") - elif node["type"] == 'E': - print(f"{prefix}{connector}❌ \033[31m{node['name']}\033[0m") - -def get_flat_file_list(nodes) -> list: - """ - Wandelt die Baumstruktur in eine flache Liste von Dateipfaden um. - Wird von 'rm -r' benötigt, um nacheinander alle Dateien zu löschen. - """ - flat_list = [] - for node in nodes: - if node["type"] == 'F': - flat_list.append(node) - elif node["type"] == 'D' and "children" in node: - flat_list.extend(get_flat_file_list(node["children"])) - return flat_list \ No newline at end of file diff --git a/buzzer_tool/core/commands/mkdir.py b/buzzer_tool/core/commands/mkdir.py deleted file mode 100644 index 4873404..0000000 --- a/buzzer_tool/core/commands/mkdir.py +++ /dev/null @@ -1,10 +0,0 @@ -# core/commands/mkdir.py -from core.connection import BuzzerError - -def execute(conn, path: str): - """Erstellt ein Verzeichnis auf dem Controller.""" - try: - 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 deleted file mode 100644 index 4e2b300..0000000 --- a/buzzer_tool/core/commands/mv.py +++ /dev/null @@ -1,10 +0,0 @@ -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/play.py b/buzzer_tool/core/commands/play.py deleted file mode 100644 index b1c6f0b..0000000 --- a/buzzer_tool/core/commands/play.py +++ /dev/null @@ -1,10 +0,0 @@ -# core/commands/mkdir.py -from core.connection import BuzzerError - -def execute(conn, path: str): - """Spielt eine Datei auf dem Controller ab.""" - try: - conn.send_command(f"play {path}") - print(f"▶️ Datei '{path}' wird abgespielt.") - except BuzzerError as e: - print(f"❌ Fehler beim Abspielen von '{path}': {e}") \ No newline at end of file diff --git a/buzzer_tool/core/commands/pull.py b/buzzer_tool/core/commands/pull.py deleted file mode 100644 index 7cf0e82..0000000 --- a/buzzer_tool/core/commands/pull.py +++ /dev/null @@ -1,62 +0,0 @@ -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 deleted file mode 100644 index 8c171d8..0000000 --- a/buzzer_tool/core/commands/put.py +++ /dev/null @@ -1,220 +0,0 @@ -import glob -import os -import sys -import time - -from core.connection import BuzzerError - -TAG_MAGIC = b"TAG!" -TAG_FOOTER_LEN = 7 -TAG_VERSION_V1 = 0x01 -TAG_TYPE_CRC32 = 0x10 - - -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(item["local"]) for item in uploads) - sent_all = 0 - start_time_all = time.monotonic() - last_ui_update = start_time_all - - 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() - - def progress_handler(chunk_len, sent_file, total_file): - nonlocal sent_all, last_ui_update - sent_all += chunk_len - - 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}" - - 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" - ) - sys.stdout.flush() - - try: - 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.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 deleted file mode 100644 index b03e5de..0000000 --- a/buzzer_tool/core/commands/reboot.py +++ /dev/null @@ -1,11 +0,0 @@ -# core/commands/reboot.py -from core.connection import BuzzerError - - -def execute(conn): - """Startet den Buzzer per Binary-Protokoll neu.""" - try: - 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 deleted file mode 100644 index 17c9089..0000000 --- a/buzzer_tool/core/commands/rm.py +++ /dev/null @@ -1,68 +0,0 @@ -# core/commands/rm.py -import fnmatch -import posixpath -from core.connection import BuzzerError -from core.commands.ls import get_file_tree - -def _delete_recursive(conn, nodes): - """Löscht Knoten Bottom-Up (erst Dateien/Unterordner, dann den Ordner selbst)""" - for node in nodes: - if node["type"] == 'D': - if "children" in node and node["children"]: - _delete_recursive(conn, node["children"]) - _try_rm(conn, node["path"], is_dir=True) - elif node["type"] == 'F': - _try_rm(conn, node["path"], is_dir=False) - -def _try_rm(conn, path, is_dir=False): - icon = "📁" if is_dir else "📄" - try: - conn.rm(path) - print(f" 🗑️ {icon} Gelöscht: {path}") - except BuzzerError as e: - print(f" ❌ Fehler bei {path}: {e}") - -def execute(conn, path: str, recursive: bool = False): - """Löscht eine Datei, ein Verzeichnis oder löst Wildcards (*) auf.""" - - # 1. Wildcard-Behandlung (z.B. /lfs/a/* oder *.wav) - if '*' in path or '?' in path: - dirname, pattern = posixpath.split(path) - if not dirname: - dirname = "/" - - print(f"Suche nach Dateien passend zu '{pattern}' in '{dirname}'...") - tree = get_file_tree(conn, target_path=dirname, recursive=False) - - # Fehler beim Verzeichnis-Lesen abfangen - if len(tree) == 1 and tree[0].get("type") == "E": - print(f"❌ Verzeichnis '{dirname}' nicht gefunden.") - return - - # Filtern mit fnmatch (funktioniert wie in der Linux-Shell) - matches = [node for node in tree if node.get("type") == "F" and fnmatch.fnmatch(node["name"], pattern)] - - if not matches: - print(f"Keine passenden Dateien für '{path}' gefunden.") - return - - for match in matches: - _try_rm(conn, match["path"], is_dir=False) - - return # Fertig mit Wildcard-Löschen - - # 2. Rekursives Löschen (-r) - if recursive: - 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.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 deleted file mode 100644 index fb5beb5..0000000 --- a/buzzer_tool/core/commands/stat.py +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index f4d654e..0000000 --- a/buzzer_tool/core/commands/tags.py +++ /dev/null @@ -1,140 +0,0 @@ -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/commands/xy b/buzzer_tool/core/commands/xy deleted file mode 100644 index e69de29..0000000 diff --git a/buzzer_tool/core/config.py b/buzzer_tool/core/config.py deleted file mode 100644 index 606d1f6..0000000 --- a/buzzer_tool/core/config.py +++ /dev/null @@ -1,48 +0,0 @@ -# core/config.py -import os -import sys -import yaml - -DEFAULT_CONFIG = { - "port": None, - "baudrate": 115200, - "timeout": 5.0, - "crc_timeout_min_seconds": 2.0, - "crc_timeout_ms_per_100kb": 1.5, -} - -def load_config(cli_args=None): - config = DEFAULT_CONFIG.copy() - - cwd_config = os.path.join(os.getcwd(), "config.yaml") - script_dir = os.path.dirname(os.path.abspath(sys.argv[0])) - script_config = os.path.join(script_dir, "config.yaml") - - yaml_path = cwd_config if os.path.exists(cwd_config) else script_config if os.path.exists(script_config) else None - - if yaml_path: - try: - with open(yaml_path, "r", encoding="utf-8") as f: - yaml_data = yaml.safe_load(f) - if yaml_data and "serial" in yaml_data: - 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}") - - if cli_args: - if getattr(cli_args, "port", None) is not None: - config["port"] = cli_args.port - if getattr(cli_args, "baudrate", None) is not None: - config["baudrate"] = cli_args.baudrate - if getattr(cli_args, "timeout", None) is not None: - config["timeout"] = cli_args.timeout - - return config \ No newline at end of file diff --git a/buzzer_tool/core/connection.py b/buzzer_tool/core/connection.py deleted file mode 100644 index 4adfc16..0000000 --- a/buzzer_tool/core/connection.py +++ /dev/null @@ -1,866 +0,0 @@ -# core/connection.py -import time -import os -import struct -import binascii - -PROTOCOL_ERROR_MESSAGES = { - 0x01: "Ungültiger Befehl.", - 0x02: "Ungültige Parameter.", - 0x03: "Befehl oder Parameter sind zu lang.", - 0x10: "Datei oder Verzeichnis wurde nicht gefunden.", - 0x11: "Ziel existiert bereits.", - 0x12: "Pfad ist kein Verzeichnis.", - 0x13: "Pfad ist ein Verzeichnis.", - 0x14: "Zugriff verweigert.", - 0x15: "Kein freier Speicher mehr vorhanden.", - 0x16: "Datei ist zu groß.", - 0x20: "Allgemeiner Ein-/Ausgabefehler auf dem Gerät.", - 0x21: "Zeitüberschreitung auf dem Gerät.", - 0x22: "CRC-Prüfung fehlgeschlagen (Daten beschädigt).", - 0x23: "Übertragung wurde vom Gerät abgebrochen.", - 0x30: "Befehl wird vom Gerät nicht unterstützt.", - 0x31: "Gerät ist beschäftigt.", - 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 - -class BuzzerConnection: - def __init__(self, config): - 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( - port=self.port, - baudrate=self.baudrate, - timeout=self.timeout, - write_timeout=self.timeout - ) - self.serial.reset_input_buffer() - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - if self.serial and self.serial.is_open: - self.serial.close() - - def _parse_controller_error(self, line: str) -> str: - code_str = line.split(" ", 1)[1].strip() if " " in line else "" - try: - code = int(code_str, 10) - except ValueError: - return f"Controller meldet einen unbekannten Fehler: '{line}'" - - 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() - - try: - self.serial.write(f"{command}\n".encode('utf-8')) - 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 = [] - start_time = time.monotonic() - - while (time.monotonic() - start_time) < eff_timeout: - if self.serial.in_waiting > 0: - try: - line = self.serial.readline().decode('utf-8', errors='ignore').strip() - if not line: - continue - if line == "OK": - return lines - elif line.startswith("ERR"): - raise BuzzerError(self._parse_controller_error(line)) - else: - lines.append(line) - except BuzzerError: - raise - except Exception as e: - raise BuzzerError(f"Fehler beim Lesen der Antwort: {e}") - else: - time.sleep(POLL_SLEEP_SECONDS) - - raise TimeoutError(f"Lese-Timeout ({eff_timeout}s) beim Warten auf Antwort für: '{command}'") - - def send_binary(self, filepath: str, chunk_size: int = 4096, timeout: float = 10.0, progress_callback=None): - """ - Überträgt eine Binärdatei in Chunks, nachdem das READY-Signal empfangen wurde. - """ - # 1. Warte auf die READY-Bestätigung vom Controller - start_time = time.time() - ready = False - while (time.time() - start_time) < timeout: - if self.serial.in_waiting > 0: - line = self.serial.readline().decode('utf-8', errors='ignore').strip() - if line == "READY": - ready = True - break - elif line.startswith("ERR"): - raise BuzzerError(f"Fehler vor Binärtransfer: {self._parse_controller_error(line)}") - time.sleep(POLL_SLEEP_SECONDS) - - if not ready: - raise TimeoutError("Kein READY-Signal vom Controller empfangen.") - - # 2. Sende die Datei in Blöcken - file_size = os.path.getsize(filepath) - bytes_sent = 0 - - with open(filepath, 'rb') as f: - while bytes_sent < file_size: - # 1. Nicht blockierende Fehlerprüfung vor jedem Chunk - if self.serial.in_waiting > 0: - line = self.serial.readline().decode('utf-8', errors='ignore').strip() - if line.startswith("ERR"): - raise BuzzerError(f"Controller hat Transfer abgebrochen: {self._parse_controller_error(line)}") - - # 2. Chunk lesen und schreiben - chunk = f.read(chunk_size) - if not chunk: - break - - self.serial.write(chunk) - # WICHTIG: self.serial.flush() hier entfernen. - # Dies verhindert den Deadlock mit dem OS-USB-Puffer. - - bytes_sent += len(chunk) - - # 3. Callback für UI - if progress_callback: - progress_callback(len(chunk)) - - # 3. Warte auf das finale OK (oder ERR bei CRC/Schreib-Fehlern) - start_time = time.time() - while (time.time() - start_time) < timeout: - if self.serial.in_waiting > 0: - line = self.serial.readline().decode('utf-8', errors='ignore').strip() - if line == "OK": - return True - elif line.startswith("ERR"): - raise BuzzerError(f"Fehler beim Speichern der Binärdatei: {self._parse_controller_error(line)}") - 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/core/util.py b/buzzer_tool/core/util.py deleted file mode 100644 index a36726b..0000000 --- a/buzzer_tool/core/util.py +++ /dev/null @@ -1,25 +0,0 @@ -def hex_to_bytearray(hex_string): - """ - Wandelt einen Hex-String (z.B. "deadbeef") in ein bytearray um. - Entfernt vorher Leerzeichen und prüft auf Gültigkeit. - """ - try: - # Whitespace entfernen (falls vorhanden) - clean_hex = hex_string.strip().replace(" ", "") - - # Konvertierung - return bytearray.fromhex(clean_hex) - - except ValueError as e: - print(f"Fehler bei der Konvertierung: {e}") - return None - -def string_to_hexstring(text): - """ - Wandelt einen String in einen UTF-8-kodierten Hex-String um. - """ - # 1. String zu UTF-8 Bytes - utf8_bytes = text.encode('utf-8') - - # 2. Bytes zu Hex-String - return utf8_bytes.hex() \ No newline at end of file diff --git a/buzzer_tool/requirements.txt b/buzzer_tool/requirements.txt deleted file mode 100644 index c43cbbf..0000000 --- a/buzzer_tool/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -pyyaml -pyserial \ No newline at end of file diff --git a/buzzer_tool/smoke_test.sh b/buzzer_tool/smoke_test.sh deleted file mode 100755 index f29c349..0000000 --- a/buzzer_tool/smoke_test.sh +++ /dev/null @@ -1,165 +0,0 @@ -#!/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/prj.conf b/firmware/prj.conf index d7df214..805fd52 100644 --- a/firmware/prj.conf +++ b/firmware/prj.conf @@ -13,7 +13,6 @@ CONFIG_FLASH_MAP=y CONFIG_FILE_SYSTEM=y CONFIG_FILE_SYSTEM_LITTLEFS=y CONFIG_FILE_SYSTEM_MKFS=y -CONFIG_CRC=y CONFIG_FS_LITTLEFS_READ_SIZE=64 CONFIG_FS_LITTLEFS_PROG_SIZE=256 CONFIG_FS_LITTLEFS_CACHE_SIZE=512 @@ -25,7 +24,7 @@ CONFIG_MAIN_STACK_SIZE=2048 CONFIG_USB_DEVICE_STACK=y CONFIG_DEPRECATION_TEST=y CONFIG_USB_DEVICE_MANUFACTURER="Eduard Iten" -CONFIG_USB_DEVICE_PRODUCT="Edi's Buzzer" +CONFIG_USB_DEVICE_PRODUCT="Edis Buzzer" CONFIG_USB_DEVICE_PID=0x0001 CONFIG_USB_DRIVER_LOG_LEVEL_ERR=y CONFIG_USB_DEVICE_LOG_LEVEL_ERR=y diff --git a/firmware/src/fs.c b/firmware/src/fs.c index 3354ca3..19fd7f7 100644 --- a/firmware/src/fs.c +++ b/firmware/src/fs.c @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -416,4 +417,79 @@ int flash_write_firmware_block(const uint8_t *buffer, size_t length, bool is_las return rc; } return 0; +} + +size_t fs_get_external_flash_page_size(void) { + const struct flash_area *fa; + const struct device *dev; + struct flash_pages_info info; + int rc; + + rc = flash_area_open(STORAGE_PARTITION_ID, &fa); + if (rc != 0) { + LOG_ERR("Failed to open flash area for page size retrieval"); + return 256; // Fallback to a common page size + } + + dev = flash_area_get_device(fa); + if (dev == NULL) { + flash_area_close(fa); + LOG_ERR("Failed to get flash device for page size retrieval"); + return 256; // Fallback to a common page size + } + + rc = flash_get_page_info_by_offs(dev, fa->fa_off, &info); + flash_area_close(fa); + + if (rc != 0) { + LOG_ERR("Failed to get flash page info: %d", rc); + return 256; // Fallback to a common page size + } + + return info.size; +} + +size_t fs_get_fw_slot_size(void) { + const struct flash_area *fa; + int rc; + + rc = flash_area_open(SLOT1_ID, &fa); + if (rc != 0) { + LOG_ERR("Failed to open flash area for slot size retrieval"); + return 0; + } + + size_t slot_size = fa->fa_size; + flash_area_close(fa); + return slot_size; +} + +size_t fs_get_internal_flash_page_size(void) { + const struct flash_area *fa; + const struct device *dev; + struct flash_pages_info info; + int rc; + + rc = flash_area_open(SLOT1_ID, &fa); + if (rc != 0) { + LOG_ERR("Failed to open flash area for page size retrieval"); + return 256; // Fallback to a common page size + } + + dev = flash_area_get_device(fa); + if (dev == NULL) { + flash_area_close(fa); + LOG_ERR("Failed to get flash device for page size retrieval"); + return 256; // Fallback to a common page size + } + + rc = flash_get_page_info_by_offs(dev, fa->fa_off, &info); + flash_area_close(fa); + + if (rc != 0) { + LOG_ERR("Failed to get flash page info: %d", rc); + return 256; // Fallback to a common page size + } + + return info.size; } \ No newline at end of file diff --git a/firmware/src/fs.h b/firmware/src/fs.h index 936f933..281b328 100644 --- a/firmware/src/fs.h +++ b/firmware/src/fs.h @@ -3,6 +3,8 @@ #include +#define MAX_PATH_LEN 32U + typedef struct slot_info_t { size_t start_addr; size_t size; @@ -182,4 +184,21 @@ int flash_init_firmware_upload(void); */ int flash_write_firmware_block(const uint8_t *buffer, size_t length, bool is_last_block); +/** + * @brief Gets the page size of the internal flash, which is needed for proper write operations + * @return Page size in bytes + */ +size_t fs_get_internal_flash_page_size(void); + +/** + * @brief Gets the size of the firmware slot, which is needed for proper write operations + * @return Size in bytes + */ +size_t fs_get_fw_slot_size(void); + +/** + * @brief Gets the page size of the external flash, which is needed for proper write operations + * @return Page size in bytes + */ +size_t fs_get_external_flash_page_size(void); #endif // FS_H \ No newline at end of file diff --git a/firmware/src/protocol.c b/firmware/src/protocol.c index 5094f7e..04ad166 100644 --- a/firmware/src/protocol.c +++ b/firmware/src/protocol.c @@ -2,130 +2,55 @@ #include #include #include -#include #include #include #include #include #include +#include +#include #include #include #include #include -#define PROTOCOL_VERSION 1 +#define PROTOCOL_VERSION 1U -LOG_MODULE_REGISTER(protocol, LOG_LEVEL_INF); +LOG_MODULE_REGISTER(protocol, LOG_LEVEL_DBG); #define PROTOCOL_STACK_SIZE 3072 #define PROTOCOL_PRIORITY 6 -#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 +#define PROTOCOL_TIMEOUT_MS 10 +#define PROTOCOL_TIMEOUT K_MSEC(PROTOCOL_TIMEOUT_MS) -static const uint8_t sync_pattern[4] = { 'B', 'U', 'Z', 'Z' }; +static const uint8_t sync_pattern[4] = {'B', 'U', 'Z', 'Z'}; -static protocol_state_t rx_state = PS_WAIT_SYNC; -static uint8_t sync_index = 0; +#define SEND_SYNC() \ + usb_write_buffer(sync_pattern, sizeof(sync_pattern)); -static uint8_t header_buf[HEADER_SIZE]; -static size_t header_index = 0; +typedef struct { + char path[PROTOCOL_MAX_PATH_LEN]; + size_t index; + size_t len; +} path_context_t; -static uint8_t payload_buf[MAX_RX_PAYLOAD_SIZE]; -static uint32_t payload_index = 0; +path_context_t path1_ctx; +path_context_t path2_ctx; -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) -{ - 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); -} +K_MEM_SLAB_DEFINE(file_buffer_slab, 4096, 4, 4); 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; - switch (err) { + switch (err) + { case ENOENT: return P_ERR_FILE_NOT_FOUND; case EEXIST: @@ -166,1005 +91,512 @@ static protocol_error_t protocol_map_error(int32_t rc) } } -static int protocol_extract_path(const uint8_t *rx_payload, uint32_t rx_len, char *path, size_t path_size) +static void send_ack_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; + uint8_t resp[5] = {0x42, 0x55, 0x5A, 0x5A, FRAME_RESP_ACK}; + usb_write_buffer(resp, sizeof(resp)); } -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) +static void send_error_response(int32_t err) { - 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; + uint8_t resp[6] = {0x42, 0x55, 0x5A, 0x5A, FRAME_RESP_ERROR, protocol_map_error(err)}; + usb_write_buffer(resp, sizeof(resp)); } -static int protocol_join_path(char *out_path, size_t out_path_size, - const char *base_path, const char *name) +/** + * Fills the provided buffer with data read from the USB interface, waiting up to the specified timeout for data to become available. + * @param buffer Buffer to fill with received data + * @param len Number of bytes to read into the buffer + * @param timeout Maximum time to wait for data before giving up + * @return Number of bytes read into the buffer, or a negative error code on failure + */ + +static int fill_buffer(uint8_t *buffer, size_t len, k_timeout_t timeout) { - 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; - fs_dir_t_init(&dirp); - - int rc = fs_pm_opendir(&dirp, path); - if (rc == -ENOTDIR) { - return fs_pm_unlink(path); - } - if (rc != 0) { - return rc; - } - - 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; + size_t offset = 0; + while (offset < len) { + if (!usb_wait_for_data(timeout)) { + return -ETIMEDOUT; } - if (entry.type == FS_DIR_ENTRY_DIR) { - rc = protocol_remove_recursive_impl(child_path); + size_t read_now = usb_read_buffer(buffer + offset, len - offset); + if (read_now == 0) { + return -EIO; + } + + offset += read_now; + } + return (int)offset; +} + +/** + * Reads a single byte from the USB interface, waiting up to the specified timeout. + * @param byte Pointer to store the read byte + * @return 1 if a byte was read, 0 if no data was available, or a negative error code on failure + */ +static int get_byte(uint8_t *byte, k_timeout_t timeout) +{ + if (!usb_wait_for_data(timeout)) { + return -ETIMEDOUT; + } + + int ret = usb_read_byte(byte); + return (ret ? 1 : -ENODATA); +} + +static bool get_path(uint8_t *buffer, size_t len) { + uint8_t path_len; + int rc = get_byte(&path_len, PROTOCOL_TIMEOUT); + if (rc <= 0) + { + send_error_response((rc < 0) ? rc : -EIO); + LOG_ERR("Failed to read path length: %d", rc); + return false; + } + if (path_len == 0U || path_len > len) + { + send_error_response(-EMSGSIZE); + LOG_ERR("Invalid path length: %u (max %u)", path_len, (unsigned int)len); + return false; + } + + rc = fill_buffer(buffer, path_len, PROTOCOL_TIMEOUT); + if (rc != (int)path_len) + { + if (rc >= 0) { + send_error_response(-EIO); } else { - rc = fs_pm_unlink(child_path); - } - - if (rc != 0) { - (void)fs_pm_closedir(&dirp); - return rc; + send_error_response(rc); } + return false; } - - int close_rc = fs_pm_closedir(&dirp); - if (rc != 0) { - return rc; - } - if (close_rc != 0) { - return close_rc; - } - - return fs_pm_unlink(path); + buffer[path_len] = '\0'; + return true; } -static int protocol_handle_get_protocol_version(uint8_t *tx_payload, uint32_t *tx_len) +static void proc_protocol_version() { - sys_put_le16(PROTOCOL_VERSION, tx_payload); - *tx_len = 2U; - return 0; + uint8_t resp[3]; + resp[0] = FRAME_RESP_DATA; + sys_put_le16(PROTOCOL_VERSION, &resp[1]); + SEND_SYNC(); + usb_write_buffer(resp, sizeof(resp)); + LOG_DBG("RESP: Protocol version %u", PROTOCOL_VERSION); + return; } -static int protocol_handle_get_firmware_status(uint8_t *tx_payload, uint32_t *tx_len) +static void proc_firmware_status() { - 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; - } + uint8_t resp[11]; + resp[0] = FRAME_RESP_DATA; int swap_type = mcuboot_swap_type(); - if (swap_type < 0) { - return swap_type; + if (swap_type < 0) + { + send_error_response(swap_type); + LOG_ERR("Failed to get swap type: %d", swap_type); + return; } - uint8_t fw_status = FW_STATUS_CONFIRMED; - if (!boot_is_img_confirmed()) { + firmware_status_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) { + } + 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; + resp[1] = fw_status; + sys_put_le32(APPVERSION, &resp[2]); + sys_put_le32(KERNELVERSION, &resp[6]); + resp[10] = strlen(APP_VERSION_STRING); + SEND_SYNC(); + usb_write_buffer(resp, sizeof(resp)); + usb_write_buffer((const uint8_t *)APP_VERSION_STRING, strlen(APP_VERSION_STRING)); + LOG_DBG("RESP: Firmware status"); + return; } -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) +static void proc_flash_info() { struct fs_statvfs stat; + int rc = fs_pm_statvfs("/lfs", &stat); - if (rc != 0) { - return rc; + if (rc != 0) + { + send_error_response(rc); + LOG_ERR("Failed to get flash status: %d", rc); + return; } - 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; + uint8_t resp[22]; + resp[0] = FRAME_RESP_DATA; + sys_put_le32((uint32_t)stat.f_frsize, &resp[1]); + sys_put_le32((uint32_t)stat.f_blocks, &resp[5]); + sys_put_le32((uint32_t)stat.f_bfree, &resp[9]); + sys_put_le32((uint32_t)fs_get_fw_slot_size(), &resp[13]); + sys_put_le16((uint16_t)fs_get_external_flash_page_size(), &resp[17]); + sys_put_le16((uint16_t)fs_get_internal_flash_page_size(), &resp[19]); + resp[21] = MAX_PATH_LEN; + SEND_SYNC(); + usb_write_buffer(resp, sizeof(resp)); + return; } -static int protocol_handle_list_dir(const uint8_t *rx_payload, uint32_t rx_len) +static void proc_stat() { - char path[PROTOCOL_MAX_PATH_LEN + 1U]; - int rc = protocol_extract_path(rx_payload, rx_len, path, sizeof(path)); - if (rc != 0) { - return rc; + uint8_t buffer[MAX_PATH_LEN+1]; + if (!get_path(buffer, MAX_PATH_LEN)) + { + return; } - - struct fs_dir_t dirp; + + char *path = (char *)buffer; struct fs_dirent entry; - fs_dir_t_init(&dirp); - - rc = fs_pm_opendir(&dirp, path); - if (rc != 0) { - return rc; + if (strcmp(path, "/") == 0) + { + // Special case for root directory + entry.type = FS_DIR_ENTRY_DIR; + entry.size = 0; } - - 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; + else + { + int rc = fs_pm_stat(path, &entry); + if (rc != 0) + { + send_error_response(rc); + LOG_ERR("Failed to stat path '%s': %d", path, rc); + return; } - - 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); - - if (rc < 0) { - return rc; - } - - protocol_send_frame(FRAME_RESP_STREAM_END, current_command_id, current_sequence, NULL, 0U); - return PROTOCOL_RC_STREAMING_DONE; + uint8_t resp[6]; + resp[0] = FRAME_RESP_DATA; + resp[1] = (entry.type == FS_DIR_ENTRY_DIR) ? 1 : 0; + sys_put_le32((uint32_t)entry.size, &resp[2]); + SEND_SYNC(); + usb_write_buffer(resp, sizeof(resp)); + LOG_DBG("RESP: Stat path '%s'", path); + return; } -static int protocol_handle_check_file_crc(const uint8_t *rx_payload, uint32_t rx_len, uint8_t *out_payload, uint32_t *out_len) +static void proc_ls() { - char path[PROTOCOL_MAX_PATH_LEN + 1U]; - int rc = protocol_extract_path(rx_payload, rx_len, path, sizeof(path)); + uint8_t buffer[MAX_PATH_LEN+1]; + if (!get_path(buffer, MAX_PATH_LEN)) + { + return; + } + + char *path = (char *)buffer; + if (strlen(path) == 0) + { + path[0] = '/'; + path[1] = '\0'; + } + + struct fs_dirent entry; + struct fs_dir_t dir; + fs_dir_t_init(&dir); + int rc = fs_pm_opendir(&dir, path); + if (rc != 0) + { + send_error_response(rc); + LOG_ERR("Failed to open directory '%s': %d", path, rc); + return; + } + + SEND_SYNC(); + usb_write_byte(FRAME_RESP_LIST_START); + + int num_entries = 0; + uint8_t resp[8]; + resp[0] = FRAME_RESP_LIST_CHUNK; + + while ((rc = fs_readdir(&dir, &entry)) == 0) + { + if (entry.name[0] == '\0') + { + break; // End of directory + } + resp[3] = (entry.type == FS_DIR_ENTRY_DIR) ? 1 : 0; + sys_put_le32(entry.size, &resp[4]); + sys_put_le16(strlen(entry.name)+5, &resp[1]); + + SEND_SYNC(); + usb_write_buffer(resp, sizeof(resp)); + usb_write_buffer(entry.name, strlen(entry.name) ); + num_entries++; + } + fs_pm_closedir(&dir); + + resp[0]= FRAME_RESP_LIST_END; + sys_put_le16(num_entries, &resp[1]); + SEND_SYNC(); + usb_write_buffer(resp, 3); + + LOG_DBG("LIST: List directory '%s', %d entries", path, num_entries); + return; +} + +static void proc_rm() { + uint8_t buffer[MAX_PATH_LEN+1]; + if (!get_path(buffer, MAX_PATH_LEN)) + { + return; + } + + char *path = (char *)buffer; + int rc = fs_pm_unlink(path); + if (rc != 0) + { + send_error_response(rc); + LOG_ERR("Failed to remove path '%s': %d", path, rc); + return; + } + send_ack_response(); + LOG_DBG("RESP: Removed path '%s'", path); + return; +} + +static void proc_rename() { + uint8_t src_buffer[MAX_PATH_LEN+1]; + uint8_t dst_buffer[MAX_PATH_LEN+1]; + if (!get_path(src_buffer, MAX_PATH_LEN)) + { + return; + } + if (!get_path(dst_buffer, MAX_PATH_LEN)) + { + return; + } + + char *source_path = (char *)src_buffer; + char *dest_path = (char *)dst_buffer; + + int rc = fs_pm_rename(source_path, dest_path); + if (rc != 0) + { + send_error_response(rc); + LOG_ERR("Failed to rename path '%s' to '%s': %d", source_path, dest_path, rc); + return; + } + send_ack_response(); + LOG_DBG("RESP: Renamed path '%s' to '%s'", source_path, dest_path); + return; +} + +static void proc_get_file() { + uint8_t path_buffer[MAX_PATH_LEN + 1]; + if (!get_path(path_buffer, MAX_PATH_LEN)) { + return; + } + + char *path = (char *)path_buffer; + struct fs_dirent entry; + int rc = fs_pm_stat(path, &entry); + if (rc != 0) { - return rc; + send_error_response(rc); + return; + } + if (entry.type == FS_DIR_ENTRY_DIR) { + send_error_response(-EISDIR); + return; } struct fs_file_t file; fs_file_t_init(&file); rc = fs_pm_open(&file, path, FS_O_READ); if (rc != 0) { - return rc; + send_error_response(rc); + LOG_ERR("Failed to open file '%s': %d", path, rc); + return; } - ssize_t audio_len = fs_get_audio_data_len(&file); - if (audio_len < 0) { + uint8_t resp[5]; + resp[0] = FRAME_RESP_STREAM_START; + uint32_t file_size = (uint32_t)entry.size; + sys_put_le32(file_size, &resp[1]); + + SEND_SYNC(); + usb_write_buffer(resp, sizeof(resp)); + + uint8_t *slab_ptr = NULL; + rc = k_mem_slab_alloc(&file_buffer_slab, (void **)&slab_ptr, K_NO_WAIT); + if (rc != 0) { + send_error_response(-ENOMEM); + LOG_ERR("Slab allocation failed: %d", rc); fs_pm_close(&file); - return (int)audio_len; + return; + } + + uint32_t bytes_remaining = file_size; + uint32_t running_crc32 = 0U; + bool stream_ok = true; + + while (bytes_remaining > 0) { + size_t to_read = MIN(bytes_remaining, 1024); + ssize_t bytes_read = fs_read(&file, slab_ptr, to_read); + if (bytes_read < 0) { + send_error_response((int)bytes_read); + stream_ok = false; + break; + } + if (bytes_read == 0) { + send_error_response(-EIO); + stream_ok = false; + break; + } + + LOG_DBG("1"); + running_crc32 = crc32_ieee_update(running_crc32, slab_ptr, (size_t)bytes_read); + LOG_DBG("2"); + rc = usb_write_buffer(slab_ptr, (size_t)bytes_read); + LOG_DBG("3"); + if (rc < 0) { + send_error_response(rc); + LOG_ERR("USB write failed: %d", rc); + stream_ok = false; + break; + } + LOG_DBG("4"); + bytes_remaining -= bytes_read; + } + + k_mem_slab_free(&file_buffer_slab, (void **)&slab_ptr); + fs_pm_close(&file); + + if (!stream_ok || bytes_remaining != 0U) { + LOG_ERR("Aborting stream for '%s' (remaining=%u, ok=%d)", path, bytes_remaining, stream_ok); + return; + } + + uint8_t resp_end[5]; + resp_end[0] = FRAME_RESP_STREAM_END; + sys_put_le32(running_crc32, &resp_end[1]); + SEND_SYNC(); + usb_write_buffer(resp_end, sizeof(resp_end)); + LOG_INF("File '%s' sent. CRC32: 0x%08X", path, running_crc32); +} + +static void proc_check_file_crc() +{ + uint8_t path_buffer[MAX_PATH_LEN + 1]; + if (!get_path(path_buffer, MAX_PATH_LEN)) { + return; + } + + char *path = (char *)path_buffer; + struct fs_dirent entry; + int rc = fs_pm_stat(path, &entry); + if (rc != 0) { + send_error_response(rc); + return; + } + if (entry.type == FS_DIR_ENTRY_DIR) { + send_error_response(-EISDIR); + return; + } + + struct fs_file_t file; + fs_file_t_init(&file); + rc = fs_pm_open(&file, path, FS_O_READ); + if (rc != 0) { + send_error_response(rc); + LOG_ERR("Failed to open file '%s' for CRC: %d", path, rc); + return; + } + + uint8_t *slab_ptr = NULL; + rc = k_mem_slab_alloc(&file_buffer_slab, (void **)&slab_ptr, K_NO_WAIT); + if (rc != 0) { + send_error_response(-ENOMEM); + fs_pm_close(&file); + return; } uint32_t crc32 = 0U; - 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); + while (1) { + ssize_t bytes_read = fs_read(&file, slab_ptr, 4096); + if (bytes_read < 0) { + send_error_response((int)bytes_read); + k_mem_slab_free(&file_buffer_slab, (void **)&slab_ptr); + fs_pm_close(&file); + return; + } + if (bytes_read == 0) { + break; + } + crc32 = crc32_ieee_update(crc32, slab_ptr, (size_t)bytes_read); } + k_mem_slab_free(&file_buffer_slab, (void **)&slab_ptr); fs_pm_close(&file); - if (read < 0) { - return (int)read; - } - - sys_put_le32(crc32, out_payload); - *out_len = 4U; - return 0; + uint8_t resp[5]; + resp[0] = FRAME_RESP_DATA; + sys_put_le32(crc32, &resp[1]); + SEND_SYNC(); + usb_write_buffer(resp, sizeof(resp)); + LOG_DBG("CRC32 for '%s': 0x%08X", path, crc32); } -static int protocol_handle_mkdir(const uint8_t *rx_payload, uint32_t rx_len) +static protocol_state_t process_frame_type(protocol_frame_type_t frame_type) { - char path[PROTOCOL_MAX_PATH_LEN + 1U]; - int rc = protocol_extract_path(rx_payload, rx_len, path, sizeof(path)); - if (rc != 0) { - return rc; + switch (frame_type) + { + case FRAME_REQ: + return PS_READ_REQ; + case FRAME_REQ_DATA: + return PS_READ_REQ_DATA; + default: + LOG_ERR("Invalid frame type: %d", frame_type); + send_error_response(-EINVAL); + return PS_WAIT_SYNC; } - - return fs_pm_mkdir(path); } -static int protocol_handle_rm(const uint8_t *rx_payload, uint32_t rx_len) +static void process_req(uint8_t req) { - 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; - fs_file_t_init(&file); - rc = fs_pm_open(&file, path, FS_O_READ); - if (rc != 0) { - return rc; - } - - 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 running_crc = 0U; - size_t bytes_sent = 0U; - - 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; - } - 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) == 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) { - ssize_t written = fs_write(&file, payload_buf, accumulated); - if (written < 0) { - fs_pm_close(&file); - (void)fs_pm_unlink(path); - return (int)written; - } - - if ((size_t)written != accumulated) { - fs_pm_close(&file); - (void)fs_pm_unlink(path); - return -EIO; - } - - running_crc = crc32_ieee_update(running_crc, payload_buf, accumulated); - bytes_written += accumulated; - accumulated = 0U; - } - } - - rc = fs_pm_close(&file); - if (rc != 0) { - (void)fs_pm_unlink(path); - return rc; - } - - if (running_crc != expected_crc) { - (void)fs_pm_unlink(path); - return -EBADMSG; - } - - return 0; -} - -static int protocol_handle_put_fw_start(const uint8_t *rx_payload, uint32_t rx_len) -{ - if (rx_len != 8U) { - return -EINVAL; - } - - 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; -} - -static void protocol_dispatch_request(void) -{ - if (current_frame_type != FRAME_REQ) { - protocol_send_error(current_command_id, current_sequence, P_ERR_INVALID_PARAMETERS); - return; - } - - uint32_t tx_len = 0U; - int rc = 0; - - switch (current_command_id) { + switch (req) + { 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; - + proc_protocol_version(); + return; 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; - + proc_firmware_status(); + return; 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_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_REBOOT: - 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); + proc_flash_info(); return; - - case CMD_LIST_DIR: - rc = protocol_handle_list_dir(payload_buf, current_payload_len); - break; - - case CMD_CHECK_FILE_CRC: - rc = protocol_handle_check_file_crc(payload_buf, current_payload_len, tx_payload, &tx_len); - break; - - case CMD_MKDIR: - rc = protocol_handle_mkdir(payload_buf, current_payload_len); - tx_len = 0U; - break; - - case CMD_RM: - rc = protocol_handle_rm(payload_buf, current_payload_len); - tx_len = 0U; - break; - case CMD_STAT: - rc = protocol_handle_stat(payload_buf, current_payload_len, tx_payload, &tx_len); - break; - + proc_stat(); + return; + case CMD_LIST_DIR: + proc_ls(); + return; + case CMD_CHECK_FILE_CRC: + proc_check_file_crc(); + return; + case CMD_RM: + proc_rm(); + return; 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; - + proc_rename(); + return; + case CMD_PUT_FILE: + send_error_response(-ENOTSUP); + return; 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)); - } + proc_get_file(); 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: - protocol_send_error(current_command_id, current_sequence, P_ERR_INVALID_COMMAND); + LOG_ERR("Invalid req type: 0x%02x", req); + send_error_response(ENOTSUP); 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; - - 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); - - 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; - } - - 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 (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; - } - - payload_index = 0; - payload_crc_index = 0; - rx_state = (current_payload_len == 0U) ? PS_READ_PAYLOAD_CRC : PS_READ_PAYLOAD; - } - break; - - 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; - } + return; } void protocol_thread_entry(void *p1, void *p2, void *p3) @@ -1174,18 +606,61 @@ void protocol_thread_entry(void *p1, void *p2, void *p3) ARG_UNUSED(p3); LOG_INF("Protocol thread started"); - - protocol_reset_rx_state(); - while (1) { - if (!usb_wait_for_data(K_FOREVER)) { - continue; - } + while (1) + { + uint8_t byte; + protocol_state_t state = PS_WAIT_SYNC; + uint8_t sync_pos = 0; + k_timeout_t timeout = K_FOREVER; - uint8_t rx_byte; - while (usb_read_char(&rx_byte) > 0) { - protocol_process_rx_byte(rx_byte); + while (usb_wait_for_data(timeout)) + { + while (usb_read_byte(&byte)) + { + + switch (state) + { + case PS_WAIT_SYNC: + if (byte == sync_pattern[sync_pos]) + { + timeout = PROTOCOL_TIMEOUT; + sync_pos++; + LOG_DBG("SYNC %u", sync_pos); + if (sync_pos == sizeof(sync_pattern)) + { + sync_pos = 0; + state = PS_READ_FRAME_TYPE; + } + } + else + { + sync_pos = 0; + timeout = K_FOREVER; + } + break; + case PS_READ_FRAME_TYPE: + LOG_DBG("FT: 0x%02x", byte); + state = process_frame_type(byte); + break; + case PS_READ_REQ: + process_req(byte); + state = PS_WAIT_SYNC; + timeout = K_FOREVER; + break; + default: + LOG_WRN("Invalid protocol state: 0x%02x", state); + state = PS_WAIT_SYNC; + timeout = K_FOREVER; + break; + } + } } + send_error_response(-ETIMEDOUT); + state = PS_WAIT_SYNC; + timeout = K_FOREVER; + LOG_ERR("TIMEOUT, PS 0x%02x", state); + continue; } } diff --git a/firmware/src/protocol.h b/firmware/src/protocol.h index acc3684..f48cd1e 100644 --- a/firmware/src/protocol.h +++ b/firmware/src/protocol.h @@ -8,44 +8,42 @@ typedef enum { PS_WAIT_SYNC = 0, - PS_READ_HEADER, - PS_READ_PAYLOAD, - PS_READ_PAYLOAD_CRC, + PS_READ_FRAME_TYPE, + PS_READ_REQ, + PS_READ_REQ_DATA, } protocol_state_t; typedef enum { - CMD_INVALID = 0, 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, + + CMD_PUT_FILE = 0x20, + CMD_PUT_FW = 0x21, + CMD_GET_FILE = 0x22, } protocol_cmd_t; typedef enum { FRAME_REQ = 0x01, + FRAME_REQ_DATA = 0x02, FRAME_RESP_ACK = 0x10, FRAME_RESP_DATA = 0x11, FRAME_RESP_STREAM_START = 0x12, - FRAME_RESP_STREAM_CHUNK = 0x13, + // FRAME_RESP_STREAM_CHUNK = 0x13, FRAME_RESP_STREAM_END = 0x14, - FRAME_RESP_ERROR = 0x7F, + FRAME_RESP_LIST_START = 0x15, + FRAME_RESP_LIST_CHUNK = 0x16, + FRAME_RESP_LIST_END = 0x17, + FRAME_RESP_ERROR = 0xFF, } protocol_frame_type_t; typedef enum { @@ -72,6 +70,13 @@ typedef enum { P_ERR_INTERNAL = 0x32, } protocol_error_t; +typedef enum +{ + FW_STATUS_CONFIRMED = 0x00, + FW_STATUS_PENDING = 0x01, + FW_STATUS_TESTING = 0x02, +} firmware_status_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/protocol_old.c b/firmware/src/protocol_old.c new file mode 100644 index 0000000..5094f7e --- /dev/null +++ b/firmware/src/protocol_old.c @@ -0,0 +1,1195 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#define PROTOCOL_VERSION 1 + +LOG_MODULE_REGISTER(protocol, LOG_LEVEL_INF); + +#define PROTOCOL_STACK_SIZE 3072 +#define PROTOCOL_PRIORITY 6 +#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 const uint8_t sync_pattern[4] = { 'B', 'U', 'Z', 'Z' }; + +static protocol_state_t rx_state = PS_WAIT_SYNC; +static uint8_t sync_index = 0; + +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) +{ + 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) { + return P_ERR_NONE; + } + + int32_t err = (rc < 0) ? -rc : rc; + + switch (err) { + case ENOENT: + return P_ERR_FILE_NOT_FOUND; + case EEXIST: + return P_ERR_ALREADY_EXISTS; + case ENOTDIR: + return P_ERR_NOT_A_DIRECTORY; + case EISDIR: + return P_ERR_IS_A_DIRECTORY; + case EACCES: + case EPERM: + return P_ERR_ACCESS_DENIED; + case ENOSPC: + case ENOMEM: + return P_ERR_NO_SPACE; + case EFBIG: + return P_ERR_FILE_TOO_LARGE; + case ETIMEDOUT: + return P_ERR_TIMEOUT; + case EMSGSIZE: + return P_ERR_COMMAND_TOO_LONG; + case EINVAL: + return P_ERR_INVALID_PARAMETERS; + case EILSEQ: + return P_ERR_INVALID_COMMAND; + case ECANCELED: + return P_ERR_TRANSFER_ABORTED; + case ENOSYS: + case ENOTSUP: + return P_ERR_NOT_SUPPORTED; + case EBUSY: + return P_ERR_BUSY; + case EBADMSG: + return P_ERR_CRC_MISMATCH; + case EIO: + return P_ERR_IO; + default: + return P_ERR_INTERNAL; + } +} + +static int protocol_extract_path(const uint8_t *rx_payload, uint32_t rx_len, char *path, size_t path_size) +{ + 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; +} + +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; + fs_dir_t_init(&dirp); + + int rc = fs_pm_opendir(&dirp, path); + if (rc == -ENOTDIR) { + return fs_pm_unlink(path); + } + if (rc != 0) { + return rc; + } + + 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); + + if (rc < 0) { + return rc; + } + + 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; +} + +static int protocol_handle_mkdir(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_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; + fs_file_t_init(&file); + rc = fs_pm_open(&file, path, FS_O_READ); + if (rc != 0) { + return rc; + } + + 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 running_crc = 0U; + size_t bytes_sent = 0U; + + 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; + } + 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) == 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) { + ssize_t written = fs_write(&file, payload_buf, accumulated); + if (written < 0) { + fs_pm_close(&file); + (void)fs_pm_unlink(path); + return (int)written; + } + + if ((size_t)written != accumulated) { + fs_pm_close(&file); + (void)fs_pm_unlink(path); + return -EIO; + } + + running_crc = crc32_ieee_update(running_crc, payload_buf, accumulated); + bytes_written += accumulated; + accumulated = 0U; + } + } + + rc = fs_pm_close(&file); + if (rc != 0) { + (void)fs_pm_unlink(path); + return rc; + } + + if (running_crc != expected_crc) { + (void)fs_pm_unlink(path); + return -EBADMSG; + } + + return 0; +} + +static int protocol_handle_put_fw_start(const uint8_t *rx_payload, uint32_t rx_len) +{ + if (rx_len != 8U) { + return -EINVAL; + } + + 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; +} + +static void protocol_dispatch_request(void) +{ + if (current_frame_type != FRAME_REQ) { + protocol_send_error(current_command_id, current_sequence, P_ERR_INVALID_PARAMETERS); + return; + } + + uint32_t tx_len = 0U; + int rc = 0; + + 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_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_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_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_REBOOT: + 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_CHECK_FILE_CRC: + rc = protocol_handle_check_file_crc(payload_buf, current_payload_len, tx_payload, &tx_len); + break; + + case CMD_MKDIR: + rc = protocol_handle_mkdir(payload_buf, current_payload_len); + tx_len = 0U; + break; + + case CMD_RM: + rc = protocol_handle_rm(payload_buf, current_payload_len); + tx_len = 0U; + break; + + 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: + 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; + + 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); + + 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; + } + + 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 (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; + } + + payload_index = 0; + payload_crc_index = 0; + rx_state = (current_payload_len == 0U) ? PS_READ_PAYLOAD_CRC : PS_READ_PAYLOAD; + } + break; + + 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) +{ + ARG_UNUSED(p1); + ARG_UNUSED(p2); + ARG_UNUSED(p3); + + LOG_INF("Protocol thread started"); + + protocol_reset_rx_state(); + + while (1) { + if (!usb_wait_for_data(K_FOREVER)) { + continue; + } + + uint8_t rx_byte; + while (usb_read_char(&rx_byte) > 0) { + protocol_process_rx_byte(rx_byte); + } + } +} + +/* Thread statisch definieren und automatisch starten lassen */ +K_THREAD_DEFINE(protocol_tid, PROTOCOL_STACK_SIZE, + protocol_thread_entry, NULL, NULL, NULL, + PROTOCOL_PRIORITY, 0, 0); \ No newline at end of file diff --git a/firmware/src/usb.c b/firmware/src/usb.c index b61592f..dd5a796 100644 --- a/firmware/src/usb.c +++ b/firmware/src/usb.c @@ -6,6 +6,8 @@ #include +#define RX_RING_BUF_SIZE 1024 + LOG_MODULE_REGISTER(usb, LOG_LEVEL_INF); K_SEM_DEFINE(usb_rx_sem, 0, 1); @@ -13,9 +15,8 @@ K_SEM_DEFINE(usb_tx_sem, 0, 1); #define UART_NODE DT_ALIAS(usb_uart) const struct device *cdc_dev = DEVICE_DT_GET(UART_NODE); +static volatile bool rx_interrupt_enabled = false; -/* NEU: Ringbuffer für stabilen asynchronen USB-Empfang */ -#define RX_RING_BUF_SIZE 5*1024 /* 8 KB Ringpuffer für eingehende USB-Daten */ RING_BUF_DECLARE(rx_ringbuf, RX_RING_BUF_SIZE); static void cdc_acm_irq_cb(const struct device *dev, void *user_data) @@ -36,7 +37,6 @@ static void cdc_acm_irq_cb(const struct device *dev, void *user_data) und der USB-Stack den Host drosselt (NAK). */ uart_irq_rx_disable(dev); } else { - /* Nur so viele Daten lesen, wie Platz im Ringpuffer ist */ int to_read = MIN(sizeof(buffer), space); int len = uart_fifo_read(dev, buffer, to_read); @@ -68,23 +68,15 @@ bool usb_wait_for_data(k_timeout_t timeout) return (k_sem_take(&usb_rx_sem, timeout) == 0); } -int usb_read_char(uint8_t *c) +bool usb_read_byte(uint8_t *c) { int ret = ring_buf_get(&rx_ringbuf, c, 1); - if (ret > 0 && device_is_ready(cdc_dev)) { - /* Platz geschaffen -> Empfang wieder aktivieren */ - uart_irq_rx_enable(cdc_dev); - } - return ret; + return (ret > 0); } int usb_read_buffer(uint8_t *buf, size_t max_len) { int ret = ring_buf_get(&rx_ringbuf, buf, max_len); - if (ret > 0 && device_is_ready(cdc_dev)) { - /* Platz geschaffen -> Empfang wieder aktivieren */ - uart_irq_rx_enable(cdc_dev); - } return ret; } @@ -95,7 +87,7 @@ void usb_resume_rx(void) } } -void usb_write_char(uint8_t c) +void usb_write_byte(uint8_t c) { if (!device_is_ready(cdc_dev)) { return; @@ -103,11 +95,11 @@ void usb_write_char(uint8_t c) uart_poll_out(cdc_dev, c); } -void usb_write_buffer(const uint8_t *buf, size_t len) +int usb_write_buffer(const uint8_t *buf, size_t len) { if (!device_is_ready(cdc_dev)) { - return; + return -ENODEV; } size_t written; @@ -126,10 +118,11 @@ void usb_write_buffer(const uint8_t *buf, size_t len) if (k_sem_take(&usb_tx_sem, K_MSEC(100)) != 0) { LOG_WRN("USB TX timeout - consumer not reading?"); - return; + return -ETIMEDOUT; } } } + return 0; } void usb_flush_rx(void) diff --git a/firmware/src/usb.h b/firmware/src/usb.h index 9ece4ee..0ca56ce 100644 --- a/firmware/src/usb.h +++ b/firmware/src/usb.h @@ -17,9 +17,9 @@ bool usb_wait_for_data(k_timeout_t timeout); /** * @brief Reads a single character from the USB RX FIFO * @param c Pointer to store the read character - * @return 1 if a character was read, 0 if no data was available + * @return true if a character was read, false if no data was available */ -int usb_read_char(uint8_t *c); +bool usb_read_byte(uint8_t *c); /** * @brief Reads a block of data from the USB RX FIFO @@ -36,16 +36,16 @@ void usb_resume_rx(void); /** * @brief Writes a single character to the USB TX FIFO - * @param c Character to write + * @param c Character to write */ -void usb_write_char(uint8_t c); +void usb_write_byte(uint8_t c); /** * @brief Writes a block of data to the USB TX FIFO * @param buf Buffer containing the data to write * @param len Number of bytes to write */ -void usb_write_buffer(const uint8_t *buf, size_t len); +int usb_write_buffer(const uint8_t *buf, size_t len); /** * @brief Flushes the USB RX FIFO diff --git a/tool/buzz.py b/tool/buzz.py new file mode 100644 index 0000000..2d54146 --- /dev/null +++ b/tool/buzz.py @@ -0,0 +1,170 @@ +import sys +import os +import argparse + +# # Falls buzz.py tief in Unterordnern liegt, stellen wir sicher, +# # dass das Hauptverzeichnis im Pfad ist: +# sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +def main(): + try: + parser = argparse.ArgumentParser(description="Buzzer Serial Comm Tool") + + # Allgemeine Parameter + parser.add_argument("-c", "--config", help="Pfad zur config.yaml (optional)", type=str) + parser.add_argument("-d", "--debug", help="Aktiviert detaillierte Hex-Logs", action="store_true") + + # Verbindungsparameter (können auch in config.yaml definiert werden) + parser.add_argument("-p", "--port", help="Serieller Port", type=str) + parser.add_argument("-b", "--baud", help="Baudrate", type=int) + parser.add_argument("-t", "--timeout", help="Timeout in Sekunden", type=float) + + # Subparser für Befehle + subparsers = parser.add_subparsers(dest="command", help="Verfügbare Befehle") + + # Befehl: flash_info + flash_info_parser = subparsers.add_parser("flash_info", help="Informationen über den Flash-Speicher des Controllers abfragen") + + # Befehl: fw_status + fw_status_parser = subparsers.add_parser("fw_status", help="Firmware- und Kernel-Status des Controllers abfragen") + + # Befehl: get_file + get_file_parser = subparsers.add_parser("get_file", help="Datei vom Zielsystem herunterladen") + get_file_parser.add_argument("source_path", help="Pfad der Datei auf dem Zielsystem") + get_file_parser.add_argument("dest_path", help="Zielpfad auf dem lokalen System") + + # Befehl: ls + ls_parser = subparsers.add_parser("ls", help="Listet Dateien/Ordner in einem Verzeichnis auf") + ls_parser.add_argument("path", help="Pfad auf dem Zielsystem") + ls_parser.add_argument("-r", "--recursive", help="Rekursiv durch die Verzeichnisse durchsuchen", action="store_true") + + # Befehl: proto + proto_parser = subparsers.add_parser("proto", help="Protokollversion des Controllers abfragen") + + # Befehl: rename + rename_parser = subparsers.add_parser("rename", help="Benennen Sie eine Datei oder einen Ordner auf dem Zielsystem um") + rename_parser.add_argument("source_path", help="Aktueller Pfad der Datei/des Ordners auf dem Zielsystem") + rename_parser.add_argument("dest_path", help="Neuer Pfad der Datei/des Ordners auf dem Zielsystem") + + # Befehl: rm + rm_parser = subparsers.add_parser("rm", help="Entfernt eine Datei oder einen Ordner auf dem Zielsystem") + rm_parser.add_argument("path", help="Pfad auf dem Zielsystem") + + # Befehl: stat + stat_parser = subparsers.add_parser("stat", help="Informationen zu einer Datei/Ordner") + stat_parser.add_argument("path", help="Pfad auf dem Zielsystem") + + args = parser.parse_args() + + if not args.command: + parser.print_help() + sys.exit(0) + + from core.config import cfg + from core.utils import console, console_err + + if args.config: + cfg.custom_path = args.config + + if args.debug: + cfg.debug = True + + console.print("[bold blue]Buzzer Tool v1.0[/bold blue]", justify="left") + + settings = cfg.serial_settings + + settings['debug'] = args.debug + + # Überschreibe Einstellungen mit Kommandozeilenparametern, falls vorhanden + if args.port: + settings['port'] = args.port + if args.baud: + settings['baudrate'] = args.baud + if args.timeout: + settings['timeout'] = args.timeout + + # Ausgabe der aktuellen Einstellungen + port = settings.get('port') + baud = settings.get('baudrate', 'N/A') + timeout = settings.get('timeout', 'N/A') + + if not port: + console_err.print("[error]Fehler: Kein serieller Port angegeben.[/error]") + sys.exit(1) + + if args.debug: + console.print(f" • Port: [info]{port}[/info]") + console.print(f" • Baud: [info]{baud}[/info]") + console.print(f" • Timeout: [info]{timeout:1.2f}s[/info]") + console.print("-" * 78) + + from core.serial_conn import SerialBus + bus = SerialBus(settings) + + try: + bus.open() + if args.command == "get_file": + from core.cmd.get_file import get_file + cmd = get_file(bus) + result = cmd.get(args.source_path, args.dest_path) + cmd.print(result) + elif args.command == "flash_info": + from core.cmd.flash_info import flash_info + cmd = flash_info(bus) + result = cmd.get() + cmd.print(result) + elif args.command == "fw_status": + from core.cmd.fw_status import fw_status + cmd = fw_status(bus) + result = cmd.get() + cmd.print(result) + elif args.command == "ls": + from core.cmd.list_dir import list_dir + cmd = list_dir(bus) + result = cmd.get(args.path, recursive=args.recursive) + cmd.print(result, args.path) + elif args.command == "proto": + from core.cmd.proto import proto + cmd = proto(bus) + result = cmd.get() + cmd.print(result) + elif args.command == "rename": + from core.cmd.rename import rename + cmd = rename(bus) + result = cmd.get(args.source_path, args.dest_path) + cmd.print(result) + elif args.command == "rm": + from core.cmd.rm import rm + cmd = rm(bus) + result = cmd.get(args.path) + cmd.print(result, args.path) + elif args.command == "stat": + from core.cmd.stat import stat + cmd = stat(bus) + result = cmd.get(args.path) + cmd.print(result, args.path) + + finally: + bus.close() + + except FileNotFoundError as e: + console_err.print(f"[error]Fehler: {e}[/error]") + sys.exit(1) + except (TimeoutError, IOError, ValueError) as e: + console_err.print(f"[bold red]KOMMUNIKATIONSFEHLER:[/bold red] [error_msg]{e}[/error_msg]") + sys.exit(1) # Beendet das Script mit Fehlercode 1 für Tests + except Exception as e: + # Hier fangen wir auch deinen neuen ControllerError ab + from core.serial_conn import ControllerError + if isinstance(e, ControllerError): + console_err.print(f"[bold red]CONTROLLER FEHLER:[/bold red] [error_msg]{e}[/error_msg]") + else: + console_err.print(f"[bold red]UNERWARTETER FEHLER:[/bold red] [error_msg]{e}[/error_msg]") + + if args.debug: + import traceback + traceback.print_exc() + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tool/config.yaml b/tool/config.yaml new file mode 100644 index 0000000..bf40082 --- /dev/null +++ b/tool/config.yaml @@ -0,0 +1,4 @@ +serial: + port: "/dev/cu.usbmodem83401" + baudrate: 115200 + timeout: 1.0 diff --git a/tool/core/cmd/flash_info.py b/tool/core/cmd/flash_info.py new file mode 100644 index 0000000..ec7d2b7 --- /dev/null +++ b/tool/core/cmd/flash_info.py @@ -0,0 +1,53 @@ +# tool/core/cmd/flash_info.py +import struct +from core.utils import console, console_err +from core.protocol import COMMANDS, ERRORS + +class flash_info: + def __init__(self, bus): + self.bus = bus + + + def get(self): + import struct + self.bus.send_request(COMMANDS['get_flash_info']) + + data = self.bus.receive_response(length=21) + if not data or data.get('type') == 'error': + return None + + payload = data['data'] + ext_block_size = struct.unpack('> 24) & 0xFF, + 'fw_minor': (app_version_raw >> 16) & 0xFF, + 'fw_patch': (app_version_raw >> 8)& 0xFF, + 'kernel_major': (ker_version_raw >> 16) & 0xFF, + 'kernel_minor': (ker_version_raw >> 8) & 0xFF, + 'kernel_patch': ker_version_raw & 0xFF, + 'fw_string': fw_string, + 'kernel_string': f"{(ker_version_raw >> 16) & 0xFF}.{(ker_version_raw >> 8) & 0xFF}.{ker_version_raw & 0xFF}" + } + return result + + def print(self, result): + if not result: + return + + status = "UNKNOWN" + if result['status'] == 0x00: status = "CONFIRMED" + elif result['status'] == 0x01: status = "PENDING" + elif result['status'] == 0x02: status = "TESTING" + console.print(f"[info]Firmware Status[/info] des Controllers ist [info]{status}[/info]:") + console.print(f" • Firmware: [info]{result['fw_string']}[/info] ({result['fw_major']}.{result['fw_minor']}.{result['fw_patch']})") + console.print(f" • Kernel: [info]{result['kernel_string']}[/info] ({result['kernel_major']}.{result['kernel_minor']}.{result['kernel_patch']})") \ No newline at end of file diff --git a/tool/core/cmd/get_file.py b/tool/core/cmd/get_file.py new file mode 100644 index 0000000..bfb4cf9 --- /dev/null +++ b/tool/core/cmd/get_file.py @@ -0,0 +1,76 @@ +# tool/core/cmd/get_file.py +import struct +import zlib +from pathlib import Path +from core.utils import console, console_err +from core.protocol import COMMANDS + +class get_file: + def __init__(self, bus): + self.bus = bus + + def get(self, source_path: str, dest_path: str): + try: + p = Path(dest_path) + p.parent.mkdir(parents=True, exist_ok=True) + with open(p, 'wb') as f: + pass + except Exception as e: + console_err.print(f"Fehler: Kann Zieldatei nicht anlegen: {e}") + return None + + source_path_bytes = source_path.encode('utf-8') + payload = struct.pack('B', len(source_path_bytes)) + source_path_bytes + device_file_crc = None + try: + self.bus.send_request(COMMANDS['crc_32'], payload) + crc_resp = self.bus.receive_response(length=4) + if crc_resp and crc_resp.get('type') == 'response': + device_file_crc = struct.unpack(' VERSION["max_protocol_version"]: + if VERSION["min_protocol_version"] == VERSION["max_protocol_version"]: + expected = f"Version {VERSION['min_protocol_version']}" + else: + expected = f"Version {VERSION['min_protocol_version']} bis {VERSION['max_protocol_version']}" + raise ValueError(f"Inkompatibles Protokoll. Controller spricht {data['protocol_version']}, erwartet wird {expected}.") + else: + raise ValueError("Keine gültige Antwort auf Protokollversion erhalten.") + except serial.SerialException as e: + console_err.print(f"[bold red]Serieller Fehler:[/bold red] [error_msg]{e}[/error_msg]") + raise + except Exception as e: + console_err.print(f"[bold red]Unerwarteter Fehler beim Öffnen:[/bold red] [error_msg]{e}[/error_msg]") + raise + + def flush_input(self): + """Leert den Empfangspuffer der seriellen Schnittstelle.""" + if self.connection and self.connection.is_open: + self.connection.reset_input_buffer() + + def close(self): + """Schließt die Verbindung sauber.""" + if self.connection and self.connection.is_open: + self.connection.close() + if self.debug: console.print(f"Verbindung zu [info]{self.port}[/info] geschlossen.") + + def send_binary(self, data: bytes): + """Sendet Rohdaten und loggt sie im Hex-Format.""" + if not self.connection or not self.connection.is_open: + raise ConnectionError("Port ist nicht geöffnet.") + + self.connection.write(data) + + if self.debug: + hex_data = data.hex(' ').upper() + console.print(f"TX -> [grey62]{hex_data}[/grey62]") + + def _read_exact(self, length: int, context: str = "Daten") -> bytes: + data = bytearray() + while len(data) < length: + try: + chunk = self.connection.read(length - len(data)) + except serial.SerialException as e: + raise IOError(f"Serielle Verbindung verloren beim Lesen von {context}: {e}") from e + if not chunk: + raise TimeoutError(f"Timeout beim Lesen von {context}: {len(data)}/{length} Bytes.") + data.extend(chunk) + return bytes(data) + + def wait_for_sync(self, sync_seq: bytes, max_time: float = 2.0): + """Wartet maximal max_time Sekunden auf die Sync-Sequenz.""" + buffer = b"" + start_time = time.time() + + if self.debug: + console.print(f"[bold cyan]Warte auf SYNC-Sequenz:[/bold cyan] [grey62]{sync_seq.hex(' ').upper()}[/grey62]") + + # Kurzer interner Timeout für reaktive Schleife + original_timeout = self.connection.timeout + self.connection.timeout = 0.1 + + try: + while (time.time() - start_time) < max_time: + char = self.connection.read(1) + if not char: + continue + + buffer += char + if len(buffer) > len(sync_seq): + buffer = buffer[1:] + + if buffer == sync_seq: + if self.debug: console.print("[bold cyan]RX <- SYNC OK[/bold cyan]") + return True + return False + finally: + self.connection.timeout = original_timeout + + def send_request(self, cmd_id: int, payload: bytes = b''): + self.flush_input() + frame_type = struct.pack('B', FRAME_TYPES['request']) + cmd_byte = struct.pack('B', cmd_id) + + full_frame = SYNC_SEQ + frame_type + cmd_byte + if payload: + full_frame += payload + self.send_binary(full_frame) + + def receive_ack(self, timeout: float = None): + wait_time = timeout if timeout is not None else self.timeout + + if not self.wait_for_sync(SYNC_SEQ, max_time=wait_time): + raise TimeoutError(f"SYNC-Sequenz nicht innerhalb von {wait_time}s gefunden.") + + ftype_raw = self.connection.read(1) + if not ftype_raw: + raise TimeoutError("Timeout beim Lesen des Frame-Typs.") + ftype = ftype_raw[0] + + if ftype == FRAME_TYPES['error']: + err_code_raw = self.connection.read(1) + err_code = err_code_raw[0] if err_code_raw else 0xFF + err_name = ERRORS.get(err_code, "UNKNOWN") + raise ControllerError(err_code, err_name) + + elif ftype == FRAME_TYPES['ack']: + if self.debug: + console.print(f"[green]ACK empfangen[/green]") + return {"type": "ack"} + raise ValueError(f"Unerwarteter Frame-Typ (0x{FRAME_TYPES['ack']:02X} (ACK) erwartet): 0x{ftype:02X}") + + def receive_response(self, length: int, timeout: float = None, varlen_params: int = 0): + wait_time = timeout if timeout is not None else self.timeout + + if not self.wait_for_sync(SYNC_SEQ, max_time=wait_time): + raise TimeoutError(f"SYNC-Sequenz nicht innerhalb von {wait_time}s gefunden.") + + ftype_raw = self.connection.read(1) + if not ftype_raw: + raise TimeoutError("Timeout beim Lesen des Frame-Typs.") + ftype = ftype_raw[0] + + if ftype == FRAME_TYPES['error']: + err_code_raw = self.connection.read(1) + err_code = err_code_raw[0] if err_code_raw else 0xFF + err_name = ERRORS.get(err_code, "UNKNOWN") + raise ControllerError(err_code, err_name) + + elif ftype == FRAME_TYPES['response']: + data = self.connection.read(length) + for varlen_param in range(varlen_params): + length_byte = self.connection.read(1) + if not length_byte: + raise TimeoutError("Timeout beim Lesen der Länge eines variablen Parameters.") + param_length = length_byte[0] + param_data = self.connection.read(param_length) + if not param_data: + raise TimeoutError("Timeout beim Lesen eines variablen Parameters.") + data += length_byte + param_data + if self.debug: + console.print(f"RX <- [grey62]{data.hex(' ').upper()}[/grey62]") + if len(data) < length: + raise IOError(f"Unvollständiges Paket: {len(data)}/{length} Bytes.") + return {"type": "response", "data": data} + + raise ValueError(f"Unerwarteter Frame-Typ: 0x{ftype:02X}") + + def receive_list(self): + """Liest eine Liste von Einträgen, bis list_end kommt.""" + is_list = False + list_items = [] + + while True: + if not self.wait_for_sync(SYNC_SEQ): + raise TimeoutError("Timeout beim Warten auf Sync im List-Modus.") + + ftype = self.connection.read(1)[0] + + if ftype == FRAME_TYPES['list_start']: + is_list = True + list_items = [] + elif ftype == FRAME_TYPES['list_chunk']: + if not is_list: raise ValueError("Chunk ohne Start.") + length = struct.unpack('