pre uart exchange
This commit is contained in:
@@ -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()
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# config.yaml
|
|
||||||
serial:
|
|
||||||
port: "/dev/cu.usbmodem83401"
|
|
||||||
baudrate: 2500000
|
|
||||||
timeout: 1
|
|
||||||
@@ -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
|
|
||||||
@@ -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,
|
|
||||||
}
|
|
||||||
@@ -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}")
|
|
||||||
@@ -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")
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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}")
|
|
||||||
@@ -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
|
|
||||||
@@ -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}")
|
|
||||||
@@ -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),
|
|
||||||
}
|
|
||||||
@@ -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)")
|
|
||||||
@@ -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}")
|
|
||||||
@@ -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}")
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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("<BBHI", frame_type, command_id, sequence, payload_len)
|
|
||||||
header_crc = self._crc16_ccitt_false(header_no_sync_crc)
|
|
||||||
header = SYNC + header_no_sync_crc + struct.pack("<H", header_crc)
|
|
||||||
payload_crc = binascii.crc32(payload) & 0xFFFFFFFF
|
|
||||||
return header + payload + struct.pack("<I", payload_crc)
|
|
||||||
|
|
||||||
def _write_frame(self, frame: bytes) -> 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("<BBHIH", rest_header)
|
|
||||||
|
|
||||||
calc_header_crc = self._crc16_ccitt_false(struct.pack("<BBHI", frame_type, command_id, sequence, payload_len))
|
|
||||||
if rx_header_crc != calc_header_crc:
|
|
||||||
raise BuzzerError(
|
|
||||||
f"Ungültige Header-CRC: empfangen 0x{rx_header_crc:04X}, erwartet 0x{calc_header_crc:04X}"
|
|
||||||
)
|
|
||||||
|
|
||||||
remaining = deadline - time.monotonic()
|
|
||||||
if remaining <= 0:
|
|
||||||
raise TimeoutError("Timeout beim Lesen des Payloads.")
|
|
||||||
payload = self._read_exact(payload_len, remaining) if payload_len else b""
|
|
||||||
|
|
||||||
remaining = deadline - time.monotonic()
|
|
||||||
if remaining <= 0:
|
|
||||||
raise TimeoutError("Timeout beim Lesen der Payload-CRC.")
|
|
||||||
rx_payload_crc = struct.unpack("<I", self._read_exact(4, remaining))[0]
|
|
||||||
calc_payload_crc = binascii.crc32(payload) & 0xFFFFFFFF
|
|
||||||
if rx_payload_crc != calc_payload_crc:
|
|
||||||
raise BuzzerError(
|
|
||||||
f"Ungültige Payload-CRC: empfangen 0x{rx_payload_crc:08X}, erwartet 0x{calc_payload_crc:08X}"
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"frame_type": frame_type,
|
|
||||||
"command_id": command_id,
|
|
||||||
"sequence": sequence,
|
|
||||||
"payload": payload,
|
|
||||||
}
|
|
||||||
|
|
||||||
def send_binary_command(self, command_id: int, payload: bytes = b"", timeout: float = 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, 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("<H", payload)[0]
|
|
||||||
|
|
||||||
def get_firmware_status(self, timeout: float = None) -> 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("<IIII", payload)
|
|
||||||
if path_max_len > 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("<I", payload[2:6])[0]
|
|
||||||
name = payload[6:6 + name_len].decode("utf-8", errors="replace")
|
|
||||||
|
|
||||||
if entry_type == 0:
|
|
||||||
type_char = "F"
|
|
||||||
elif entry_type == 1:
|
|
||||||
type_char = "D"
|
|
||||||
else:
|
|
||||||
raise BuzzerError(f"Ungültiger LIST_DIR entry_type: {entry_type}")
|
|
||||||
|
|
||||||
lines.append(f"{type_char},{size},{name}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
if frame_type == FRAME_RESP_STREAM_END:
|
|
||||||
return lines
|
|
||||||
|
|
||||||
if not stream_started and frame_type == FRAME_RESP_DATA:
|
|
||||||
text = payload.decode("utf-8", errors="replace")
|
|
||||||
return [line for line in text.splitlines() if line]
|
|
||||||
|
|
||||||
if frame_type == FRAME_RESP_ACK:
|
|
||||||
return lines
|
|
||||||
|
|
||||||
raise BuzzerError(f"Unerwarteter LIST_DIR Response-Typ: 0x{frame_type:02X}")
|
|
||||||
|
|
||||||
def check_file_crc(self, path: str, timeout: float = None) -> 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("<I", payload)[0]
|
|
||||||
|
|
||||||
def mkdir(self, path: str, timeout: float = None) -> 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("<I", payload[1:5])[0]
|
|
||||||
return {
|
|
||||||
"type": type_char,
|
|
||||||
"size": int(size),
|
|
||||||
}
|
|
||||||
|
|
||||||
def rename(self, old_path: str, new_path: str, timeout: float = None) -> 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("<I", start_response["payload"])[0]
|
|
||||||
received = 0
|
|
||||||
running_crc = 0
|
|
||||||
chunks = bytearray()
|
|
||||||
chunk_size = 4096
|
|
||||||
|
|
||||||
while received < expected_len:
|
|
||||||
to_read = min(chunk_size, expected_len - received)
|
|
||||||
chunk = self._read_exact(to_read, eff_timeout)
|
|
||||||
chunks.extend(chunk)
|
|
||||||
running_crc = binascii.crc32(chunk, running_crc) & 0xFFFFFFFF
|
|
||||||
received += len(chunk)
|
|
||||||
if progress_callback:
|
|
||||||
progress_callback(len(chunk), received, expected_len)
|
|
||||||
|
|
||||||
end_response = self._read_frame(timeout=eff_timeout)
|
|
||||||
if end_response["sequence"] != sequence:
|
|
||||||
raise BuzzerError(
|
|
||||||
f"Antwort-Sequenz passt nicht: erwartet {sequence}, erhalten {end_response['sequence']}"
|
|
||||||
)
|
|
||||||
if end_response["command_id"] != CMD_GET_FILE:
|
|
||||||
raise BuzzerError(
|
|
||||||
f"Antwort-Kommando passt nicht: erwartet 0x{CMD_GET_FILE:02X}, erhalten 0x{end_response['command_id']:02X}"
|
|
||||||
)
|
|
||||||
if end_response["frame_type"] == FRAME_RESP_ERROR:
|
|
||||||
self._raise_error_from_payload(end_response["payload"])
|
|
||||||
if end_response["frame_type"] != FRAME_RESP_DATA or len(end_response["payload"]) != 4:
|
|
||||||
raise BuzzerError("Ungültige GET_FILE-Endantwort (erwartet DATA mit 4 Byte CRC32)")
|
|
||||||
|
|
||||||
expected_crc32 = struct.unpack("<I", end_response["payload"])[0]
|
|
||||||
if running_crc != expected_crc32:
|
|
||||||
raise BuzzerError(
|
|
||||||
f"GET_FILE CRC32-Mismatch: empfangen 0x{running_crc:08X}, erwartet 0x{expected_crc32:08X}"
|
|
||||||
)
|
|
||||||
|
|
||||||
return bytes(chunks)
|
|
||||||
|
|
||||||
def put_file_start(self, path: str, total_len: int, expected_crc32: int, timeout: float = None) -> None:
|
|
||||||
if total_len < 0:
|
|
||||||
raise BuzzerError("Dateigröße darf nicht negativ sein.")
|
|
||||||
payload = self._encode_path_payload(path) + struct.pack("<II", int(total_len), int(expected_crc32) & 0xFFFFFFFF)
|
|
||||||
response = self.send_binary_command(CMD_PUT_FILE_START, payload, timeout=timeout)
|
|
||||||
if len(response) != 0:
|
|
||||||
raise BuzzerError(f"Unerwartete Payload für PUT_FILE_START: {len(response)} Bytes")
|
|
||||||
|
|
||||||
def put_file_chunk(self, chunk: bytes, timeout: float = None) -> 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("<II", len(data), expected_crc32)
|
|
||||||
start_frame = self._build_frame(FRAME_REQ, CMD_PUT_FILE_START, sequence, start_payload)
|
|
||||||
self._write_frame(start_frame)
|
|
||||||
|
|
||||||
sent = 0
|
|
||||||
while sent < len(data):
|
|
||||||
chunk = data[sent:sent + chunk_size]
|
|
||||||
try:
|
|
||||||
self.serial.write(chunk)
|
|
||||||
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
|
|
||||||
sent += len(chunk)
|
|
||||||
if progress_callback:
|
|
||||||
progress_callback(len(chunk), sent, len(data))
|
|
||||||
|
|
||||||
self.serial.flush()
|
|
||||||
|
|
||||||
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_PUT_FILE_START:
|
|
||||||
raise BuzzerError(
|
|
||||||
f"Antwort-Kommando passt nicht: erwartet 0x{CMD_PUT_FILE_START: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 für PUT_FILE_START: 0x{response['frame_type']:02X}")
|
|
||||||
|
|
||||||
if len(response["payload"]) != 0:
|
|
||||||
raise BuzzerError(f"Unerwartete Payload für PUT_FILE_START: {len(response['payload'])} Bytes")
|
|
||||||
return expected_crc32
|
|
||||||
|
|
||||||
def fw_put_data(
|
|
||||||
self,
|
|
||||||
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.")
|
|
||||||
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("<II", len(data), expected_crc32)
|
|
||||||
start_frame = self._build_frame(FRAME_REQ, CMD_PUT_FW_START, sequence, start_payload)
|
|
||||||
self._write_frame(start_frame)
|
|
||||||
|
|
||||||
sent = 0
|
|
||||||
while sent < len(data):
|
|
||||||
chunk = data[sent:sent + chunk_size]
|
|
||||||
try:
|
|
||||||
self.serial.write(chunk)
|
|
||||||
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
|
|
||||||
sent += len(chunk)
|
|
||||||
if progress_callback:
|
|
||||||
progress_callback(len(chunk), sent, len(data))
|
|
||||||
|
|
||||||
self.serial.flush()
|
|
||||||
|
|
||||||
response = self._read_frame(timeout=eff_timeout)
|
|
||||||
finally:
|
|
||||||
self.serial.write_timeout = old_write_timeout
|
|
||||||
|
|
||||||
if response["sequence"] != sequence:
|
|
||||||
raise BuzzerError(
|
|
||||||
f"Antwort-Sequenz passt nicht: erwartet {sequence}, erhalten {response['sequence']}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if response["command_id"] != CMD_PUT_FW_START:
|
|
||||||
raise BuzzerError(
|
|
||||||
f"Antwort-Kommando passt nicht: erwartet 0x{CMD_PUT_FW_START: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 für PUT_FW_START: 0x{response['frame_type']:02X}")
|
|
||||||
|
|
||||||
if len(response["payload"]) != 0:
|
|
||||||
raise BuzzerError(f"Unerwartete Payload für PUT_FW_START: {len(response['payload'])} Bytes")
|
|
||||||
|
|
||||||
return expected_crc32
|
|
||||||
|
|
||||||
def get_tag_blob(self, path: str, timeout: float = 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_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("<I", payload)[0]
|
|
||||||
continue
|
|
||||||
|
|
||||||
if frame_type == FRAME_RESP_STREAM_CHUNK:
|
|
||||||
chunks.extend(payload)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if frame_type == FRAME_RESP_STREAM_END:
|
|
||||||
if expected_len is not None and len(chunks) != expected_len:
|
|
||||||
raise BuzzerError(
|
|
||||||
f"Tag-Blob-Länge inkonsistent: erwartet {expected_len}, erhalten {len(chunks)}"
|
|
||||||
)
|
|
||||||
return bytes(chunks)
|
|
||||||
|
|
||||||
if frame_type == FRAME_RESP_DATA:
|
|
||||||
return payload
|
|
||||||
|
|
||||||
raise BuzzerError(f"Unerwarteter GET_TAG_BLOB Response-Typ: 0x{frame_type:02X}")
|
|
||||||
|
|
||||||
def set_tag_blob(self, path: str, blob: bytes, timeout: float = None, chunk_size: int = 192) -> 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("<H", len(blob))
|
|
||||||
self.send_binary_command(CMD_SET_TAG_BLOB_START, start_payload, timeout=timeout)
|
|
||||||
|
|
||||||
offset = 0
|
|
||||||
while offset < len(blob):
|
|
||||||
chunk = blob[offset:offset + chunk_size]
|
|
||||||
self.send_binary_command(CMD_SET_TAG_BLOB_CHUNK, chunk, timeout=timeout)
|
|
||||||
offset += len(chunk)
|
|
||||||
|
|
||||||
self.send_binary_command(CMD_SET_TAG_BLOB_END, b"", timeout=timeout)
|
|
||||||
|
|
||||||
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 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).")
|
|
||||||
@@ -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()
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
pyyaml
|
|
||||||
pyserial
|
|
||||||
@@ -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 <<EOF
|
|
||||||
Usage: $(basename "$0") -p <port> [-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"
|
|
||||||
@@ -13,7 +13,6 @@ CONFIG_FLASH_MAP=y
|
|||||||
CONFIG_FILE_SYSTEM=y
|
CONFIG_FILE_SYSTEM=y
|
||||||
CONFIG_FILE_SYSTEM_LITTLEFS=y
|
CONFIG_FILE_SYSTEM_LITTLEFS=y
|
||||||
CONFIG_FILE_SYSTEM_MKFS=y
|
CONFIG_FILE_SYSTEM_MKFS=y
|
||||||
CONFIG_CRC=y
|
|
||||||
CONFIG_FS_LITTLEFS_READ_SIZE=64
|
CONFIG_FS_LITTLEFS_READ_SIZE=64
|
||||||
CONFIG_FS_LITTLEFS_PROG_SIZE=256
|
CONFIG_FS_LITTLEFS_PROG_SIZE=256
|
||||||
CONFIG_FS_LITTLEFS_CACHE_SIZE=512
|
CONFIG_FS_LITTLEFS_CACHE_SIZE=512
|
||||||
@@ -25,7 +24,7 @@ CONFIG_MAIN_STACK_SIZE=2048
|
|||||||
CONFIG_USB_DEVICE_STACK=y
|
CONFIG_USB_DEVICE_STACK=y
|
||||||
CONFIG_DEPRECATION_TEST=y
|
CONFIG_DEPRECATION_TEST=y
|
||||||
CONFIG_USB_DEVICE_MANUFACTURER="Eduard Iten"
|
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_DEVICE_PID=0x0001
|
||||||
CONFIG_USB_DRIVER_LOG_LEVEL_ERR=y
|
CONFIG_USB_DRIVER_LOG_LEVEL_ERR=y
|
||||||
CONFIG_USB_DEVICE_LOG_LEVEL_ERR=y
|
CONFIG_USB_DEVICE_LOG_LEVEL_ERR=y
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include <zephyr/fs/littlefs.h>
|
#include <zephyr/fs/littlefs.h>
|
||||||
#include <zephyr/drivers/flash.h>
|
#include <zephyr/drivers/flash.h>
|
||||||
|
#include <zephyr/storage/flash_map.h>
|
||||||
#include <zephyr/dfu/flash_img.h>
|
#include <zephyr/dfu/flash_img.h>
|
||||||
#include <zephyr/dfu/mcuboot.h>
|
#include <zephyr/dfu/mcuboot.h>
|
||||||
#include <zephyr/pm/device.h>
|
#include <zephyr/pm/device.h>
|
||||||
@@ -416,4 +417,79 @@ int flash_write_firmware_block(const uint8_t *buffer, size_t length, bool is_las
|
|||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
return 0;
|
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;
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
#include <zephyr/fs/fs.h>
|
#include <zephyr/fs/fs.h>
|
||||||
|
|
||||||
|
#define MAX_PATH_LEN 32U
|
||||||
|
|
||||||
typedef struct slot_info_t {
|
typedef struct slot_info_t {
|
||||||
size_t start_addr;
|
size_t start_addr;
|
||||||
size_t size;
|
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);
|
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
|
#endif // FS_H
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -8,44 +8,42 @@
|
|||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
PS_WAIT_SYNC = 0,
|
PS_WAIT_SYNC = 0,
|
||||||
PS_READ_HEADER,
|
PS_READ_FRAME_TYPE,
|
||||||
PS_READ_PAYLOAD,
|
PS_READ_REQ,
|
||||||
PS_READ_PAYLOAD_CRC,
|
PS_READ_REQ_DATA,
|
||||||
} protocol_state_t;
|
} protocol_state_t;
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
CMD_INVALID = 0,
|
|
||||||
CMD_GET_PROTOCOL_VERSION = 0x00,
|
CMD_GET_PROTOCOL_VERSION = 0x00,
|
||||||
CMD_GET_FIRMWARE_STATUS = 0x01,
|
CMD_GET_FIRMWARE_STATUS = 0x01,
|
||||||
CMD_GET_FLASH_STATUS = 0x02,
|
CMD_GET_FLASH_STATUS = 0x02,
|
||||||
CMD_CONFIRM_FIRMWARE = 0x03,
|
CMD_CONFIRM_FIRMWARE = 0x03,
|
||||||
CMD_REBOOT = 0x04,
|
CMD_REBOOT = 0x04,
|
||||||
|
|
||||||
CMD_LIST_DIR = 0x10,
|
CMD_LIST_DIR = 0x10,
|
||||||
CMD_CHECK_FILE_CRC = 0x11,
|
CMD_CHECK_FILE_CRC = 0x11,
|
||||||
CMD_MKDIR = 0x12,
|
CMD_MKDIR = 0x12,
|
||||||
CMD_RM = 0x13,
|
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_STAT = 0x18,
|
||||||
CMD_RENAME = 0x19,
|
CMD_RENAME = 0x19,
|
||||||
CMD_RM_R = 0x1A,
|
|
||||||
CMD_GET_FILE = 0x1B,
|
CMD_PUT_FILE = 0x20,
|
||||||
CMD_GET_TAG_BLOB = 0x20,
|
CMD_PUT_FW = 0x21,
|
||||||
CMD_SET_TAG_BLOB_START = 0x21,
|
CMD_GET_FILE = 0x22,
|
||||||
CMD_SET_TAG_BLOB_CHUNK = 0x22,
|
|
||||||
CMD_SET_TAG_BLOB_END = 0x23,
|
|
||||||
} protocol_cmd_t;
|
} protocol_cmd_t;
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
FRAME_REQ = 0x01,
|
FRAME_REQ = 0x01,
|
||||||
|
FRAME_REQ_DATA = 0x02,
|
||||||
FRAME_RESP_ACK = 0x10,
|
FRAME_RESP_ACK = 0x10,
|
||||||
FRAME_RESP_DATA = 0x11,
|
FRAME_RESP_DATA = 0x11,
|
||||||
FRAME_RESP_STREAM_START = 0x12,
|
FRAME_RESP_STREAM_START = 0x12,
|
||||||
FRAME_RESP_STREAM_CHUNK = 0x13,
|
// FRAME_RESP_STREAM_CHUNK = 0x13,
|
||||||
FRAME_RESP_STREAM_END = 0x14,
|
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;
|
} protocol_frame_type_t;
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
@@ -72,6 +70,13 @@ typedef enum {
|
|||||||
P_ERR_INTERNAL = 0x32,
|
P_ERR_INTERNAL = 0x32,
|
||||||
} protocol_error_t;
|
} 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);
|
void protocol_thread_entry(void *p1, void *p2, void *p3);
|
||||||
|
|
||||||
#endif // PROTOCOL_H
|
#endif // PROTOCOL_H
|
||||||
1195
firmware/src/protocol_old.c
Normal file
1195
firmware/src/protocol_old.c
Normal file
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,8 @@
|
|||||||
|
|
||||||
#include <io.h>
|
#include <io.h>
|
||||||
|
|
||||||
|
#define RX_RING_BUF_SIZE 1024
|
||||||
|
|
||||||
LOG_MODULE_REGISTER(usb, LOG_LEVEL_INF);
|
LOG_MODULE_REGISTER(usb, LOG_LEVEL_INF);
|
||||||
|
|
||||||
K_SEM_DEFINE(usb_rx_sem, 0, 1);
|
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)
|
#define UART_NODE DT_ALIAS(usb_uart)
|
||||||
const struct device *cdc_dev = DEVICE_DT_GET(UART_NODE);
|
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);
|
RING_BUF_DECLARE(rx_ringbuf, RX_RING_BUF_SIZE);
|
||||||
|
|
||||||
static void cdc_acm_irq_cb(const struct device *dev, void *user_data)
|
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). */
|
und der USB-Stack den Host drosselt (NAK). */
|
||||||
uart_irq_rx_disable(dev);
|
uart_irq_rx_disable(dev);
|
||||||
} else {
|
} else {
|
||||||
/* Nur so viele Daten lesen, wie Platz im Ringpuffer ist */
|
|
||||||
int to_read = MIN(sizeof(buffer), space);
|
int to_read = MIN(sizeof(buffer), space);
|
||||||
int len = uart_fifo_read(dev, buffer, to_read);
|
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);
|
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);
|
int ret = ring_buf_get(&rx_ringbuf, c, 1);
|
||||||
if (ret > 0 && device_is_ready(cdc_dev)) {
|
return (ret > 0);
|
||||||
/* Platz geschaffen -> Empfang wieder aktivieren */
|
|
||||||
uart_irq_rx_enable(cdc_dev);
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int usb_read_buffer(uint8_t *buf, size_t max_len)
|
int usb_read_buffer(uint8_t *buf, size_t max_len)
|
||||||
{
|
{
|
||||||
int ret = ring_buf_get(&rx_ringbuf, buf, 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;
|
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)) {
|
if (!device_is_ready(cdc_dev)) {
|
||||||
return;
|
return;
|
||||||
@@ -103,11 +95,11 @@ void usb_write_char(uint8_t c)
|
|||||||
uart_poll_out(cdc_dev, 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))
|
if (!device_is_ready(cdc_dev))
|
||||||
{
|
{
|
||||||
return;
|
return -ENODEV;
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t written;
|
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)
|
if (k_sem_take(&usb_tx_sem, K_MSEC(100)) != 0)
|
||||||
{
|
{
|
||||||
LOG_WRN("USB TX timeout - consumer not reading?");
|
LOG_WRN("USB TX timeout - consumer not reading?");
|
||||||
return;
|
return -ETIMEDOUT;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void usb_flush_rx(void)
|
void usb_flush_rx(void)
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ bool usb_wait_for_data(k_timeout_t timeout);
|
|||||||
/**
|
/**
|
||||||
* @brief Reads a single character from the USB RX FIFO
|
* @brief Reads a single character from the USB RX FIFO
|
||||||
* @param c Pointer to store the read character
|
* @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
|
* @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
|
* @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
|
* @brief Writes a block of data to the USB TX FIFO
|
||||||
* @param buf Buffer containing the data to write
|
* @param buf Buffer containing the data to write
|
||||||
* @param len Number of bytes 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
|
* @brief Flushes the USB RX FIFO
|
||||||
|
|||||||
170
tool/buzz.py
Normal file
170
tool/buzz.py
Normal file
@@ -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()
|
||||||
4
tool/config.yaml
Normal file
4
tool/config.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
serial:
|
||||||
|
port: "/dev/cu.usbmodem83401"
|
||||||
|
baudrate: 115200
|
||||||
|
timeout: 1.0
|
||||||
53
tool/core/cmd/flash_info.py
Normal file
53
tool/core/cmd/flash_info.py
Normal file
@@ -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('<I', payload[0:4])[0]
|
||||||
|
ext_total_blocks = struct.unpack('<I', payload[4:8])[0]
|
||||||
|
ext_free_blocks = struct.unpack('<I', payload[8:12])[0]
|
||||||
|
int_slot_size = struct.unpack('<I', payload[12:16])[0]
|
||||||
|
ext_page_size = struct.unpack('<H', payload[16:18])[0]
|
||||||
|
int_page_size = struct.unpack('<H', payload[18:20])[0]
|
||||||
|
max_path_len = payload[20]
|
||||||
|
|
||||||
|
result = {
|
||||||
|
'ext_block_size': ext_block_size,
|
||||||
|
'ext_total_blocks': ext_total_blocks,
|
||||||
|
'ext_free_blocks': ext_free_blocks,
|
||||||
|
'int_slot_size': int_slot_size,
|
||||||
|
'ext_page_size': ext_page_size,
|
||||||
|
'int_page_size': int_page_size,
|
||||||
|
'max_path_len': max_path_len,
|
||||||
|
'ext_total_size': ext_block_size * ext_total_blocks,
|
||||||
|
'ext_free_size': ext_block_size * ext_free_blocks,
|
||||||
|
'ext_used_size': ext_block_size * (ext_total_blocks - ext_free_blocks)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
def print(self, result):
|
||||||
|
if not result:
|
||||||
|
return
|
||||||
|
|
||||||
|
console.print(f"[info]Flash-Informationen:[/info]")
|
||||||
|
console.print(f" • [info]Externer Flash:[/info] {result['ext_total_size']/1024/1024:.2f} MB ({result['ext_total_blocks']} Blöcke à {result['ext_block_size']} Bytes)")
|
||||||
|
console.print(f" - Belegt: {result['ext_used_size']/1024/1024:.2f} MB ({result['ext_total_blocks'] - result['ext_free_blocks']} Blöcke)")
|
||||||
|
console.print(f" - Frei: {result['ext_free_size']/1024/1024:.2f} MB ({result['ext_free_blocks']} Blöcke)")
|
||||||
|
console.print(f" • [info]FW Flash Slot:[/info] {result['int_slot_size']/1024:.2f} KB")
|
||||||
|
console.print(f" • [info]EXTFLASH Seitengröße:[/info] {result['ext_page_size']} Bytes")
|
||||||
|
console.print(f" • [info]INTFLASH Seitengröße:[/info] {result['int_page_size']} Bytes")
|
||||||
|
console.print(f" • [info]Maximale Pfadlänge:[/info] {result['max_path_len']} Zeichen")
|
||||||
53
tool/core/cmd/fw_status.py
Normal file
53
tool/core/cmd/fw_status.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# tool/core/cmd/fw_status.py
|
||||||
|
import struct
|
||||||
|
from core.utils import console, console_err
|
||||||
|
from core.protocol import COMMANDS, ERRORS
|
||||||
|
|
||||||
|
class fw_status:
|
||||||
|
def __init__(self, bus):
|
||||||
|
self.bus = bus
|
||||||
|
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
import struct
|
||||||
|
self.bus.send_request(COMMANDS['get_firmware_status'])
|
||||||
|
|
||||||
|
data = self.bus.receive_response(length=10)
|
||||||
|
if not data or data.get('type') == 'error':
|
||||||
|
return None
|
||||||
|
|
||||||
|
header = data['data']
|
||||||
|
status = header[0]
|
||||||
|
app_version_raw = struct.unpack('<I', header[1:5])[0]
|
||||||
|
ker_version_raw = struct.unpack('<I', header[5:9])[0]
|
||||||
|
str_len = header[9]
|
||||||
|
|
||||||
|
fw_string_bytes = self.bus.connection.read(str_len)
|
||||||
|
fw_string = fw_string_bytes.decode('utf-8')
|
||||||
|
|
||||||
|
result = {
|
||||||
|
'status': status,
|
||||||
|
'fw_version_raw': hex(app_version_raw),
|
||||||
|
'kernel_version_raw': hex(ker_version_raw),
|
||||||
|
'fw_major': (app_version_raw >> 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']})")
|
||||||
76
tool/core/cmd/get_file.py
Normal file
76
tool/core/cmd/get_file.py
Normal file
@@ -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('<I', crc_resp['data'])[0]
|
||||||
|
except Exception:
|
||||||
|
device_file_crc = None
|
||||||
|
|
||||||
|
self.bus.send_request(COMMANDS['get_file'], payload)
|
||||||
|
|
||||||
|
stream_res = self.bus.receive_stream()
|
||||||
|
|
||||||
|
if not stream_res or stream_res.get('type') == 'error':
|
||||||
|
return None
|
||||||
|
|
||||||
|
file_data = stream_res['data']
|
||||||
|
remote_crc = stream_res['crc32']
|
||||||
|
|
||||||
|
if local_crc == remote_crc:
|
||||||
|
with open(p, 'wb') as f:
|
||||||
|
f.write(file_data)
|
||||||
|
success = True
|
||||||
|
else:
|
||||||
|
with open(p, 'wb') as f:
|
||||||
|
f.write(file_data)
|
||||||
|
success = False
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': success,
|
||||||
|
'source_path': source_path,
|
||||||
|
'dest_path': dest_path,
|
||||||
|
'crc32_remote': remote_crc,
|
||||||
|
'crc32_local': local_crc,
|
||||||
|
'crc32_device_file': device_file_crc,
|
||||||
|
'size': len(file_data)
|
||||||
|
}
|
||||||
|
|
||||||
|
def print(self, result):
|
||||||
|
if not result:
|
||||||
|
return
|
||||||
|
|
||||||
|
if result['success']:
|
||||||
|
console.print(f"✓ Datei [info]{result['source_path']}[/info] erfolgreich heruntergeladen.")
|
||||||
|
console.print(f" • Größe: [info]{result['size'] / 1024:.2f} KB[/info]")
|
||||||
|
else:
|
||||||
|
console_err.print(f"❌ CRC-FEHLER: Datei [error]{result['source_path']}[/error] wurde nicht korrekt empfangen!")
|
||||||
|
|
||||||
|
console.print(f" • Remote CRC: [info]{result['crc32_remote']:08X}[/info]")
|
||||||
|
console.print(f" • Local CRC: [info]{result['crc32_local']:08X}[/info]")
|
||||||
|
if result.get('crc32_device_file') is not None:
|
||||||
|
console.print(f" • Device CRC: [info]{result['crc32_device_file']:08X}[/info]")
|
||||||
|
console.print(f" • Zielpfad: [info]{result['dest_path']}[/info]")
|
||||||
71
tool/core/cmd/list_dir.py
Normal file
71
tool/core/cmd/list_dir.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# tool/core/cmd/list_dir.py
|
||||||
|
import struct
|
||||||
|
from core.utils import console
|
||||||
|
from core.protocol import COMMANDS
|
||||||
|
|
||||||
|
class list_dir:
|
||||||
|
def __init__(self, bus):
|
||||||
|
self.bus = bus
|
||||||
|
|
||||||
|
def get(self, path: str, recursive: bool = False):
|
||||||
|
# Wir stellen sicher, dass der Pfad nicht leer ist und normalisieren ihn leicht
|
||||||
|
clean_path = path if path == "/" else path.rstrip('/')
|
||||||
|
path_bytes = clean_path.encode('utf-8')
|
||||||
|
|
||||||
|
payload = struct.pack('B', len(path_bytes)) + path_bytes
|
||||||
|
self.bus.send_request(COMMANDS['list_dir'], payload)
|
||||||
|
|
||||||
|
chunks = self.bus.receive_list()
|
||||||
|
if chunks is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
entries = []
|
||||||
|
for chunk in chunks:
|
||||||
|
if len(chunk) < 6: # Typ(1) + Size(4) + min 1 char Name
|
||||||
|
continue
|
||||||
|
|
||||||
|
is_dir = chunk[0] == 1
|
||||||
|
size = struct.unpack('<I', chunk[1:5])[0] if not is_dir else None
|
||||||
|
name = chunk[5:].decode('utf-8').rstrip('\x00')
|
||||||
|
|
||||||
|
entry = {
|
||||||
|
'name': name,
|
||||||
|
'is_dir': is_dir,
|
||||||
|
'size': size
|
||||||
|
}
|
||||||
|
|
||||||
|
if recursive and is_dir:
|
||||||
|
# Rekursiver Aufruf: Pfad sauber zusammenfügen
|
||||||
|
sub_path = f"{clean_path}/{name}"
|
||||||
|
entry['children'] = self.get(sub_path, recursive=True)
|
||||||
|
|
||||||
|
entries.append(entry)
|
||||||
|
|
||||||
|
return entries
|
||||||
|
|
||||||
|
def print(self, entries, path: str, prefix: str = ""):
|
||||||
|
if prefix == "":
|
||||||
|
console.print(f"Inhalt von [info]{path}[/info]:")
|
||||||
|
|
||||||
|
if not entries:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Sortierung: Verzeichnisse zuerst
|
||||||
|
entries.sort(key=lambda x: (not x['is_dir'], x['name'].lower()))
|
||||||
|
|
||||||
|
for i, entry in enumerate(entries):
|
||||||
|
# Prüfen, ob es das letzte Element auf dieser Ebene ist
|
||||||
|
is_last = (i == len(entries) - 1)
|
||||||
|
connector = "└" if is_last else "├"
|
||||||
|
|
||||||
|
icon = "📁" if entry['is_dir'] else "📄"
|
||||||
|
size_str = f" ({entry['size']/1024:.2f} KB)" if entry['size'] is not None else ""
|
||||||
|
|
||||||
|
# Ausgabe der aktuellen Zeile
|
||||||
|
console.print(f"{prefix}{connector}{icon} [info]{entry['name']}[/info]{size_str}")
|
||||||
|
|
||||||
|
# Wenn Kinder vorhanden sind, rekursiv weiter
|
||||||
|
if 'children' in entry and entry['children']:
|
||||||
|
# Für die Kinder-Ebene das Prefix anpassen
|
||||||
|
extension = " " if is_last else "│ "
|
||||||
|
self.print(entry['children'], "", prefix=prefix + extension)
|
||||||
28
tool/core/cmd/proto.py
Normal file
28
tool/core/cmd/proto.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# tool/core/cmd/proto.py
|
||||||
|
import struct
|
||||||
|
from core.utils import console, console_err
|
||||||
|
from core.protocol import COMMANDS, ERRORS
|
||||||
|
|
||||||
|
class proto:
|
||||||
|
def __init__(self, bus):
|
||||||
|
self.bus = bus
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
self.bus.send_request(COMMANDS['get_protocol_version'], None)
|
||||||
|
|
||||||
|
data = self.bus.receive_response(length=1)
|
||||||
|
if not data or data.get('type') == 'error':
|
||||||
|
return None
|
||||||
|
|
||||||
|
payload = data['data']
|
||||||
|
result = {
|
||||||
|
'protocol_version': payload[0]
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
def print(self, result):
|
||||||
|
if not result:
|
||||||
|
return
|
||||||
|
|
||||||
|
protocol_version = result['protocol_version']
|
||||||
|
console.print(f"[title]Protokoll Version[/info] des Controllers ist [info]{protocol_version}[/info]:")
|
||||||
37
tool/core/cmd/rename.py
Normal file
37
tool/core/cmd/rename.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# tool/core/cmd/rename.py
|
||||||
|
import struct
|
||||||
|
from core.utils import console, console_err
|
||||||
|
from core.protocol import COMMANDS, ERRORS
|
||||||
|
|
||||||
|
class rename:
|
||||||
|
def __init__(self, bus):
|
||||||
|
self.bus = bus
|
||||||
|
|
||||||
|
def get(self, source_path: str, dest_path: str):
|
||||||
|
source_path_bytes = source_path.encode('utf-8')
|
||||||
|
dest_path_bytes = dest_path.encode('utf-8')
|
||||||
|
|
||||||
|
payload = struct.pack('B', len(source_path_bytes)) + source_path_bytes
|
||||||
|
payload += struct.pack('B', len(dest_path_bytes)) + dest_path_bytes
|
||||||
|
|
||||||
|
self.bus.send_request(COMMANDS['rename'], payload)
|
||||||
|
|
||||||
|
data = self.bus.receive_ack()
|
||||||
|
|
||||||
|
if not data or data.get('type') == 'error':
|
||||||
|
return None
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': data.get('type') == 'ack',
|
||||||
|
'source_path': source_path,
|
||||||
|
'dest_path': dest_path
|
||||||
|
}
|
||||||
|
|
||||||
|
def print(self, result):
|
||||||
|
if not result or not result.get('success'):
|
||||||
|
return
|
||||||
|
|
||||||
|
console.print(
|
||||||
|
f"Pfad [info]{result['source_path']}[/info] wurde erfolgreich in "
|
||||||
|
f"[info]{result['dest_path']}[/info] umbenannt."
|
||||||
|
)
|
||||||
32
tool/core/cmd/rm.py
Normal file
32
tool/core/cmd/rm.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# tool/core/cmd/rm.py
|
||||||
|
import struct
|
||||||
|
from core.utils import console, console_err
|
||||||
|
from core.protocol import COMMANDS, ERRORS
|
||||||
|
|
||||||
|
class rm:
|
||||||
|
def __init__(self, bus):
|
||||||
|
self.bus = bus
|
||||||
|
|
||||||
|
def get(self, path: str):
|
||||||
|
path_bytes = path.encode('utf-8')
|
||||||
|
payload = struct.pack('B', len(path_bytes)) + path_bytes
|
||||||
|
self.bus.send_request(COMMANDS['rm'], payload)
|
||||||
|
|
||||||
|
# 1 Byte Type + 4 Byte Size = 5
|
||||||
|
data = self.bus.receive_ack()
|
||||||
|
|
||||||
|
if not data or data.get('type') == 'error':
|
||||||
|
return None
|
||||||
|
|
||||||
|
if data.get('type') == 'ack':
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def print(self, result, path: str):
|
||||||
|
if result is None:
|
||||||
|
console_err.print(f"Fehler: Pfad [error]{path}[/error] konnte nicht entfernt werden.")
|
||||||
|
elif result is False:
|
||||||
|
console_err.print(f"Fehler: Pfad [error]{path}[/error] existiert nicht oder konnte nicht entfernt werden.")
|
||||||
|
else:
|
||||||
|
console.print(f"Pfad [info]{path}[/info] wurde erfolgreich entfernt.")
|
||||||
36
tool/core/cmd/stat.py
Normal file
36
tool/core/cmd/stat.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# tool/core/cmd/stat.py
|
||||||
|
import struct
|
||||||
|
from core.utils import console, console_err
|
||||||
|
from core.protocol import COMMANDS, ERRORS
|
||||||
|
|
||||||
|
class stat:
|
||||||
|
def __init__(self, bus):
|
||||||
|
self.bus = bus
|
||||||
|
|
||||||
|
def get(self, path: str):
|
||||||
|
path_bytes = path.encode('utf-8')
|
||||||
|
payload = struct.pack('B', len(path_bytes)) + path_bytes
|
||||||
|
self.bus.send_request(COMMANDS['stat'], payload)
|
||||||
|
|
||||||
|
# 1 Byte Type + 4 Byte Size = 5
|
||||||
|
data = self.bus.receive_response(length=5)
|
||||||
|
|
||||||
|
if not data or data.get('type') == 'error':
|
||||||
|
return None
|
||||||
|
|
||||||
|
payload = data['data']
|
||||||
|
result = {
|
||||||
|
'is_directory': payload[0] == 1,
|
||||||
|
'size': struct.unpack('<I', payload[1:5])[0]
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
def print(self, result, path: str):
|
||||||
|
if not result:
|
||||||
|
return
|
||||||
|
|
||||||
|
t_name = "📁 Verzeichnis" if result['is_directory'] else "📄 Datei"
|
||||||
|
console.print(f"[info_title]Stat[/info_title] für [info]{path}[/info]:")
|
||||||
|
console.print(f" • Typ: [info]{t_name}[/info]")
|
||||||
|
if not result['is_directory']:
|
||||||
|
console.print(f" • Grösse: [info]{result['size']/1024:.2f} KB[/info] ({result['size']} Bytes)")
|
||||||
48
tool/core/config.py
Normal file
48
tool/core/config.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# tool/core/config.py
|
||||||
|
import os
|
||||||
|
import yaml
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
def __init__(self):
|
||||||
|
self._config = None
|
||||||
|
self.config_name = "config.yaml"
|
||||||
|
self.custom_path = None # Speicher für den Parameter-Pfad
|
||||||
|
self.debug = False
|
||||||
|
|
||||||
|
def _load(self):
|
||||||
|
if self._config is not None:
|
||||||
|
return
|
||||||
|
|
||||||
|
from core.utils import console
|
||||||
|
|
||||||
|
search_paths = []
|
||||||
|
if self.custom_path:
|
||||||
|
search_paths.append(Path(self.custom_path))
|
||||||
|
|
||||||
|
search_paths.append(Path(os.getcwd()) / self.config_name)
|
||||||
|
search_paths.append(Path(__file__).parent.parent / self.config_name)
|
||||||
|
|
||||||
|
config_path = None
|
||||||
|
for p in search_paths:
|
||||||
|
if p.exists():
|
||||||
|
config_path = p
|
||||||
|
break
|
||||||
|
|
||||||
|
if not config_path:
|
||||||
|
raise FileNotFoundError(f"Konfiguration konnte an keinem Ort gefunden werden.")
|
||||||
|
|
||||||
|
if not config_path.exists():
|
||||||
|
raise FileNotFoundError(f"Konfiguration nicht gefunden: {self.config_name}")
|
||||||
|
else:
|
||||||
|
if self.debug: console.print(f"[bold green]✓[/bold green] Konfiguration geladen: [info]{config_path}[/info]")
|
||||||
|
|
||||||
|
with open(config_path, 'r') as f:
|
||||||
|
self._config = yaml.safe_load(f)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def serial_settings(self):
|
||||||
|
self._load()
|
||||||
|
return self._config.get('serial', {})
|
||||||
|
|
||||||
|
cfg = Config()
|
||||||
88
tool/core/protocol.py
Normal file
88
tool/core/protocol.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# tool/core/protocol.py
|
||||||
|
VERSION = {
|
||||||
|
"min_protocol_version": 1,
|
||||||
|
"max_protocol_version": 1,
|
||||||
|
"current_protocol_version": None
|
||||||
|
}
|
||||||
|
|
||||||
|
SYNC_SEQ = b'BUZZ'
|
||||||
|
|
||||||
|
ERRORS = {
|
||||||
|
0x00: "NONE",
|
||||||
|
0x01: "INVALID_COMMAND",
|
||||||
|
0x02: "INVALID_PARAMETERS",
|
||||||
|
0x03: "MISSING_PARAMETERS",
|
||||||
|
|
||||||
|
0x10: "FILE_NOT_FOUND",
|
||||||
|
0x11: "ALREADY_EXISTS",
|
||||||
|
0x12: "NOT_A_DIRECTORY",
|
||||||
|
0x13: "IS_A_DIRECTORY",
|
||||||
|
0x14: "ACCESS_DENIED",
|
||||||
|
0x15: "NO_SPACE",
|
||||||
|
0x16: "FILE_TOO_LARGE",
|
||||||
|
|
||||||
|
0x20: "IO_ERROR",
|
||||||
|
0x21: "TIMEOUT",
|
||||||
|
0x22: "CRC_MISMATCH",
|
||||||
|
0x23: "TRANSFER_ABORTED",
|
||||||
|
|
||||||
|
0x30: "NOT_SUPPORTED",
|
||||||
|
0x31: "BUSY",
|
||||||
|
0x32: "INTERNAL_ERROR",
|
||||||
|
|
||||||
|
0x40: "NOT_IMPLEMENTED",
|
||||||
|
}
|
||||||
|
|
||||||
|
FRAME_TYPE_REQUEST = 0x01
|
||||||
|
FRAME_TYPE_ACK = 0x10
|
||||||
|
FRAME_TYPE_RESPONSE = 0x11
|
||||||
|
FRAME_TYPE_STREAM_START = 0x12
|
||||||
|
FRAME_TYPE_STREAM_CHUNK = 0x13
|
||||||
|
FRAME_TYPE_STREAM_END = 0x14
|
||||||
|
FRAME_TYPE_LIST_START = 0x15
|
||||||
|
FRAME_TYPE_LIST_CHUNK = 0x16
|
||||||
|
FRAME_TYPE_LIST_END = 0x17
|
||||||
|
FRAME_TYPE_ERROR = 0xFF
|
||||||
|
|
||||||
|
FRAME_TYPES = {
|
||||||
|
'request': FRAME_TYPE_REQUEST,
|
||||||
|
'ack': FRAME_TYPE_ACK,
|
||||||
|
'response': FRAME_TYPE_RESPONSE,
|
||||||
|
'error': FRAME_TYPE_ERROR,
|
||||||
|
'stream_start': FRAME_TYPE_STREAM_START,
|
||||||
|
'stream_chunk': FRAME_TYPE_STREAM_CHUNK,
|
||||||
|
'stream_end': FRAME_TYPE_STREAM_END,
|
||||||
|
'list_start': FRAME_TYPE_LIST_START,
|
||||||
|
'list_chunk': FRAME_TYPE_LIST_CHUNK,
|
||||||
|
'list_end': FRAME_TYPE_LIST_END
|
||||||
|
}
|
||||||
|
|
||||||
|
CMD_GET_PROTOCOL_VERSION = 0x00
|
||||||
|
CMD_GET_FIRMWARE_STATUS = 0x01
|
||||||
|
CMD_GET_FLASH_INFO = 0x02
|
||||||
|
|
||||||
|
CMD_LIST_DIR = 0x10
|
||||||
|
CMD_CRC_32 = 0x11
|
||||||
|
CMD_MKDIR = 0x12
|
||||||
|
CMD_RM = 0x13
|
||||||
|
CMD_STAT = 0x18
|
||||||
|
CMD_RENAME = 0x19
|
||||||
|
|
||||||
|
CMD_PUT_FILE = 0x20
|
||||||
|
CMD_PUT_FW = 0x21
|
||||||
|
CMD_GET_FILE = 0x22
|
||||||
|
|
||||||
|
COMMANDS = {
|
||||||
|
'get_protocol_version': CMD_GET_PROTOCOL_VERSION,
|
||||||
|
'get_firmware_status': CMD_GET_FIRMWARE_STATUS,
|
||||||
|
'get_flash_info': CMD_GET_FLASH_INFO,
|
||||||
|
'list_dir': CMD_LIST_DIR,
|
||||||
|
'stat': CMD_STAT,
|
||||||
|
'rm': CMD_RM,
|
||||||
|
'rename': CMD_RENAME,
|
||||||
|
'mkdir': CMD_MKDIR,
|
||||||
|
'crc_32': CMD_CRC_32,
|
||||||
|
'put_file': CMD_PUT_FILE,
|
||||||
|
'get_file': CMD_GET_FILE,
|
||||||
|
'put_fw': CMD_PUT_FW,
|
||||||
|
}
|
||||||
279
tool/core/serial_conn.py
Normal file
279
tool/core/serial_conn.py
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
# tool/core/serial_conn.py
|
||||||
|
import struct
|
||||||
|
import serial
|
||||||
|
import time
|
||||||
|
from core.utils import console, console_err
|
||||||
|
from core.protocol import SYNC_SEQ, ERRORS, FRAME_TYPES, VERSION
|
||||||
|
|
||||||
|
class SerialBus:
|
||||||
|
def __init__(self, settings: dict):
|
||||||
|
"""
|
||||||
|
Initialisiert den Bus mit den (ggf. übersteuerten) Settings.
|
||||||
|
"""
|
||||||
|
self.port = settings.get('port')
|
||||||
|
self.baudrate = settings.get('baudrate', 115200)
|
||||||
|
self.timeout = settings.get('timeout', 1.0)
|
||||||
|
self.debug = settings.get('debug', False)
|
||||||
|
self.connection = None
|
||||||
|
|
||||||
|
def open(self):
|
||||||
|
"""Öffnet die serielle Schnittstelle."""
|
||||||
|
try:
|
||||||
|
self.connection = serial.Serial(
|
||||||
|
port=self.port,
|
||||||
|
baudrate=self.baudrate,
|
||||||
|
timeout=self.timeout
|
||||||
|
)
|
||||||
|
self.flush_input()
|
||||||
|
if self.debug: console.print(f"[bold green]✓[/bold green] Port [info]{self.port}[/info] erfolgreich geöffnet.")
|
||||||
|
from core.cmd.proto import proto
|
||||||
|
cmd = proto(self)
|
||||||
|
data = cmd.get()
|
||||||
|
VERSION["current_protocol_version"] = data['protocol_version'] if data else None
|
||||||
|
if data:
|
||||||
|
if self.debug: console.print(f" • Protokoll Version: [info]{data['protocol_version']}[/info]")
|
||||||
|
if data['protocol_version'] < VERSION["min_protocol_version"] or data['protocol_version'] > 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('<H', self.connection.read(2))[0]
|
||||||
|
if self.debug: console.print(f"Erwarte List-Chunk mit Länge: {length} Bytes")
|
||||||
|
data = self.connection.read(length)
|
||||||
|
if self.debug: console.print(f"Rohdaten List-Chunk: [grey62]{data.hex(' ').upper()}[/grey62]") # Debug-Ausgabe des rohen Chunks
|
||||||
|
list_items.append(data)
|
||||||
|
elif ftype == FRAME_TYPES['list_end']:
|
||||||
|
if not is_list: raise ValueError("Ende ohne Start.")
|
||||||
|
num_entries = struct.unpack('<H', self.connection.read(2))[0]
|
||||||
|
if len(list_items) != num_entries:
|
||||||
|
console_err.print(f"[warning]Warnung: Erwartete {num_entries} Items, bekam {len(list_items)}[/warning]")
|
||||||
|
return list_items
|
||||||
|
elif ftype == FRAME_TYPES['error']:
|
||||||
|
err_code_raw = self.connection.read(1)
|
||||||
|
err_code = err_code_raw[0] if err_code_raw else 0xFF
|
||||||
|
err_name = ERRORS.get(err_code, "UNKNOWN")
|
||||||
|
raise ControllerError(err_code, err_name)
|
||||||
|
|
||||||
|
def receive_stream(self, chunk_size: int = 1024):
|
||||||
|
"""Liest einen Datenstrom in Chunks, bis ein Fehler oder Ende-Signal kommt."""
|
||||||
|
is_stream = False
|
||||||
|
data_chunks = []
|
||||||
|
|
||||||
|
while True:
|
||||||
|
if not self.wait_for_sync(SYNC_SEQ):
|
||||||
|
raise TimeoutError("Timeout beim Warten auf Sync im Stream-Modus.")
|
||||||
|
|
||||||
|
ftype = self._read_exact(1, "Frame-Typ")[0]
|
||||||
|
if self.debug: console.print(f"Empfangener Frame-Typ: 0x{ftype:02X} (start: 0x{FRAME_TYPES['stream_start']:02X}, chunk: 0x{FRAME_TYPES['stream_chunk']:02X}, end: 0x{FRAME_TYPES['stream_end']:02X})")
|
||||||
|
|
||||||
|
if ftype == FRAME_TYPES['stream_start']:
|
||||||
|
is_stream = True
|
||||||
|
data_chunks = []
|
||||||
|
size = struct.unpack('<I', self._read_exact(4, "Stream-Größe"))[0]
|
||||||
|
if self.debug: console.print(f"Stream gestartet, erwartete Gesamtgröße: {size} Bytes")
|
||||||
|
|
||||||
|
received_size = 0
|
||||||
|
while received_size < size:
|
||||||
|
chunk_length = min(chunk_size, size - received_size)
|
||||||
|
try:
|
||||||
|
chunk_data = self._read_exact(chunk_length, f"Daten-Chunk @ {received_size}/{size}")
|
||||||
|
except Exception as e:
|
||||||
|
raise IOError(f"Stream-Abbruch bei {received_size}/{size} Bytes: {e}") from e
|
||||||
|
data_chunks.append(chunk_data)
|
||||||
|
received_size += len(chunk_data)
|
||||||
|
if self.debug: console.print(f"Empfangen: {received_size}/{size} Bytes ({(received_size/size)*100:.2f}%)")
|
||||||
|
if self.debug: console.print("Stream vollständig empfangen.")
|
||||||
|
|
||||||
|
elif ftype == FRAME_TYPES['stream_end']:
|
||||||
|
if not is_stream: raise ValueError("Ende ohne Start.")
|
||||||
|
crc32 = struct.unpack('<I', self._read_exact(4, "CRC32"))[0]
|
||||||
|
if self.debug: console.print(f"Stream-Ende empfangen, CRC32: 0x{crc32:08X}")
|
||||||
|
|
||||||
|
return {'data': b''.join(data_chunks), 'crc32': crc32}
|
||||||
|
|
||||||
|
# elif ftype == FRAME_TYPES['list_chunk']:
|
||||||
|
# if not is_list: raise ValueError("Chunk ohne Start.")
|
||||||
|
# length = struct.unpack('<H', self.connection.read(2))[0]
|
||||||
|
# if self.debug: console.print(f"Erwarte List-Chunk mit Länge: {length} Bytes")
|
||||||
|
# data = self.connection.read(length)
|
||||||
|
# if self.debug: console.print(f"Rohdaten List-Chunk: [grey62]{data.hex(' ').upper()}[/grey62]") # Debug-Ausgabe des rohen Chunks
|
||||||
|
# list_items.append(data)
|
||||||
|
# elif ftype == FRAME_TYPES['list_end']:
|
||||||
|
# if not is_list: raise ValueError("Ende ohne Start.")
|
||||||
|
# num_entries = struct.unpack('<H', self.connection.read(2))[0]
|
||||||
|
# if len(list_items) != num_entries:
|
||||||
|
# console_err.print(f"[warning]Warnung: Erwartete {num_entries} Items, bekam {len(list_items)}[/warning]")
|
||||||
|
# return list_items
|
||||||
|
elif ftype == FRAME_TYPES['error']:
|
||||||
|
err_code_raw = self.connection.read(1)
|
||||||
|
err_code = err_code_raw[0] if err_code_raw else 0xFF
|
||||||
|
err_name = ERRORS.get(err_code, "UNKNOWN")
|
||||||
|
raise ControllerError(err_code, err_name)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unerwarteter Frame-Typ: 0x{ftype:02X}")
|
||||||
|
|
||||||
|
class ControllerError(Exception):
|
||||||
|
"""Wird ausgelöst, wenn der Controller einen Error-Frame (0xFF) sendet."""
|
||||||
|
def __init__(self, code, name):
|
||||||
|
self.code = code
|
||||||
|
self.name = name
|
||||||
|
super().__init__(f"Controller Error 0x{code:02X} ({name})")
|
||||||
14
tool/core/utils.py
Normal file
14
tool/core/utils.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
from rich.console import Console
|
||||||
|
from rich.theme import Theme
|
||||||
|
|
||||||
|
custom_theme = Theme({
|
||||||
|
"info": "bold blue",
|
||||||
|
"warning": "yellow",
|
||||||
|
"error": "bold red",
|
||||||
|
"error_msg": "red",
|
||||||
|
"sync": "bold magenta",
|
||||||
|
"wait": "italic grey50"
|
||||||
|
})
|
||||||
|
|
||||||
|
console = Console(theme=custom_theme, highlight=False)
|
||||||
|
console_err = Console(theme=custom_theme, stderr=True, highlight=False)
|
||||||
3
tool/requirements.txt
Normal file
3
tool/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
PyYAML
|
||||||
|
pyserial
|
||||||
|
rich
|
||||||
Reference in New Issue
Block a user