From 80c0e825a726e8bfcc9a97e3ce1a6846e90894dd Mon Sep 17 00:00:00 2001 From: Eduard Iten Date: Wed, 25 Feb 2026 10:09:17 +0100 Subject: [PATCH] added python tool inital version --- .gitignore | 21 ++++ buzzer_tool/buzzer.py | 72 ++++++++++++ buzzer_tool/config.yaml | 4 + buzzer_tool/core/commands/info.py | 35 ++++++ buzzer_tool/core/commands/ls.py | 84 +++++++++++++ buzzer_tool/core/commands/xy | 0 buzzer_tool/core/config.py | 40 +++++++ buzzer_tool/core/connection.py | 111 ++++++++++++++++++ buzzer_tool/requirements.txt | 2 + CMakeLists.txt => firmware/CMakeLists.txt | 0 VERSION => firmware/VERSION | 0 .../boards}/nrf52840dk_nrf52840.conf | 0 .../boards}/nrf52840dk_nrf52840.overlay | 0 pm_static.yml => firmware/pm_static.yml | 0 prj.conf => firmware/prj.conf | 0 {src => firmware/src}/audio.c | 0 {src => firmware/src}/audio.h | 0 {src => firmware/src}/fs.c | 0 {src => firmware/src}/fs.h | 0 {src => firmware/src}/io.c | 0 {src => firmware/src}/io.h | 0 {src => firmware/src}/main.c | 0 {src => firmware/src}/protocol.c | 0 {src => firmware/src}/protocol.h | 0 {src => firmware/src}/usb.c | 0 {src => firmware/src}/usb.h | 0 26 files changed, 369 insertions(+) create mode 100644 buzzer_tool/buzzer.py create mode 100644 buzzer_tool/config.yaml create mode 100644 buzzer_tool/core/commands/info.py create mode 100644 buzzer_tool/core/commands/ls.py create mode 100644 buzzer_tool/core/commands/xy create mode 100644 buzzer_tool/core/config.py create mode 100644 buzzer_tool/core/connection.py create mode 100644 buzzer_tool/requirements.txt rename CMakeLists.txt => firmware/CMakeLists.txt (100%) rename VERSION => firmware/VERSION (100%) rename {boards => firmware/boards}/nrf52840dk_nrf52840.conf (100%) rename {boards => firmware/boards}/nrf52840dk_nrf52840.overlay (100%) rename pm_static.yml => firmware/pm_static.yml (100%) rename prj.conf => firmware/prj.conf (100%) rename {src => firmware/src}/audio.c (100%) rename {src => firmware/src}/audio.h (100%) rename {src => firmware/src}/fs.c (100%) rename {src => firmware/src}/fs.h (100%) rename {src => firmware/src}/io.c (100%) rename {src => firmware/src}/io.h (100%) rename {src => firmware/src}/main.c (100%) rename {src => firmware/src}/protocol.c (100%) rename {src => firmware/src}/protocol.h (100%) rename {src => firmware/src}/usb.c (100%) rename {src => firmware/src}/usb.h (100%) diff --git a/.gitignore b/.gitignore index a5309e6..af32dae 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,22 @@ +# Build-Verzeichnisse (Zephyr) build*/ + +# Virtuelle Umgebungen und Umgebungsvariablen +venv/ +.venv/ +env/ +.env + +# Python Cache und kompilierte Dateien +__pycache__/ +*.py[cod] +*$py.class +*.so + +# OS-spezifische Dateien +.DS_Store +Thumbs.db + +# Tooling / IDEs (optional) +.vscode/ +.idea/ \ No newline at end of file diff --git a/buzzer_tool/buzzer.py b/buzzer_tool/buzzer.py new file mode 100644 index 0000000..78a6b87 --- /dev/null +++ b/buzzer_tool/buzzer.py @@ -0,0 +1,72 @@ +# buzzer.py +import argparse +import sys +from core.config import load_config +from core.connection import BuzzerConnection, BuzzerError +from core.commands import info, ls + +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)") + + # 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") + + # Argumente parsen + args = parser.parse_args() + 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("-" * 55) + + if not config.get("port"): + print("Abbruch: Es muss ein Port in der config.yaml oder via --port definiert werden.") + sys.exit(1) + + try: + with BuzzerConnection(config) as conn: + # 1. Immer die Info holen und anzeigen + sys_info = info.execute(conn) + print(f"Buzzer Firmware: v{sys_info['app_version']} (Protokoll v{sys_info['protocol_version']})") + print(f"LittleFS Status: {sys_info['used_kb']:.1f} KB / {sys_info['total_kb']:.1f} KB belegt ({sys_info['percent_used']:.1f}%)") + print("-" * 55) + + # 2. Spezifisches Kommando ausführen + if args.command == "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 == "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 new file mode 100644 index 0000000..ee8d3b4 --- /dev/null +++ b/buzzer_tool/config.yaml @@ -0,0 +1,4 @@ +# config.yaml +serial: + port: "COM15" + baudrate: 112500 \ No newline at end of file diff --git a/buzzer_tool/core/commands/info.py b/buzzer_tool/core/commands/info.py new file mode 100644 index 0000000..9d10c39 --- /dev/null +++ b/buzzer_tool/core/commands/info.py @@ -0,0 +1,35 @@ +# core/commands/info.py +from core.connection import BuzzerError + +def execute(conn) -> dict: + """Holt die Systeminformationen und gibt sie als strukturiertes Dictionary zurück.""" + lines = conn.send_command("info") + if not lines: + raise BuzzerError("Keine Antwort auf 'info' empfangen.") + + parts = lines[0].split(';') + if len(parts) != 5: + raise BuzzerError(f"Unerwartetes Info-Format: {lines[0]}") + + protocol_version = int(parts[0]) + if protocol_version != 1: + raise BuzzerError(f"Inkompatibles Protokoll: Gerät nutzt v{protocol_version}, Host erwartet v1.") + + app_version = parts[1] + f_frsize = int(parts[2]) + f_blocks = int(parts[3]) + f_bfree = int(parts[4]) + + 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 + } \ No newline at end of file diff --git a/buzzer_tool/core/commands/ls.py b/buzzer_tool/core/commands/ls.py new file mode 100644 index 0000000..771495f --- /dev/null +++ b/buzzer_tool/core/commands/ls.py @@ -0,0 +1,84 @@ +# 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.send_command(f"ls {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/xy b/buzzer_tool/core/commands/xy new file mode 100644 index 0000000..e69de29 diff --git a/buzzer_tool/core/config.py b/buzzer_tool/core/config.py new file mode 100644 index 0000000..cf3483c --- /dev/null +++ b/buzzer_tool/core/config.py @@ -0,0 +1,40 @@ +# core/config.py +import os +import sys +import yaml + +DEFAULT_CONFIG = { + "port": None, + "baudrate": 115200, + "timeout": 5.0 +} + +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"]) + 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 new file mode 100644 index 0000000..4ba9c5e --- /dev/null +++ b/buzzer_tool/core/connection.py @@ -0,0 +1,111 @@ +# core/connection.py +import serial +import time + +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.serial = None + + def __enter__(self): + if not self.port: + raise ValueError("Kein serieller Port konfiguriert.") + + # 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 send_command(self, command: str, custom_timeout: float = None) -> 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 serial.SerialTimeoutException: + raise TimeoutError(f"Schreib-Timeout am Port {self.port}. Ist das Gerät blockiert?") + + 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"): + err_code = line.split(" ")[1] if " " in line else "UNKNOWN" + raise BuzzerError(f"Controller meldet Fehlercode: {err_code}") + else: + lines.append(line) + except Exception as e: + raise BuzzerError(f"Fehler beim Lesen der Antwort: {e}") + else: + time.sleep(0.01) + + raise TimeoutError(f"Lese-Timeout ({eff_timeout}s) beim Warten auf Antwort für: '{command}'") + +def send_binary(self, filepath: str, chunk_size: int = 512, timeout: float = 10.0): + """ + Ü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: {line}") + time.sleep(0.01) + + 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: + chunk = f.read(chunk_size) + if not chunk: + break + self.serial.write(chunk) + # Flush blockiert, bis die Daten an den OS-USB-Treiber übergeben wurden + self.serial.flush() + bytes_sent += 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: {line}") + time.sleep(0.01) + + raise TimeoutError("Zeitüberschreitung nach Binärtransfer (kein OK empfangen).") \ No newline at end of file diff --git a/buzzer_tool/requirements.txt b/buzzer_tool/requirements.txt new file mode 100644 index 0000000..c43cbbf --- /dev/null +++ b/buzzer_tool/requirements.txt @@ -0,0 +1,2 @@ +pyyaml +pyserial \ No newline at end of file diff --git a/CMakeLists.txt b/firmware/CMakeLists.txt similarity index 100% rename from CMakeLists.txt rename to firmware/CMakeLists.txt diff --git a/VERSION b/firmware/VERSION similarity index 100% rename from VERSION rename to firmware/VERSION diff --git a/boards/nrf52840dk_nrf52840.conf b/firmware/boards/nrf52840dk_nrf52840.conf similarity index 100% rename from boards/nrf52840dk_nrf52840.conf rename to firmware/boards/nrf52840dk_nrf52840.conf diff --git a/boards/nrf52840dk_nrf52840.overlay b/firmware/boards/nrf52840dk_nrf52840.overlay similarity index 100% rename from boards/nrf52840dk_nrf52840.overlay rename to firmware/boards/nrf52840dk_nrf52840.overlay diff --git a/pm_static.yml b/firmware/pm_static.yml similarity index 100% rename from pm_static.yml rename to firmware/pm_static.yml diff --git a/prj.conf b/firmware/prj.conf similarity index 100% rename from prj.conf rename to firmware/prj.conf diff --git a/src/audio.c b/firmware/src/audio.c similarity index 100% rename from src/audio.c rename to firmware/src/audio.c diff --git a/src/audio.h b/firmware/src/audio.h similarity index 100% rename from src/audio.h rename to firmware/src/audio.h diff --git a/src/fs.c b/firmware/src/fs.c similarity index 100% rename from src/fs.c rename to firmware/src/fs.c diff --git a/src/fs.h b/firmware/src/fs.h similarity index 100% rename from src/fs.h rename to firmware/src/fs.h diff --git a/src/io.c b/firmware/src/io.c similarity index 100% rename from src/io.c rename to firmware/src/io.c diff --git a/src/io.h b/firmware/src/io.h similarity index 100% rename from src/io.h rename to firmware/src/io.h diff --git a/src/main.c b/firmware/src/main.c similarity index 100% rename from src/main.c rename to firmware/src/main.c diff --git a/src/protocol.c b/firmware/src/protocol.c similarity index 100% rename from src/protocol.c rename to firmware/src/protocol.c diff --git a/src/protocol.h b/firmware/src/protocol.h similarity index 100% rename from src/protocol.h rename to firmware/src/protocol.h diff --git a/src/usb.c b/firmware/src/usb.c similarity index 100% rename from src/usb.c rename to firmware/src/usb.c diff --git a/src/usb.h b/firmware/src/usb.h similarity index 100% rename from src/usb.h rename to firmware/src/usb.h