Compare commits
1 Commits
main
...
b665cb5def
| Author | SHA1 | Date | |
|---|---|---|---|
| b665cb5def |
@@ -1,9 +1,6 @@
|
|||||||
# buzzer.py
|
# buzzer.py
|
||||||
import argparse
|
import argparse
|
||||||
import sys
|
import sys
|
||||||
from core.config import load_config
|
|
||||||
from core.connection import BuzzerConnection, BuzzerError
|
|
||||||
from core.commands import info, ls, put, mkdir, rm, confirm, reboot, play, check, get_tag
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description="Edis Buzzer Host Tool")
|
parser = argparse.ArgumentParser(description="Edis Buzzer Host Tool")
|
||||||
@@ -12,6 +9,7 @@ def main():
|
|||||||
parser.add_argument("-p", "--port", type=str, help="Serielle Schnittstelle (z.B. COM15)")
|
parser.add_argument("-p", "--port", type=str, help="Serielle Schnittstelle (z.B. COM15)")
|
||||||
parser.add_argument("-b", "--baudrate", type=int, help="Verbindungsgeschwindigkeit")
|
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("-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
|
# Subkommandos einrichten
|
||||||
subparsers = parser.add_subparsers(dest="command", help="Verfügbare Befehle")
|
subparsers = parser.add_subparsers(dest="command", help="Verfügbare Befehle")
|
||||||
@@ -26,8 +24,19 @@ def main():
|
|||||||
|
|
||||||
# Befehl: put
|
# Befehl: put
|
||||||
put_parser = subparsers.add_parser("put", help="Lädt eine oder mehrere Dateien auf den Controller hoch")
|
put_parser = subparsers.add_parser("put", help="Lädt eine oder mehrere Dateien auf den Controller hoch")
|
||||||
put_parser.add_argument("sources", nargs="+", help="Lokale Quelldatei(en) oder Wildcards (z.B. *.raw)")
|
put_parser.add_argument("sources", nargs="+", help="Lokale Quelldatei(en), Verzeichnisse oder Wildcards (z.B. *.raw)")
|
||||||
put_parser.add_argument("target", type=str, help="Zielpfad auf dem Controller (Verzeichnis muss mit '/' enden)")
|
put_parser.add_argument("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
|
# Befehl: mkdir
|
||||||
mkdir_parser = subparsers.add_parser("mkdir", help="Erstellt ein neues Verzeichnis")
|
mkdir_parser = subparsers.add_parser("mkdir", help="Erstellt ein neues Verzeichnis")
|
||||||
@@ -38,6 +47,25 @@ def main():
|
|||||||
rm_parser.add_argument("path", type=str, help="Pfad der zu löschenden Datei/Ordner")
|
rm_parser.add_argument("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")
|
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
|
# Befehl: play
|
||||||
play_parser = subparsers.add_parser("play", help="Spielt eine Datei auf dem Controller ab")
|
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)")
|
play_parser.add_argument("path", type=str, help="Pfad der abzuspielenden Datei (z.B. /lfs/a/neu)")
|
||||||
@@ -52,39 +80,58 @@ def main():
|
|||||||
# Befehl: reboot
|
# Befehl: reboot
|
||||||
reboot_parser = subparsers.add_parser("reboot", help="Startet den Buzzer neu")
|
reboot_parser = subparsers.add_parser("reboot", help="Startet den Buzzer neu")
|
||||||
|
|
||||||
# Befehl: get_tag
|
# Befehl: get_tags (neuer Blob/TLV-Parser)
|
||||||
get_tag_parser = subparsers.add_parser("get_tag", help="Holt die Tags einer Datei")
|
get_tags_parser = subparsers.add_parser("get_tags", help="Holt alle Tags einer Datei als JSON")
|
||||||
get_tag_parser.add_argument("path", type=str, help="Pfad der Datei (z.B. /lfs/a/neu)")
|
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
|
# Argumente parsen
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
from core.config import load_config
|
||||||
config = load_config(args)
|
config = load_config(args)
|
||||||
|
|
||||||
print("--- Aktuelle Verbindungsparameter ---------------------")
|
print("--- Aktuelle Verbindungsparameter -------------------------------")
|
||||||
print(f"Port: {config.get('port', 'Nicht definiert')}")
|
print(f"Port: {config.get('port', 'Nicht definiert')}")
|
||||||
print(f"Baudrate: {config.get('baudrate')}")
|
print(f"Baudrate: {config.get('baudrate')}")
|
||||||
print(f"Timeout: {config.get('timeout')}s")
|
print(f"Timeout: {config.get('timeout')}s")
|
||||||
print("-" * 55)
|
print("-" * 65)
|
||||||
|
|
||||||
if not config.get("port"):
|
if not config.get("port"):
|
||||||
print("Abbruch: Es muss ein Port in der config.yaml oder via --port definiert werden.")
|
print("Abbruch: Es muss ein Port in der config.yaml oder via --port definiert werden.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
from core.connection import BuzzerConnection, BuzzerError
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with BuzzerConnection(config) as conn:
|
with BuzzerConnection(config) as conn:
|
||||||
# 1. Immer die Info holen und anzeigen
|
if not args.no_auto_info:
|
||||||
|
from core.commands import info
|
||||||
sys_info = info.execute(conn)
|
sys_info = info.execute(conn)
|
||||||
|
|
||||||
# Neu: Status auslesen und Farbe zuweisen (Grün für CONFIRMED, Gelb für UNCONFIRMED)
|
|
||||||
status = sys_info.get("image_status", "UNKNOWN")
|
status = sys_info.get("image_status", "UNKNOWN")
|
||||||
status_color = "\033[32m" if status == "CONFIRMED" else "\033[33m"
|
status_colors = {
|
||||||
|
"CONFIRMED": "\033[32m",
|
||||||
|
"TESTING": "\033[33m",
|
||||||
|
"PENDING": "\033[36m",
|
||||||
|
}
|
||||||
|
status_color = status_colors.get(status, "\033[37m")
|
||||||
|
|
||||||
# Neu: Die print-Anweisung enthält nun den formatierten Status
|
|
||||||
print(f"Buzzer Firmware: v{sys_info['app_version']} [{status_color}{status}\033[0m] (Protokoll v{sys_info['protocol_version']})")
|
print(f"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(f"LittleFS Status: {sys_info['used_kb']:.1f} KB / {sys_info['total_kb']:.1f} KB belegt ({sys_info['percent_used']:.1f}%)")
|
||||||
print("-" * 55)
|
print("-" * 65)
|
||||||
|
|
||||||
# 2. Spezifisches Kommando ausführen
|
# 2. Spezifisches Kommando ausführen
|
||||||
if args.command == "ls":
|
if args.command == "ls":
|
||||||
|
from core.commands import ls
|
||||||
print(f"Inhalt von '{args.path}':\n")
|
print(f"Inhalt von '{args.path}':\n")
|
||||||
tree = ls.get_file_tree(conn, target_path=args.path, recursive=args.recursive)
|
tree = ls.get_file_tree(conn, target_path=args.path, recursive=args.recursive)
|
||||||
if not tree:
|
if not tree:
|
||||||
@@ -92,31 +139,66 @@ def main():
|
|||||||
else:
|
else:
|
||||||
ls.print_tree(tree, path=args.path )
|
ls.print_tree(tree, path=args.path )
|
||||||
elif args.command == "put":
|
elif args.command == "put":
|
||||||
put.execute(conn, sources=args.sources, target=args.target)
|
from core.commands import put
|
||||||
|
put.execute(conn, sources=args.sources, target=args.target, recursive=args.recursive)
|
||||||
|
elif args.command == "put_many":
|
||||||
|
from core.commands import put
|
||||||
|
recursive = True if not args.recursive else args.recursive
|
||||||
|
put.execute(conn, sources=args.sources, target=args.target, recursive=recursive)
|
||||||
|
elif args.command == "fw_put":
|
||||||
|
from core.commands import fw_put
|
||||||
|
fw_put.execute(conn, source=args.source)
|
||||||
elif args.command == "mkdir":
|
elif args.command == "mkdir":
|
||||||
|
from core.commands import mkdir
|
||||||
mkdir.execute(conn, path=args.path)
|
mkdir.execute(conn, path=args.path)
|
||||||
elif args.command == "rm":
|
elif args.command == "rm":
|
||||||
|
from core.commands import rm
|
||||||
rm.execute(conn, path=args.path, recursive=args.recursive)
|
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":
|
elif args.command == "confirm":
|
||||||
|
from core.commands import confirm
|
||||||
confirm.execute(conn)
|
confirm.execute(conn)
|
||||||
elif args.command == "reboot":
|
elif args.command == "reboot":
|
||||||
|
from core.commands import reboot
|
||||||
reboot.execute(conn)
|
reboot.execute(conn)
|
||||||
elif args.command == "play":
|
elif args.command == "play":
|
||||||
|
from core.commands import play
|
||||||
play.execute(conn, path=args.path)
|
play.execute(conn, path=args.path)
|
||||||
elif args.command == "check":
|
elif args.command == "check":
|
||||||
|
from core.commands import check
|
||||||
CRC32 = check.execute(conn, path=args.path)
|
CRC32 = check.execute(conn, path=args.path)
|
||||||
if CRC32:
|
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}")
|
print(f"CRC32 von '{args.path}': 0x{CRC32['crc32']:08x}")
|
||||||
else:
|
else:
|
||||||
print(f"Fehler: Keine CRC32-Information für '{args.path}' erhalten.")
|
print(f"Fehler: Keine CRC32-Information für '{args.path}' erhalten.")
|
||||||
elif args.command == "get_tag":
|
elif args.command == "get_tags":
|
||||||
tags = get_tag.execute(conn, path=args.path)
|
from core.commands import tags
|
||||||
if tags:
|
tag_map = tags.get_tags(conn, args.path)
|
||||||
print(f"Tags von '{args.path}':")
|
print(tag_map)
|
||||||
for key, value in tags.items():
|
elif args.command == "write_tags":
|
||||||
print(f" {key}: {value}")
|
from core.commands import tags
|
||||||
else:
|
updates = tags.parse_tags_json_input(args.json)
|
||||||
print(f"Fehler: Keine Tags für '{args.path}' erhalten.")
|
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:
|
elif args.command == "info" or args.command is None:
|
||||||
# Wurde kein Befehl oder explizit 'info' angegeben, sind wir hier schon fertig
|
# Wurde kein Befehl oder explizit 'info' angegeben, sind wir hier schon fertig
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# config.yaml
|
# config.yaml
|
||||||
serial:
|
serial:
|
||||||
port: "COM17"
|
port: "/dev/cu.usbmodem83401"
|
||||||
baudrate: 250000
|
baudrate: 2500000
|
||||||
timeout: 10
|
timeout: 1
|
||||||
|
|||||||
@@ -3,3 +3,5 @@ serial:
|
|||||||
port: "COM17"
|
port: "COM17"
|
||||||
baudrate: 250000
|
baudrate: 250000
|
||||||
timeout: 20
|
timeout: 20
|
||||||
|
crc_timeout_min_seconds: 2.0
|
||||||
|
crc_timeout_ms_per_100kb: 1.5
|
||||||
@@ -1,18 +1,60 @@
|
|||||||
# core/commands/check.py
|
# core/commands/check.py
|
||||||
from core.connection import BuzzerError
|
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:
|
def execute(conn, path: str) -> dict:
|
||||||
"""Holt die CRC32 einer datei und gibt sie als Int zurück."""
|
"""Holt die CRC32 nur über Audiodaten und passt Timeout für große Dateien an."""
|
||||||
lines = conn.send_command("check " + path)
|
size_bytes = _lookup_file_size_bytes(conn, path)
|
||||||
if not lines:
|
timeout = _estimate_crc_timeout_seconds(conn, size_bytes)
|
||||||
raise BuzzerError("Keine Antwort auf 'check' empfangen.")
|
crc32 = conn.check_file_crc(path, timeout=timeout)
|
||||||
|
|
||||||
parts = lines[0].split()
|
|
||||||
if len(parts) != 1:
|
|
||||||
raise BuzzerError(f"Unerwartetes Check-Format: {lines[0]}")
|
|
||||||
|
|
||||||
crc32 = int(parts[0], 16)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"crc32": crc32
|
"crc32": crc32,
|
||||||
|
"size_bytes": size_bytes,
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
# core/commands/mkdir.py
|
# core/commands/confirm.py
|
||||||
from core.connection import BuzzerError
|
from core.connection import BuzzerError
|
||||||
|
|
||||||
|
|
||||||
def execute(conn):
|
def execute(conn):
|
||||||
"""Confirmt die aktuelle Firmware."""
|
"""Bestätigt die aktuell laufende Firmware per Binary-Protokoll."""
|
||||||
try:
|
try:
|
||||||
conn.send_command(f"confirm")
|
conn.confirm_firmware()
|
||||||
print(f"✅ Firmware erfolgreich bestätigt.")
|
print("✅ Firmware erfolgreich bestätigt.")
|
||||||
except BuzzerError as e:
|
except BuzzerError as e:
|
||||||
print(f"❌ Fehler beim Bestätigen der Firmware: {e}")
|
print(f"❌ Fehler beim Bestätigen der Firmware: {e}")
|
||||||
57
buzzer_tool/core/commands/fw_put.py
Normal file
57
buzzer_tool/core/commands/fw_put.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import os
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def _estimate_fw_timeout_seconds(conn, total_size: int) -> float:
|
||||||
|
base = float(getattr(conn, "timeout", 5.0))
|
||||||
|
erase_budget = 8.0
|
||||||
|
stream_and_write_budget = total_size / (25.0 * 1024.0)
|
||||||
|
return max(base, erase_budget + stream_and_write_budget)
|
||||||
|
|
||||||
|
|
||||||
|
def execute(conn, source: str):
|
||||||
|
if not os.path.isfile(source):
|
||||||
|
raise FileNotFoundError(f"Firmware-Datei nicht gefunden: {source}")
|
||||||
|
|
||||||
|
with open(source, "rb") as f:
|
||||||
|
data = f.read()
|
||||||
|
|
||||||
|
total_size = len(data)
|
||||||
|
if total_size == 0:
|
||||||
|
raise ValueError("Firmware-Datei ist leer.")
|
||||||
|
|
||||||
|
print(f"Sende 🧩 Firmware ({total_size / 1024:.1f} KB) -> secondary slot")
|
||||||
|
fw_timeout = _estimate_fw_timeout_seconds(conn, total_size)
|
||||||
|
print(f" Timeout fw_put: {fw_timeout:.1f}s")
|
||||||
|
print(" Phase 1/3: Lösche secondary slot und initialisiere Flash...")
|
||||||
|
|
||||||
|
start_time = time.monotonic()
|
||||||
|
last_ui_update = start_time
|
||||||
|
transfer_started = False
|
||||||
|
|
||||||
|
def progress_handler(chunk_len, sent_file, total_file):
|
||||||
|
nonlocal last_ui_update, transfer_started
|
||||||
|
if not transfer_started:
|
||||||
|
transfer_started = True
|
||||||
|
print(" Phase 2/3: Übertrage Firmware...")
|
||||||
|
now = time.monotonic()
|
||||||
|
if now - last_ui_update < 0.2 and sent_file < total_file:
|
||||||
|
return
|
||||||
|
last_ui_update = now
|
||||||
|
|
||||||
|
elapsed = now - start_time
|
||||||
|
speed = (sent_file / 1024.0) / elapsed if elapsed > 0 else 0.0
|
||||||
|
perc = (sent_file / total_file) * 100.0 if total_file > 0 else 100.0
|
||||||
|
eta_sec = ((total_file - sent_file) / (sent_file / elapsed)) if sent_file > 0 and elapsed > 0 else 0.0
|
||||||
|
eta_str = f"{int(eta_sec // 60):02d}:{int(eta_sec % 60):02d}"
|
||||||
|
|
||||||
|
sys.stdout.write(
|
||||||
|
f"\r \033[90mProg: {perc:3.0f}% | {speed:6.1f} KB/s | ETA: {eta_str}\033[0m"
|
||||||
|
)
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
crc32 = conn.fw_put_data(data, timeout=fw_timeout, progress_callback=progress_handler)
|
||||||
|
print("\n Phase 3/3: Finalisiere und warte auf Geräte-ACK...")
|
||||||
|
print(f"\r \033[32mFertig: Firmware übertragen (CRC32: 0x{crc32:08x}).{' ' * 16}\033[0m")
|
||||||
|
print("ℹ️ Nächste Schritte: reboot -> testen -> confirm")
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
# core/commands/get_tag.py
|
|
||||||
from core.connection import BuzzerError
|
|
||||||
from core.util import hex_to_bytearray
|
|
||||||
|
|
||||||
def execute(conn, path: str) -> dict:
|
|
||||||
"""Holt Tags einer Datei und gibt sie als strukturiertes Dictionary zurück."""
|
|
||||||
lines = conn.send_command("get_tag " + path)
|
|
||||||
if not lines:
|
|
||||||
raise BuzzerError("Keine Antwort auf 'get_tag' empfangen.")
|
|
||||||
|
|
||||||
parts = lines[0].split()
|
|
||||||
if len(parts) != 1:
|
|
||||||
raise BuzzerError(f"Unerwartetes get_tag-Format: {lines[0]}")
|
|
||||||
|
|
||||||
data = hex_to_bytearray(parts[0])
|
|
||||||
if data is None:
|
|
||||||
raise BuzzerError("Ungültiger Hex-String in get_tag-Antwort.")
|
|
||||||
|
|
||||||
pos = 0
|
|
||||||
tags = {}
|
|
||||||
while pos < len(data):
|
|
||||||
tag_type = data[pos]
|
|
||||||
pos += 1
|
|
||||||
if pos >= len(data):
|
|
||||||
raise BuzzerError(f"Unerwartetes Ende des Hex-Strings bei get_tag (Tag-Typ erwartet). Position: {pos}/{len(data)}")
|
|
||||||
|
|
||||||
match tag_type:
|
|
||||||
case 0x01: # Kommentar
|
|
||||||
length = data[pos]
|
|
||||||
pos += 1
|
|
||||||
if pos + length > len(data):
|
|
||||||
raise BuzzerError(f"Unerwartetes Ende des Hex-Strings bei get_tag (Kommentar erwartet). Position: {pos}")
|
|
||||||
comment = data[pos:pos+length].decode('utf-8')
|
|
||||||
pos += length
|
|
||||||
tags["comment"] = comment
|
|
||||||
|
|
||||||
case 0x02: # Author
|
|
||||||
length = data[pos]
|
|
||||||
pos += 1
|
|
||||||
if pos + length > len(data):
|
|
||||||
raise BuzzerError(f"Unerwartetes Ende des Hex-Strings bei get_tag (Author erwartet). Position: {pos}")
|
|
||||||
author = data[pos:pos+length].decode('utf-8')
|
|
||||||
pos += length
|
|
||||||
tags["author"] = author
|
|
||||||
|
|
||||||
case 0x10: # CRC32
|
|
||||||
if pos + 4 > len(data):
|
|
||||||
raise BuzzerError(f"Unerwartetes Ende des Hex-Strings bei get_tag (CRC32 erwartet). Position: {pos}")
|
|
||||||
crc32 = int.from_bytes(data[pos:pos+4], byteorder='big')
|
|
||||||
pos += 4
|
|
||||||
tags["crc32"] = hex(crc32)
|
|
||||||
|
|
||||||
case _: # Default / Unbekannter Tag
|
|
||||||
tags[f"unknown_0x{tag_type:02x}"] = tag_value_raw.hex()
|
|
||||||
|
|
||||||
return tags
|
|
||||||
@@ -3,24 +3,23 @@ from core.connection import BuzzerError
|
|||||||
|
|
||||||
def execute(conn) -> dict:
|
def execute(conn) -> dict:
|
||||||
"""Holt die Systeminformationen und gibt sie als strukturiertes Dictionary zurück."""
|
"""Holt die Systeminformationen und gibt sie als strukturiertes Dictionary zurück."""
|
||||||
lines = conn.send_command("info")
|
protocol_version = conn.get_protocol_version()
|
||||||
if not lines:
|
if protocol_version != 1:
|
||||||
raise BuzzerError("Keine Antwort auf 'info' empfangen.")
|
raise BuzzerError(f"Inkompatibles Protokoll: Gerät nutzt v{protocol_version}, Host erwartet v1.")
|
||||||
|
|
||||||
parts = lines[0].split(';')
|
status_code, app_version = conn.get_firmware_status()
|
||||||
# Auf 6 Parameter aktualisiert
|
flash = conn.get_flash_status()
|
||||||
if len(parts) != 6:
|
|
||||||
raise BuzzerError(f"Unerwartetes Info-Format: {lines[0]}")
|
|
||||||
|
|
||||||
protocol_version = int(parts[0])
|
f_frsize = flash["block_size"]
|
||||||
if protocol_version != 2:
|
f_blocks = flash["total_blocks"]
|
||||||
raise BuzzerError(f"Inkompatibles Protokoll: Gerät nutzt v{protocol_version}, Host erwartet v2.")
|
f_bfree = flash["free_blocks"]
|
||||||
|
|
||||||
app_version = parts[1]
|
status_map = {
|
||||||
f_frsize = int(parts[2])
|
1: "CONFIRMED",
|
||||||
f_blocks = int(parts[3])
|
2: "TESTING",
|
||||||
f_bfree = int(parts[4])
|
3: "PENDING",
|
||||||
image_status = parts[5].strip() # CONFIRMED oder UNCONFIRMED
|
}
|
||||||
|
image_status = status_map.get(status_code, f"UNKNOWN({status_code})")
|
||||||
|
|
||||||
total_kb = (f_blocks * f_frsize) / 1024
|
total_kb = (f_blocks * f_frsize) / 1024
|
||||||
free_kb = (f_bfree * f_frsize) / 1024
|
free_kb = (f_bfree * f_frsize) / 1024
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ def get_file_tree(conn, target_path="/", recursive=False) -> list:
|
|||||||
cmd_path = target_path.rstrip('/') if target_path != '/' else '/'
|
cmd_path = target_path.rstrip('/') if target_path != '/' else '/'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
lines = conn.send_command(f"ls {cmd_path}")
|
lines = conn.list_directory(cmd_path)
|
||||||
except BuzzerError as e:
|
except BuzzerError as e:
|
||||||
return [{"type": "E", "name": f"Fehler beim Lesen: {e}", "path": target_path}]
|
return [{"type": "E", "name": f"Fehler beim Lesen: {e}", "path": target_path}]
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from core.connection import BuzzerError
|
|||||||
def execute(conn, path: str):
|
def execute(conn, path: str):
|
||||||
"""Erstellt ein Verzeichnis auf dem Controller."""
|
"""Erstellt ein Verzeichnis auf dem Controller."""
|
||||||
try:
|
try:
|
||||||
conn.send_command(f"mkdir {path}")
|
conn.mkdir(path)
|
||||||
print(f"📁 Verzeichnis '{path}' erfolgreich erstellt.")
|
print(f"📁 Verzeichnis '{path}' erfolgreich erstellt.")
|
||||||
except BuzzerError as e:
|
except BuzzerError as e:
|
||||||
print(f"❌ Fehler beim Erstellen von '{path}': {e}")
|
print(f"❌ Fehler beim Erstellen von '{path}': {e}")
|
||||||
10
buzzer_tool/core/commands/mv.py
Normal file
10
buzzer_tool/core/commands/mv.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from core.connection import BuzzerError
|
||||||
|
|
||||||
|
|
||||||
|
def execute(conn, source: str, target: str):
|
||||||
|
try:
|
||||||
|
conn.rename(source, target)
|
||||||
|
print(f"✅ Umbenannt/Verschoben: '{source}' -> '{target}'")
|
||||||
|
except BuzzerError as e:
|
||||||
|
print(f"❌ Fehler beim Umbenennen/Verschieben: {e}")
|
||||||
|
raise
|
||||||
62
buzzer_tool/core/commands/pull.py
Normal file
62
buzzer_tool/core/commands/pull.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import os
|
||||||
|
import posixpath
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_local_target(remote_path: str, target: str | None) -> str:
|
||||||
|
if target:
|
||||||
|
return target
|
||||||
|
|
||||||
|
basename = posixpath.basename(remote_path.rstrip("/"))
|
||||||
|
if not basename:
|
||||||
|
raise ValueError("Kann keinen lokalen Dateinamen aus dem Remote-Pfad ableiten. Bitte Zielpfad angeben.")
|
||||||
|
return basename
|
||||||
|
|
||||||
|
|
||||||
|
def execute(conn, source: str, target: str | None = None):
|
||||||
|
local_path = _resolve_local_target(source, target)
|
||||||
|
|
||||||
|
os.makedirs(os.path.dirname(local_path) or ".", exist_ok=True)
|
||||||
|
|
||||||
|
last_print = 0.0
|
||||||
|
start_time = time.monotonic()
|
||||||
|
|
||||||
|
def _progress(_chunk_len: int, received: int, expected: int | None):
|
||||||
|
nonlocal last_print
|
||||||
|
now = time.monotonic()
|
||||||
|
if now - last_print < 0.2:
|
||||||
|
return
|
||||||
|
last_print = now
|
||||||
|
|
||||||
|
elapsed = max(now - start_time, 1e-6)
|
||||||
|
speed_kb_s = (received / 1024.0) / elapsed
|
||||||
|
|
||||||
|
if expected is not None and expected > 0:
|
||||||
|
percent = (received * 100.0) / expected
|
||||||
|
remaining = max(expected - received, 0)
|
||||||
|
eta_sec = (remaining / 1024.0) / speed_kb_s if speed_kb_s > 0 else 0.0
|
||||||
|
eta_str = f"{int(eta_sec // 60):02d}:{int(eta_sec % 60):02d}"
|
||||||
|
print(
|
||||||
|
f"\r⬇️ {received}/{expected} B ({percent:5.1f}%) | {speed_kb_s:6.1f} KB/s | ETA {eta_str}",
|
||||||
|
end="",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print(f"\r⬇️ {received} B | {speed_kb_s:6.1f} KB/s", end="", flush=True)
|
||||||
|
|
||||||
|
data = conn.get_file_data(source, progress_callback=_progress)
|
||||||
|
|
||||||
|
if len(data) > 0:
|
||||||
|
print()
|
||||||
|
|
||||||
|
with open(local_path, "wb") as f:
|
||||||
|
f.write(data)
|
||||||
|
|
||||||
|
total_duration = max(time.monotonic() - start_time, 1e-6)
|
||||||
|
avg_speed_kb_s = (len(data) / 1024.0) / total_duration
|
||||||
|
print(f"✅ Heruntergeladen: '{source}' -> '{local_path}' ({len(data)} B, {avg_speed_kb_s:.1f} KB/s)")
|
||||||
|
return {
|
||||||
|
"source": source,
|
||||||
|
"target": local_path,
|
||||||
|
"size": len(data),
|
||||||
|
}
|
||||||
@@ -1,61 +1,202 @@
|
|||||||
import os
|
|
||||||
import zlib
|
|
||||||
import glob
|
import glob
|
||||||
import time
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
from core.connection import BuzzerError
|
from core.connection import BuzzerError
|
||||||
|
|
||||||
def get_file_crc32(filepath: str) -> int:
|
TAG_MAGIC = b"TAG!"
|
||||||
"""Berechnet die IEEE CRC32-Prüfsumme einer Datei in Chunks."""
|
TAG_FOOTER_LEN = 7
|
||||||
crc = 0
|
TAG_VERSION_V1 = 0x01
|
||||||
with open(filepath, 'rb') as f:
|
TAG_TYPE_CRC32 = 0x10
|
||||||
while chunk := f.read(4096):
|
|
||||||
crc = zlib.crc32(chunk, crc)
|
|
||||||
return crc & 0xFFFFFFFF
|
|
||||||
|
|
||||||
def execute(conn, sources: list, target: str):
|
|
||||||
# 1. Globbing auflösen
|
|
||||||
resolved_files = [f for src in sources for f in glob.glob(src) if os.path.isfile(f)]
|
|
||||||
|
|
||||||
if not resolved_files:
|
def _split_audio_and_tag_blob(filepath: str) -> tuple[bytes, bytes | None]:
|
||||||
|
with open(filepath, "rb") as f:
|
||||||
|
data = f.read()
|
||||||
|
|
||||||
|
if len(data) < TAG_FOOTER_LEN:
|
||||||
|
return data, None
|
||||||
|
|
||||||
|
if data[-4:] != TAG_MAGIC:
|
||||||
|
return data, None
|
||||||
|
|
||||||
|
tag_total_len = int.from_bytes(data[-6:-4], byteorder="little", signed=False)
|
||||||
|
tag_version = data[-7]
|
||||||
|
if tag_version != TAG_VERSION_V1:
|
||||||
|
return data, None
|
||||||
|
|
||||||
|
if tag_total_len < TAG_FOOTER_LEN or tag_total_len > len(data):
|
||||||
|
return data, None
|
||||||
|
|
||||||
|
audio_end = len(data) - tag_total_len
|
||||||
|
tag_payload_len = tag_total_len - TAG_FOOTER_LEN
|
||||||
|
tag_payload = data[audio_end:audio_end + tag_payload_len]
|
||||||
|
audio_data = data[:audio_end]
|
||||||
|
return audio_data, tag_payload
|
||||||
|
|
||||||
|
|
||||||
|
def _upsert_crc32_tag(tag_blob: bytes | None, crc32: int) -> tuple[bytes, bool]:
|
||||||
|
crc_payload = int(crc32).to_bytes(4, byteorder="little", signed=False)
|
||||||
|
crc_tlv = bytes([TAG_TYPE_CRC32, 0x04, 0x00]) + crc_payload
|
||||||
|
|
||||||
|
if not tag_blob:
|
||||||
|
return crc_tlv, True
|
||||||
|
|
||||||
|
pos = 0
|
||||||
|
out = bytearray()
|
||||||
|
found_crc = False
|
||||||
|
|
||||||
|
while pos < len(tag_blob):
|
||||||
|
if pos + 3 > len(tag_blob):
|
||||||
|
return crc_tlv, True
|
||||||
|
|
||||||
|
tag_type = tag_blob[pos]
|
||||||
|
tag_len = tag_blob[pos + 1] | (tag_blob[pos + 2] << 8)
|
||||||
|
header = tag_blob[pos:pos + 3]
|
||||||
|
pos += 3
|
||||||
|
|
||||||
|
if pos + tag_len > len(tag_blob):
|
||||||
|
return crc_tlv, True
|
||||||
|
|
||||||
|
value = tag_blob[pos:pos + tag_len]
|
||||||
|
pos += tag_len
|
||||||
|
|
||||||
|
if tag_type == TAG_TYPE_CRC32:
|
||||||
|
if not found_crc:
|
||||||
|
out.extend(crc_tlv)
|
||||||
|
found_crc = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
out.extend(header)
|
||||||
|
out.extend(value)
|
||||||
|
|
||||||
|
if not found_crc:
|
||||||
|
out.extend(crc_tlv)
|
||||||
|
|
||||||
|
return bytes(out), True
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_source_files(sources: list[str], recursive: bool) -> list[dict]:
|
||||||
|
entries = []
|
||||||
|
|
||||||
|
for source in sources:
|
||||||
|
matches = glob.glob(source)
|
||||||
|
if not matches:
|
||||||
|
print(f"⚠️ Keine Treffer für Quelle: {source}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
for match in matches:
|
||||||
|
if os.path.isfile(match):
|
||||||
|
entries.append({"local": match, "relative": os.path.basename(match)})
|
||||||
|
continue
|
||||||
|
|
||||||
|
if os.path.isdir(match):
|
||||||
|
if recursive:
|
||||||
|
for root, _, files in os.walk(match):
|
||||||
|
for name in sorted(files):
|
||||||
|
local_path = os.path.join(root, name)
|
||||||
|
rel = os.path.relpath(local_path, match)
|
||||||
|
entries.append({"local": local_path, "relative": rel.replace("\\", "/")})
|
||||||
|
else:
|
||||||
|
for name in sorted(os.listdir(match)):
|
||||||
|
local_path = os.path.join(match, name)
|
||||||
|
if os.path.isfile(local_path):
|
||||||
|
entries.append({"local": local_path, "relative": name})
|
||||||
|
|
||||||
|
return entries
|
||||||
|
|
||||||
|
|
||||||
|
def _remote_parent(path: str) -> str:
|
||||||
|
idx = path.rfind("/")
|
||||||
|
if idx <= 0:
|
||||||
|
return "/"
|
||||||
|
return path[:idx]
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_remote_dir(conn, remote_dir: str) -> None:
|
||||||
|
if not remote_dir or remote_dir == "/":
|
||||||
|
return
|
||||||
|
|
||||||
|
current = ""
|
||||||
|
for part in [p for p in remote_dir.split("/") if p]:
|
||||||
|
current = f"{current}/{part}"
|
||||||
|
try:
|
||||||
|
conn.mkdir(current)
|
||||||
|
except BuzzerError as e:
|
||||||
|
msg = str(e)
|
||||||
|
if "0x11" in msg or "existiert bereits" in msg:
|
||||||
|
continue
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def _build_upload_plan(entries: list[dict], target: str) -> list[dict]:
|
||||||
|
if not entries:
|
||||||
|
return []
|
||||||
|
|
||||||
|
needs_dir_semantics = target.endswith("/") or len(entries) > 1 or any("/" in e["relative"] for e in entries)
|
||||||
|
if not needs_dir_semantics:
|
||||||
|
return [{"local": entries[0]["local"], "remote": target}]
|
||||||
|
|
||||||
|
base = target.rstrip("/")
|
||||||
|
if not base:
|
||||||
|
base = "/"
|
||||||
|
|
||||||
|
plan = []
|
||||||
|
for entry in entries:
|
||||||
|
rel = entry["relative"].lstrip("/")
|
||||||
|
if base == "/":
|
||||||
|
remote = f"/{rel}"
|
||||||
|
else:
|
||||||
|
remote = f"{base}/{rel}"
|
||||||
|
plan.append({"local": entry["local"], "remote": remote})
|
||||||
|
|
||||||
|
return plan
|
||||||
|
|
||||||
|
|
||||||
|
def execute(conn, sources: list[str], target: str, recursive: bool = False):
|
||||||
|
uploads = _build_upload_plan(_collect_source_files(sources, recursive=recursive), target)
|
||||||
|
if not uploads:
|
||||||
print("Keine gültigen Dateien gefunden.")
|
print("Keine gültigen Dateien gefunden.")
|
||||||
return
|
return
|
||||||
|
|
||||||
total_size_all = sum(os.path.getsize(f) for f in resolved_files)
|
total_size_all = sum(os.path.getsize(item["local"]) for item in uploads)
|
||||||
sent_all = 0
|
sent_all = 0
|
||||||
start_time_all = time.monotonic()
|
start_time_all = time.monotonic()
|
||||||
is_target_dir = target.endswith('/')
|
last_ui_update = start_time_all
|
||||||
|
|
||||||
for filepath in resolved_files:
|
for item in uploads:
|
||||||
filename = os.path.basename(filepath)
|
local_path = item["local"]
|
||||||
filesize = os.path.getsize(filepath)
|
remote_path = item["remote"]
|
||||||
crc32 = get_file_crc32(filepath)
|
filename = os.path.basename(local_path)
|
||||||
dest_path = f"{target}{filename}" if is_target_dir else target
|
|
||||||
|
|
||||||
print(f"Sende 📄 {filename} ({filesize/1024:.1f} KB) -> {dest_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()
|
start_time_file = time.monotonic()
|
||||||
sent_file = 0
|
|
||||||
|
|
||||||
def progress_handler(chunk_len):
|
def progress_handler(chunk_len, sent_file, total_file):
|
||||||
nonlocal sent_file, sent_all
|
nonlocal sent_all, last_ui_update
|
||||||
sent_file += chunk_len
|
|
||||||
sent_all += chunk_len
|
sent_all += chunk_len
|
||||||
|
|
||||||
elapsed = time.monotonic() - start_time_file
|
now = time.monotonic()
|
||||||
speed = (sent_file / 1024) / elapsed if elapsed > 0 else 0
|
if now - last_ui_update < 0.2 and sent_file < total_file:
|
||||||
|
return
|
||||||
|
last_ui_update = now
|
||||||
|
|
||||||
# Prozentberechnungen
|
elapsed = now - start_time_file
|
||||||
perc_file = (sent_file / filesize) * 100
|
speed = (sent_file / 1024.0) / elapsed if elapsed > 0 else 0.0
|
||||||
perc_all = (sent_all / total_size_all) * 100
|
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
|
||||||
|
|
||||||
# ETA (Basierend auf Gesamtgeschwindigkeit)
|
elapsed_all = now - start_time_all
|
||||||
elapsed_all = time.monotonic() - start_time_all
|
avg_speed_all = sent_all / elapsed_all if elapsed_all > 0 else 0.0
|
||||||
avg_speed_all = sent_all / elapsed_all if elapsed_all > 0 else 0
|
eta_sec = (total_size_all - sent_all) / avg_speed_all if avg_speed_all > 0 else 0.0
|
||||||
eta_sec = (total_size_all - sent_all) / avg_speed_all if avg_speed_all > 0 else 0
|
|
||||||
eta_str = f"{int(eta_sec // 60):02d}:{int(eta_sec % 60):02d}"
|
eta_str = f"{int(eta_sec // 60):02d}:{int(eta_sec % 60):02d}"
|
||||||
|
|
||||||
# Ausgabezeile (\r überschreibt die aktuelle Zeile)
|
|
||||||
sys.stdout.write(
|
sys.stdout.write(
|
||||||
f"\r \033[90mProg: {perc_file:3.0f}% | Gesamt: {perc_all:3.0f}% | "
|
f"\r \033[90mProg: {perc_file:3.0f}% | Gesamt: {perc_all:3.0f}% | "
|
||||||
f"{speed:6.1f} KB/s | ETA: {eta_str}\033[0m"
|
f"{speed:6.1f} KB/s | ETA: {eta_str}\033[0m"
|
||||||
@@ -63,20 +204,17 @@ def execute(conn, sources: list, target: str):
|
|||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cmd = f"put {dest_path};{filesize};{crc32}\n"
|
audio_crc32 = conn.put_file_data(remote_path, audio_data, progress_callback=progress_handler)
|
||||||
conn.serial.write(cmd.encode('utf-8'))
|
|
||||||
conn.serial.flush()
|
|
||||||
|
|
||||||
# Binärtransfer mit unserem Handler
|
rewritten_blob, _ = _upsert_crc32_tag(tag_blob, audio_crc32)
|
||||||
conn.send_binary(filepath, progress_callback=progress_handler)
|
conn.set_tag_blob(remote_path, rewritten_blob)
|
||||||
|
tag_note = " (CRC32-Tag gesetzt)"
|
||||||
|
|
||||||
# Zeile nach Erfolg abschließen
|
print(f"\r \033[32mFertig: {filename} übertragen{tag_note}.{' ' * 20}\033[0m")
|
||||||
print(f"\r \033[32mFertig: {filename} übertragen. \033[0m")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"\n ❌ \033[31mFehler: {e}\033[0m")
|
print(f"\n ❌ \033[31mFehler: {e}\033[0m")
|
||||||
|
|
||||||
total_duration = time.monotonic() - start_time_all
|
total_duration = time.monotonic() - start_time_all
|
||||||
total_kb = total_size_all / 1024
|
total_kb = total_size_all / 1024.0
|
||||||
avg_speed = total_kb / total_duration if total_duration > 0 else 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)")
|
print(f"\nÜbertragung abgeschlossen: {total_kb:.1f} KB in {total_duration:.1f}s ({avg_speed:.1f} KB/s)")
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
# core/commands/mkdir.py
|
# core/commands/reboot.py
|
||||||
from core.connection import BuzzerError
|
from core.connection import BuzzerError
|
||||||
|
|
||||||
|
|
||||||
def execute(conn):
|
def execute(conn):
|
||||||
"""Startet den Buzzer neu."""
|
"""Startet den Buzzer per Binary-Protokoll neu."""
|
||||||
try:
|
try:
|
||||||
conn.send_command(f"reboot")
|
conn.reboot_device()
|
||||||
print(f"🔄 Buzzer wird neu gestartet.")
|
print("🔄 Buzzer wird neu gestartet.")
|
||||||
except BuzzerError as e:
|
except BuzzerError as e:
|
||||||
print(f"❌ Fehler beim Neustarten des Buzzers: {e}")
|
print(f"❌ Fehler beim Neustarten des Buzzers: {e}")
|
||||||
@@ -17,7 +17,7 @@ def _delete_recursive(conn, nodes):
|
|||||||
def _try_rm(conn, path, is_dir=False):
|
def _try_rm(conn, path, is_dir=False):
|
||||||
icon = "📁" if is_dir else "📄"
|
icon = "📁" if is_dir else "📄"
|
||||||
try:
|
try:
|
||||||
conn.send_command(f"rm {path}")
|
conn.rm(path)
|
||||||
print(f" 🗑️ {icon} Gelöscht: {path}")
|
print(f" 🗑️ {icon} Gelöscht: {path}")
|
||||||
except BuzzerError as e:
|
except BuzzerError as e:
|
||||||
print(f" ❌ Fehler bei {path}: {e}")
|
print(f" ❌ Fehler bei {path}: {e}")
|
||||||
@@ -53,21 +53,16 @@ def execute(conn, path: str, recursive: bool = False):
|
|||||||
|
|
||||||
# 2. Rekursives Löschen (-r)
|
# 2. Rekursives Löschen (-r)
|
||||||
if recursive:
|
if recursive:
|
||||||
print(f"Sammle Dateibaum für rekursives Löschen von '{path}'...")
|
try:
|
||||||
tree = get_file_tree(conn, target_path=path, recursive=True)
|
conn.rm_recursive(path)
|
||||||
|
print(f"🗑️ '{path}' rekursiv gelöscht.")
|
||||||
if len(tree) == 1 and tree[0].get("type") == "E":
|
except BuzzerError as e:
|
||||||
print(f"❌ Pfad nicht gefunden oder Fehler: {tree[0]['name']}")
|
print(f"❌ Fehler beim rekursiven Löschen von '{path}': {e}")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not tree:
|
# 3. Standard-Löschen (Einzeldatei oder leeres Verzeichnis)
|
||||||
print(f"Ordner '{path}' ist bereits leer.")
|
|
||||||
else:
|
|
||||||
_delete_recursive(conn, tree)
|
|
||||||
|
|
||||||
# 3. Standard-Löschen (Einzeldatei oder am Ende der Rekursion der leere Ordner)
|
|
||||||
try:
|
try:
|
||||||
conn.send_command(f"rm {path}")
|
conn.rm(path)
|
||||||
print(f"🗑️ '{path}' erfolgreich gelöscht.")
|
print(f"🗑️ '{path}' erfolgreich gelöscht.")
|
||||||
except BuzzerError as e:
|
except BuzzerError as e:
|
||||||
print(f"❌ Fehler beim Löschen von '{path}': {e}")
|
print(f"❌ Fehler beim Löschen von '{path}': {e}")
|
||||||
5
buzzer_tool/core/commands/stat.py
Normal file
5
buzzer_tool/core/commands/stat.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
def execute(conn, path: str):
|
||||||
|
info = conn.stat(path)
|
||||||
|
entry_type = "Ordner" if info["type"] == "D" else "Datei"
|
||||||
|
print(f"{path}: {entry_type}, Größe: {info['size']} B")
|
||||||
|
return info
|
||||||
140
buzzer_tool/core/commands/tags.py
Normal file
140
buzzer_tool/core/commands/tags.py
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import json
|
||||||
|
from core.connection import BuzzerError
|
||||||
|
|
||||||
|
TAG_TYPE_TO_KEY = {
|
||||||
|
0x00: "description",
|
||||||
|
0x01: "author",
|
||||||
|
0x10: "crc32",
|
||||||
|
0x20: "fileformat",
|
||||||
|
}
|
||||||
|
|
||||||
|
KEY_TO_TAG_TYPE = {v: k for k, v in TAG_TYPE_TO_KEY.items()}
|
||||||
|
VALID_TAG_KEYS = frozenset(KEY_TO_TAG_TYPE.keys())
|
||||||
|
|
||||||
|
def _u16le(value: int) -> bytes:
|
||||||
|
return bytes((value & 0xFF, (value >> 8) & 0xFF))
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_tlv(blob: bytes) -> dict:
|
||||||
|
tags = {}
|
||||||
|
pos = 0
|
||||||
|
|
||||||
|
while pos < len(blob):
|
||||||
|
if pos + 3 > len(blob):
|
||||||
|
raise BuzzerError("Ungültiger Tag-Blob: TLV-Header abgeschnitten")
|
||||||
|
|
||||||
|
tag_type = blob[pos]
|
||||||
|
tag_len = blob[pos + 1] | (blob[pos + 2] << 8)
|
||||||
|
pos += 3
|
||||||
|
|
||||||
|
if pos + tag_len > len(blob):
|
||||||
|
raise BuzzerError("Ungültiger Tag-Blob: TLV-Wert abgeschnitten")
|
||||||
|
|
||||||
|
value = blob[pos:pos + tag_len]
|
||||||
|
pos += tag_len
|
||||||
|
|
||||||
|
key = TAG_TYPE_TO_KEY.get(tag_type, f"unknown_0x{tag_type:02x}")
|
||||||
|
|
||||||
|
if tag_type in (0x00, 0x01):
|
||||||
|
tags[key] = value.decode("utf-8", errors="replace")
|
||||||
|
elif tag_type == 0x10:
|
||||||
|
if tag_len != 4:
|
||||||
|
raise BuzzerError("Ungültiger crc32-Tag: len muss 4 sein")
|
||||||
|
crc32 = int.from_bytes(value, byteorder="little", signed=False)
|
||||||
|
tags[key] = f"0x{crc32:08x}"
|
||||||
|
elif tag_type == 0x20:
|
||||||
|
if tag_len != 5:
|
||||||
|
raise BuzzerError("Ungültiger fileformat-Tag: len muss 5 sein")
|
||||||
|
bits = value[0]
|
||||||
|
samplerate = int.from_bytes(value[1:5], byteorder="little", signed=False)
|
||||||
|
tags[key] = {"bits_per_sample": bits, "sample_rate": samplerate}
|
||||||
|
else:
|
||||||
|
tags[key] = value.hex()
|
||||||
|
|
||||||
|
return tags
|
||||||
|
|
||||||
|
|
||||||
|
def _build_tlv(tags: dict) -> bytes:
|
||||||
|
entries = []
|
||||||
|
|
||||||
|
if "description" in tags and tags["description"] is not None:
|
||||||
|
data = str(tags["description"]).encode("utf-8")
|
||||||
|
entries.append(bytes([KEY_TO_TAG_TYPE["description"]]) + _u16le(len(data)) + data)
|
||||||
|
|
||||||
|
if "author" in tags and tags["author"] is not None:
|
||||||
|
data = str(tags["author"]).encode("utf-8")
|
||||||
|
entries.append(bytes([KEY_TO_TAG_TYPE["author"]]) + _u16le(len(data)) + data)
|
||||||
|
|
||||||
|
if "crc32" in tags and tags["crc32"] is not None:
|
||||||
|
crc_val = tags["crc32"]
|
||||||
|
if isinstance(crc_val, str):
|
||||||
|
crc_val = int(crc_val, 16) if crc_val.lower().startswith("0x") else int(crc_val)
|
||||||
|
data = int(crc_val).to_bytes(4, byteorder="little", signed=False)
|
||||||
|
entries.append(bytes([KEY_TO_TAG_TYPE["crc32"]]) + _u16le(4) + data)
|
||||||
|
|
||||||
|
if "fileformat" in tags and tags["fileformat"] is not None:
|
||||||
|
ff = tags["fileformat"]
|
||||||
|
if not isinstance(ff, dict):
|
||||||
|
raise BuzzerError("fileformat muss ein Objekt sein: {bits_per_sample, sample_rate}")
|
||||||
|
bits = int(ff.get("bits_per_sample", 16))
|
||||||
|
samplerate = int(ff.get("sample_rate", 16000))
|
||||||
|
data = bytes([bits]) + samplerate.to_bytes(4, byteorder="little", signed=False)
|
||||||
|
entries.append(bytes([KEY_TO_TAG_TYPE["fileformat"]]) + _u16le(5) + data)
|
||||||
|
|
||||||
|
return b"".join(entries)
|
||||||
|
|
||||||
|
|
||||||
|
def get_tags(conn, path: str) -> dict:
|
||||||
|
blob = conn.get_tag_blob(path)
|
||||||
|
if not blob:
|
||||||
|
return {}
|
||||||
|
return _parse_tlv(blob)
|
||||||
|
|
||||||
|
|
||||||
|
def write_tags(conn, path: str, tags_update: dict) -> dict:
|
||||||
|
unknown_keys = [k for k in tags_update.keys() if k not in VALID_TAG_KEYS]
|
||||||
|
if unknown_keys:
|
||||||
|
unknown_str = ", ".join(sorted(str(k) for k in unknown_keys))
|
||||||
|
valid_str = ", ".join(sorted(VALID_TAG_KEYS))
|
||||||
|
raise BuzzerError(
|
||||||
|
f"Unbekannter Tag-Key in write_tags: {unknown_str}. Erlaubte Keys: {valid_str}"
|
||||||
|
)
|
||||||
|
|
||||||
|
current = get_tags(conn, path)
|
||||||
|
merged = dict(current)
|
||||||
|
|
||||||
|
for key, value in tags_update.items():
|
||||||
|
if value is None:
|
||||||
|
merged.pop(key, None)
|
||||||
|
else:
|
||||||
|
merged[key] = value
|
||||||
|
|
||||||
|
blob = _build_tlv(merged)
|
||||||
|
conn.set_tag_blob(path, blob)
|
||||||
|
return merged
|
||||||
|
|
||||||
|
|
||||||
|
def remove_tag(conn, path: str, key: str) -> dict:
|
||||||
|
current = get_tags(conn, path)
|
||||||
|
current.pop(key, None)
|
||||||
|
blob = _build_tlv(current)
|
||||||
|
conn.set_tag_blob(path, blob)
|
||||||
|
return current
|
||||||
|
|
||||||
|
|
||||||
|
def parse_tags_json_input(raw: str) -> dict:
|
||||||
|
text = raw.strip()
|
||||||
|
if text.startswith("@"):
|
||||||
|
file_path = text[1:]
|
||||||
|
with open(file_path, "r", encoding="utf-8") as f:
|
||||||
|
text = f.read()
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = json.loads(text)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
raise BuzzerError(f"Ungültiges JSON für write_tags: {e}")
|
||||||
|
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
raise BuzzerError("write_tags erwartet ein JSON-Objekt.")
|
||||||
|
|
||||||
|
return data
|
||||||
@@ -6,7 +6,9 @@ import yaml
|
|||||||
DEFAULT_CONFIG = {
|
DEFAULT_CONFIG = {
|
||||||
"port": None,
|
"port": None,
|
||||||
"baudrate": 115200,
|
"baudrate": 115200,
|
||||||
"timeout": 5.0
|
"timeout": 5.0,
|
||||||
|
"crc_timeout_min_seconds": 2.0,
|
||||||
|
"crc_timeout_ms_per_100kb": 1.5,
|
||||||
}
|
}
|
||||||
|
|
||||||
def load_config(cli_args=None):
|
def load_config(cli_args=None):
|
||||||
@@ -26,6 +28,12 @@ def load_config(cli_args=None):
|
|||||||
config["port"] = yaml_data["serial"].get("port", config["port"])
|
config["port"] = yaml_data["serial"].get("port", config["port"])
|
||||||
config["baudrate"] = yaml_data["serial"].get("baudrate", config["baudrate"])
|
config["baudrate"] = yaml_data["serial"].get("baudrate", config["baudrate"])
|
||||||
config["timeout"] = yaml_data["serial"].get("timeout", config["timeout"])
|
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:
|
except Exception as e:
|
||||||
print(f"Fehler beim Laden der Konfigurationsdatei {yaml_path}: {e}")
|
print(f"Fehler beim Laden der Konfigurationsdatei {yaml_path}: {e}")
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
# core/connection.py
|
# core/connection.py
|
||||||
import serial
|
|
||||||
import time
|
import time
|
||||||
import os
|
import os
|
||||||
|
import struct
|
||||||
|
import binascii
|
||||||
|
|
||||||
PROTOCOL_ERROR_MESSAGES = {
|
PROTOCOL_ERROR_MESSAGES = {
|
||||||
0x01: "Ungültiger Befehl.",
|
0x01: "Ungültiger Befehl.",
|
||||||
@@ -23,6 +24,40 @@ PROTOCOL_ERROR_MESSAGES = {
|
|||||||
0x32: "Interner Gerätefehler.",
|
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):
|
class BuzzerError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -31,12 +66,21 @@ class BuzzerConnection:
|
|||||||
self.port = config.get("port")
|
self.port = config.get("port")
|
||||||
self.baudrate = config.get("baudrate", 115200)
|
self.baudrate = config.get("baudrate", 115200)
|
||||||
self.timeout = config.get("timeout", 5.0)
|
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.serial = None
|
||||||
|
self._sequence = 0
|
||||||
|
self._max_path_len = DEFAULT_MAX_PATH_LEN
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
if not self.port:
|
if not self.port:
|
||||||
raise ValueError("Kein serieller Port konfiguriert.")
|
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
|
# write_timeout verhindert endloses Blockieren auf inaktiven Ports
|
||||||
self.serial = serial.Serial(
|
self.serial = serial.Serial(
|
||||||
port=self.port,
|
port=self.port,
|
||||||
@@ -61,6 +105,670 @@ class BuzzerConnection:
|
|||||||
message = PROTOCOL_ERROR_MESSAGES.get(code, "Unbekannter Fehlercode vom Gerät.")
|
message = PROTOCOL_ERROR_MESSAGES.get(code, "Unbekannter Fehlercode vom Gerät.")
|
||||||
return f"Controller-Fehler {code} (0x{code:02X}): {message}"
|
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:
|
def send_command(self, command: str, custom_timeout: float = None) -> list:
|
||||||
eff_timeout = custom_timeout if custom_timeout is not None else self.timeout
|
eff_timeout = custom_timeout if custom_timeout is not None else self.timeout
|
||||||
self.serial.reset_input_buffer()
|
self.serial.reset_input_buffer()
|
||||||
@@ -68,8 +776,10 @@ class BuzzerConnection:
|
|||||||
try:
|
try:
|
||||||
self.serial.write(f"{command}\n".encode('utf-8'))
|
self.serial.write(f"{command}\n".encode('utf-8'))
|
||||||
self.serial.flush()
|
self.serial.flush()
|
||||||
except serial.SerialTimeoutException:
|
except Exception as e:
|
||||||
raise TimeoutError(f"Schreib-Timeout am Port {self.port}. Ist das Gerät blockiert?")
|
if e.__class__.__name__ == "SerialTimeoutException":
|
||||||
|
raise TimeoutError(f"Schreib-Timeout am Port {self.port}. Ist das Gerät blockiert?") from e
|
||||||
|
raise
|
||||||
|
|
||||||
lines = []
|
lines = []
|
||||||
start_time = time.monotonic()
|
start_time = time.monotonic()
|
||||||
@@ -91,7 +801,7 @@ class BuzzerConnection:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise BuzzerError(f"Fehler beim Lesen der Antwort: {e}")
|
raise BuzzerError(f"Fehler beim Lesen der Antwort: {e}")
|
||||||
else:
|
else:
|
||||||
time.sleep(0.01)
|
time.sleep(POLL_SLEEP_SECONDS)
|
||||||
|
|
||||||
raise TimeoutError(f"Lese-Timeout ({eff_timeout}s) beim Warten auf Antwort für: '{command}'")
|
raise TimeoutError(f"Lese-Timeout ({eff_timeout}s) beim Warten auf Antwort für: '{command}'")
|
||||||
|
|
||||||
@@ -110,7 +820,7 @@ class BuzzerConnection:
|
|||||||
break
|
break
|
||||||
elif line.startswith("ERR"):
|
elif line.startswith("ERR"):
|
||||||
raise BuzzerError(f"Fehler vor Binärtransfer: {self._parse_controller_error(line)}")
|
raise BuzzerError(f"Fehler vor Binärtransfer: {self._parse_controller_error(line)}")
|
||||||
time.sleep(0.01)
|
time.sleep(POLL_SLEEP_SECONDS)
|
||||||
|
|
||||||
if not ready:
|
if not ready:
|
||||||
raise TimeoutError("Kein READY-Signal vom Controller empfangen.")
|
raise TimeoutError("Kein READY-Signal vom Controller empfangen.")
|
||||||
@@ -151,6 +861,6 @@ class BuzzerConnection:
|
|||||||
return True
|
return True
|
||||||
elif line.startswith("ERR"):
|
elif line.startswith("ERR"):
|
||||||
raise BuzzerError(f"Fehler beim Speichern der Binärdatei: {self._parse_controller_error(line)}")
|
raise BuzzerError(f"Fehler beim Speichern der Binärdatei: {self._parse_controller_error(line)}")
|
||||||
time.sleep(0.01)
|
time.sleep(POLL_SLEEP_SECONDS)
|
||||||
|
|
||||||
raise TimeoutError("Zeitüberschreitung nach Binärtransfer (kein OK empfangen).")
|
raise TimeoutError("Zeitüberschreitung nach Binärtransfer (kein OK empfangen).")
|
||||||
165
buzzer_tool/smoke_test.sh
Executable file
165
buzzer_tool/smoke_test.sh
Executable file
@@ -0,0 +1,165 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ROOT_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
CLI="$ROOT_DIR/buzzer_tool/buzzer.py"
|
||||||
|
|
||||||
|
PORT=""
|
||||||
|
BAUDRATE=""
|
||||||
|
TIMEOUT=""
|
||||||
|
REMOTE_BASE="/lfs/smoke"
|
||||||
|
KEEP_TMP=0
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<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"
|
||||||
303
firmware/Protokoll.md
Normal file
303
firmware/Protokoll.md
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
# Buzzer-Kommunikationsprotokoll (vNext Entwurf)
|
||||||
|
|
||||||
|
Diese Datei beschreibt den geplanten Neuentwurf des Protokolls. Ziel ist ein klar strukturiertes Binärprotokoll mit stabilen Typen, expliziter Endianness und sauberem Streaming für große Daten.
|
||||||
|
|
||||||
|
## 1) Grundregeln
|
||||||
|
|
||||||
|
- Transport: USB CDC (Byte-Stream)
|
||||||
|
- Multi-Byte-Integer: **Little Endian**
|
||||||
|
- Integer-Typen im Protokoll: nur feste Breiten (`uint8_t`, `uint16_t`, `uint32_t`, `uint64_t`)
|
||||||
|
- `bool`: als `uint8_t` (`0` = false, `1` = true)
|
||||||
|
- Strings: immer `len + bytes`, ohne implizites `\0`
|
||||||
|
- `unsigned long` wird im Protokoll **nicht** verwendet (plattformabhängig)
|
||||||
|
|
||||||
|
> Hinweis: Obwohl nRF52 und STM32G0 beide Little Endian sind, bleibt Endianness explizit Teil der Spezifikation.
|
||||||
|
|
||||||
|
## 2) Frame-Struktur
|
||||||
|
|
||||||
|
Alle Requests und Responses nutzen denselben Header.
|
||||||
|
|
||||||
|
### 2.1 Header
|
||||||
|
|
||||||
|
| Feld | Typ | Beschreibung |
|
||||||
|
|---|---|---|
|
||||||
|
| `sync` | 4 Byte | Immer `BUZZ` (`0x42 0x55 0x5A 0x5A`) |
|
||||||
|
| `frame_type` | `uint8_t` | Typ des Frames (siehe unten) |
|
||||||
|
| `command_id` | `uint8_t` | Kommando-ID |
|
||||||
|
| `sequence` | `uint16_t` | Anfrage-/Antwort-Korrelation |
|
||||||
|
| `payload_len` | `uint32_t` | Länge von `payload` in Bytes |
|
||||||
|
| `header_crc16` | `uint16_t` | CRC16 über Header ohne `sync` und ohne `header_crc16` |
|
||||||
|
|
||||||
|
Danach folgen:
|
||||||
|
|
||||||
|
| Feld | Typ | Beschreibung |
|
||||||
|
|---|---|---|
|
||||||
|
| `payload` | `byte[payload_len]` | Kommandoabhängige Nutzdaten |
|
||||||
|
| `payload_crc32` | `uint32_t` | CRC32 über `payload` |
|
||||||
|
|
||||||
|
### 2.2 `frame_type`
|
||||||
|
|
||||||
|
| Wert | Name | Bedeutung |
|
||||||
|
|---:|---|---|
|
||||||
|
| `0x01` | `REQ` | Host → Device Anfrage |
|
||||||
|
| `0x10` | `RESP_ACK` | Erfolgreich, ohne Nutzdaten |
|
||||||
|
| `0x11` | `RESP_DATA` | Erfolgreich, mit Nutzdaten |
|
||||||
|
| `0x12` | `RESP_STREAM_START` | Start eines Datenstroms |
|
||||||
|
| `0x13` | `RESP_STREAM_CHUNK` | Chunk eines Datenstroms |
|
||||||
|
| `0x14` | `RESP_STREAM_END` | Ende eines Datenstroms |
|
||||||
|
| `0x7F` | `RESP_ERROR` | Fehlerantwort |
|
||||||
|
|
||||||
|
## 3) Antwortmodell (ACK vs DATA)
|
||||||
|
|
||||||
|
Ja, wir unterscheiden explizit:
|
||||||
|
|
||||||
|
- **`RESP_ACK`**: „OK ohne Payload“ (Host muss nur Erfolg verbuchen)
|
||||||
|
- **`RESP_DATA`**: „OK mit Payload“
|
||||||
|
- **`RESP_ERROR`**: Fehlercode + optionaler Detailtext
|
||||||
|
|
||||||
|
Damit ist Parsing eindeutig und ohne Sonderfälle wie „OK aber vielleicht mit Daten“.
|
||||||
|
|
||||||
|
## 4) Fehlercodes
|
||||||
|
|
||||||
|
Fehlercodes bleiben wie bisher inhaltlich erhalten (`P_ERR_*`), werden aber als Binärwert in `RESP_ERROR` übertragen.
|
||||||
|
|
||||||
|
### 4.1 Fehler-Payload (`RESP_ERROR`)
|
||||||
|
|
||||||
|
| Feld | Typ | Beschreibung |
|
||||||
|
|---|---|---|
|
||||||
|
| `error_code` | `uint8_t` | Protokollfehlercode |
|
||||||
|
| `detail_len` | `uint8_t` | Länge von `detail` |
|
||||||
|
| `detail` | `byte[detail_len]` | Optionaler Kurztext (ASCII/UTF-8) |
|
||||||
|
|
||||||
|
## 5) Kommandos (erste Aufteilung)
|
||||||
|
|
||||||
|
## 5.1 `0x00` `GET_PROTOCOL_VERSION`
|
||||||
|
|
||||||
|
**Request-Payload:** keine
|
||||||
|
|
||||||
|
**Response:** `RESP_DATA`
|
||||||
|
|
||||||
|
| Feld | Typ | Beschreibung |
|
||||||
|
|---|---|---|
|
||||||
|
| `protocol_version` | `uint16_t` | Versionsnummer des Protokolls |
|
||||||
|
|
||||||
|
## 5.2 `0x01` `GET_FIRMWARE_STATUS`
|
||||||
|
|
||||||
|
**Request-Payload:** keine
|
||||||
|
|
||||||
|
**Response:** `RESP_DATA`
|
||||||
|
|
||||||
|
| Feld | Typ | Beschreibung |
|
||||||
|
|---|---|---|
|
||||||
|
| `status` | `uint8_t` | `0` = pending, `1` = confirmed |
|
||||||
|
| `version_len` | `uint8_t` | Länge des Versionsstrings |
|
||||||
|
| `version` | `byte[version_len]` | Firmware-Version |
|
||||||
|
|
||||||
|
## 5.3 `0x02` `GET_FLASH_STATUS`
|
||||||
|
|
||||||
|
**Request-Payload:** keine
|
||||||
|
|
||||||
|
**Response:** `RESP_DATA`
|
||||||
|
|
||||||
|
| Feld | Typ | Beschreibung |
|
||||||
|
|---|---|---|
|
||||||
|
| `block_size` | `uint32_t` | Bytes pro Block |
|
||||||
|
| `total_blocks` | `uint32_t` | Gesamtblöcke |
|
||||||
|
| `free_blocks` | `uint32_t` | Freie Blöcke |
|
||||||
|
| `path_max_len` | `uint32_t` | Maximale erlaubte Pfadlänge in Bytes |
|
||||||
|
|
||||||
|
## 5.4 `0x10` `LIST_DIR`
|
||||||
|
|
||||||
|
**Request-Payload:**
|
||||||
|
|
||||||
|
| Feld | Typ | Beschreibung |
|
||||||
|
|---|---|---|
|
||||||
|
| `path_len` | `uint8_t` | Länge von `path` |
|
||||||
|
| `path` | `byte[path_len]` | Zielverzeichnis, z. B. `/` oder `/lfs/a` |
|
||||||
|
|
||||||
|
`path_len` darf den in `GET_FLASH_STATUS.path_max_len` gemeldeten Wert nicht überschreiten.
|
||||||
|
|
||||||
|
**Response:** `RESP_DATA`
|
||||||
|
|
||||||
|
`LIST_DIR` liefert Einträge als Stream:
|
||||||
|
|
||||||
|
1. `RESP_STREAM_START`
|
||||||
|
2. `RESP_STREAM_CHUNK` pro Verzeichniseintrag
|
||||||
|
3. `RESP_STREAM_END`
|
||||||
|
|
||||||
|
### `RESP_STREAM_START` Payload
|
||||||
|
|
||||||
|
| Feld | Typ | Beschreibung |
|
||||||
|
|---|---|---|
|
||||||
|
| `entry_count` | `uint32_t` | Anzahl Einträge, `0xFFFFFFFF` = unbekannt |
|
||||||
|
|
||||||
|
### `RESP_STREAM_CHUNK` Payload (ein Eintrag)
|
||||||
|
|
||||||
|
| Feld | Typ | Beschreibung |
|
||||||
|
|---|---|---|
|
||||||
|
| `entry_type` | `uint8_t` | `0` = Datei, `1` = Verzeichnis |
|
||||||
|
| `name_len` | `uint8_t` | Länge von `name` |
|
||||||
|
| `size` | `uint32_t` | Dateigröße in Bytes (`0` für Verzeichnisse) |
|
||||||
|
| `name` | `byte[name_len]` | Name ohne führenden Pfad |
|
||||||
|
|
||||||
|
### `RESP_STREAM_END` Payload
|
||||||
|
|
||||||
|
- leer
|
||||||
|
|
||||||
|
**Hinweis:** Rekursion bleibt Host-seitig. Der Host setzt aus angefragtem Basispfad + `name` den vollständigen Pfad zusammen.
|
||||||
|
|
||||||
|
## 5.5 `0x11` `CHECK_FILE_CRC`
|
||||||
|
|
||||||
|
**Request-Payload:**
|
||||||
|
|
||||||
|
| Feld | Typ | Beschreibung |
|
||||||
|
|---|---|---|
|
||||||
|
| `path_len` | `uint8_t` | Länge von `path` |
|
||||||
|
| `path` | `byte[path_len]` | Pfad zur Datei |
|
||||||
|
|
||||||
|
`path_len` darf den in `GET_FLASH_STATUS.path_max_len` gemeldeten Wert nicht überschreiten.
|
||||||
|
|
||||||
|
**Response:** `RESP_DATA`
|
||||||
|
|
||||||
|
| Feld | Typ | Beschreibung |
|
||||||
|
|---|---|---|
|
||||||
|
| `crc32` | `uint32_t` | CRC32 (Little Endian) über den Audio-Inhalt |
|
||||||
|
|
||||||
|
**Hinweis:** Die CRC-Berechnung folgt der aktuellen Firmware-Logik für Audio-Dateien (ohne angehängte Tag-Daten).
|
||||||
|
|
||||||
|
## 5.6 `0x12` `MKDIR`
|
||||||
|
|
||||||
|
**Request-Payload:**
|
||||||
|
|
||||||
|
| Feld | Typ | Beschreibung |
|
||||||
|
|---|---|---|
|
||||||
|
| `path_len` | `uint8_t` | Länge von `path` |
|
||||||
|
| `path` | `byte[path_len]` | Zielpfad für neues Verzeichnis |
|
||||||
|
|
||||||
|
`path_len` darf den in `GET_FLASH_STATUS.path_max_len` gemeldeten Wert nicht überschreiten.
|
||||||
|
|
||||||
|
**Response:** `RESP_ACK`
|
||||||
|
|
||||||
|
Bei Erfolg wird eine leere ACK-Antwort gesendet. Bei Fehlern `RESP_ERROR` mit passendem `P_ERR_*`.
|
||||||
|
|
||||||
|
## 5.7 `0x13` `RM`
|
||||||
|
|
||||||
|
**Request-Payload:**
|
||||||
|
|
||||||
|
| Feld | Typ | Beschreibung |
|
||||||
|
|---|---|---|
|
||||||
|
| `path_len` | `uint8_t` | Länge von `path` |
|
||||||
|
| `path` | `byte[path_len]` | Pfad zu Datei oder leerem Verzeichnis |
|
||||||
|
|
||||||
|
`path_len` darf den in `GET_FLASH_STATUS.path_max_len` gemeldeten Wert nicht überschreiten.
|
||||||
|
|
||||||
|
**Response:** `RESP_ACK`
|
||||||
|
|
||||||
|
Bei Erfolg wird eine leere ACK-Antwort gesendet. Bei Fehlern `RESP_ERROR` mit passendem `P_ERR_*`.
|
||||||
|
|
||||||
|
**Hinweis:** Rekursives Löschen bleibt Host-seitig (mehrere `LIST_DIR` + `RM` Aufrufe).
|
||||||
|
|
||||||
|
## 5.8 `0x20` `GET_TAG_BLOB`
|
||||||
|
|
||||||
|
**Request-Payload:**
|
||||||
|
|
||||||
|
| Feld | Typ | Beschreibung |
|
||||||
|
|---|---|---|
|
||||||
|
| `path_len` | `uint8_t` | Länge von `path` |
|
||||||
|
| `path` | `byte[path_len]` | Pfad zur Datei |
|
||||||
|
|
||||||
|
`path_len` darf den in `GET_FLASH_STATUS.path_max_len` gemeldeten Wert nicht überschreiten.
|
||||||
|
|
||||||
|
**Response:** Stream
|
||||||
|
|
||||||
|
1. `RESP_STREAM_START` mit `total_len:uint32_t`
|
||||||
|
2. `RESP_STREAM_CHUNK` mit rohen Tag-Blob-Bytes
|
||||||
|
3. `RESP_STREAM_END` (leer)
|
||||||
|
|
||||||
|
Der Blob-Inhalt ist die TLV-Metadatenstruktur aus `Tags.md` (ohne Footer `version+len+TAG!`).
|
||||||
|
|
||||||
|
## 5.9 `0x21` `SET_TAG_BLOB_START`
|
||||||
|
|
||||||
|
Startet eine Tag-Blob-Übertragung.
|
||||||
|
|
||||||
|
**Request-Payload:**
|
||||||
|
|
||||||
|
| Feld | Typ | Beschreibung |
|
||||||
|
|---|---|---|
|
||||||
|
| `path_len` | `uint8_t` | Länge von `path` |
|
||||||
|
| `path` | `byte[path_len]` | Pfad zur Datei |
|
||||||
|
| `total_len` | `uint16_t` | Gesamtlänge des folgenden Blobs |
|
||||||
|
|
||||||
|
**Response:** `RESP_ACK`
|
||||||
|
|
||||||
|
## 5.10 `0x22` `SET_TAG_BLOB_CHUNK`
|
||||||
|
|
||||||
|
Sendet einen weiteren Chunk des Blobs.
|
||||||
|
|
||||||
|
**Request-Payload:**
|
||||||
|
|
||||||
|
| Feld | Typ | Beschreibung |
|
||||||
|
|---|---|---|
|
||||||
|
| `data` | `byte[]` | Chunk-Daten |
|
||||||
|
|
||||||
|
**Response:** `RESP_ACK`
|
||||||
|
|
||||||
|
## 5.11 `0x23` `SET_TAG_BLOB_END`
|
||||||
|
|
||||||
|
Schließt den Upload ab und schreibt den Blob in die Datei (ersetzt bestehende Tags).
|
||||||
|
|
||||||
|
**Request-Payload:** leer
|
||||||
|
|
||||||
|
**Response:** `RESP_ACK`
|
||||||
|
|
||||||
|
Hinweis: Ein leerer Blob (`total_len=0`) ist erlaubt und entspricht "Tags leeren".
|
||||||
|
|
||||||
|
## 6) Große Daten (Dateien, Audio, Firmware)
|
||||||
|
|
||||||
|
Für mehrere MB nicht als einzelnes `RESP_DATA` senden, sondern streamen.
|
||||||
|
|
||||||
|
### 6.1 Warum nicht ein riesiges `payload_len`?
|
||||||
|
|
||||||
|
- Höheres Risiko bei Paketverlust/Timeout
|
||||||
|
- Mehr RAM-/Pufferdruck auf MCU
|
||||||
|
- Schlechtere Wiederanlaufstrategie bei Fehlern
|
||||||
|
|
||||||
|
### 6.2 Streaming-Ablauf
|
||||||
|
|
||||||
|
1. `RESP_STREAM_START` mit Metadaten
|
||||||
|
2. `RESP_STREAM_CHUNK` (mehrfach)
|
||||||
|
3. `RESP_STREAM_END` mit Gesamt-CRC/Abschlussstatus
|
||||||
|
|
||||||
|
### 6.3 Stream-Metadaten (`RESP_STREAM_START`)
|
||||||
|
|
||||||
|
| Feld | Typ | Beschreibung |
|
||||||
|
|---|---|---|
|
||||||
|
| `stream_id` | `uint16_t` | ID des Streams |
|
||||||
|
| `total_len` | `uint32_t` | Gesamtlänge in Bytes |
|
||||||
|
| `chunk_size` | `uint16_t` | Ziel-Chunkgröße |
|
||||||
|
|
||||||
|
### 6.4 Chunk-Payload (`RESP_STREAM_CHUNK`)
|
||||||
|
|
||||||
|
| Feld | Typ | Beschreibung |
|
||||||
|
|---|---|---|
|
||||||
|
| `stream_id` | `uint16_t` | Zuordnung zum Stream |
|
||||||
|
| `offset` | `uint32_t` | Byte-Offset im Gesamtstrom |
|
||||||
|
| `data_len` | `uint16_t` | Länge von `data` |
|
||||||
|
| `data` | `byte[data_len]` | Nutzdaten |
|
||||||
|
|
||||||
|
`total_len` ist `uint32_t`, damit sind bis 4 GiB abbildbar. Für euer Gerät ist das mehr als ausreichend und trotzdem standardisiert.
|
||||||
|
|
||||||
|
## 7) Offene Punkte für die Implementierung
|
||||||
|
|
||||||
|
- Fixe maximale `chunk_size` (z. B. 256/512/1024 Bytes)
|
||||||
|
- ACK/NACK auf Chunk-Ebene nötig oder „best effort + Retry auf Kommando-Ebene“?
|
||||||
|
- Timeout-/Retry-Policy pro Kommando
|
||||||
|
- Welche Kommandos zuerst in `protocol.c` migriert werden
|
||||||
|
|
||||||
|
## 8) Kurzfazit für den nächsten Schritt
|
||||||
|
|
||||||
|
- Ja, getrennte Kommandos sind sinnvoll.
|
||||||
|
- Ja, Endianness muss explizit definiert sein (Little Endian).
|
||||||
|
- Ja, `ACK` und `DATA` sollten als unterschiedliche Frame-Typen geführt werden.
|
||||||
|
- Für große Dateien: `uint32_t total_len` + Chunk-Streaming statt Einmal-Payload.
|
||||||
|
|
||||||
133
firmware/Tags.md
Normal file
133
firmware/Tags.md
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
# Audio-Tags Format
|
||||||
|
|
||||||
|
Dieses Dokument beschreibt das aktuelle Tag-Format für Audiodateien.
|
||||||
|
|
||||||
|
## 1) Position in der Datei
|
||||||
|
|
||||||
|
Die Tags stehen am Dateiende:
|
||||||
|
|
||||||
|
`[audio_data][metadata][tag_version_u8][footer_len_le16]["TAG!"]`
|
||||||
|
|
||||||
|
- `audio_data`: eigentliche Audiodaten
|
||||||
|
- `metadata`: Folge von Tag-Einträgen
|
||||||
|
- `tag_version_u8`: 1 Byte Versionsnummer des Tag-Formats
|
||||||
|
- `footer_len_le16`: 2 Byte, Little Endian
|
||||||
|
- `"TAG!"`: 4 Byte Magic (`0x54 0x41 0x47 0x21`)
|
||||||
|
|
||||||
|
## 2) Bedeutung von `footer_len_le16`
|
||||||
|
|
||||||
|
`footer_len_le16` ist die **Gesamtlänge des Footers**, also:
|
||||||
|
|
||||||
|
`footer_len = metadata_len + 1 + 2 + 4`
|
||||||
|
|
||||||
|
Damit beginnt `metadata` bei:
|
||||||
|
|
||||||
|
`metadata_start = file_size - footer_len`
|
||||||
|
|
||||||
|
Das passt zur aktuellen Implementierung in der Firmware.
|
||||||
|
|
||||||
|
### Tag-Version
|
||||||
|
|
||||||
|
- `tag_version` ist aktuell `0x01`.
|
||||||
|
- Der Host darf nur bekannte Versionen interpretieren.
|
||||||
|
- Bei unbekannter Version: Tag-Block ignorieren oder als "nicht unterstützt" melden.
|
||||||
|
|
||||||
|
## 3) Endianness und Typen
|
||||||
|
|
||||||
|
- Alle Multi-Byte-Werte sind **Little Endian**.
|
||||||
|
- Tag-Einträge sind TLV-basiert:
|
||||||
|
- `type`: `uint8_t`
|
||||||
|
- `len`: `uint16_t`
|
||||||
|
- `value`: `byte[len]`
|
||||||
|
|
||||||
|
Dadurch können auch unbekannte Typen sauber übersprungen werden.
|
||||||
|
|
||||||
|
## 4) Unterstützte Tag-Typen
|
||||||
|
|
||||||
|
Aktuell definierte Typen:
|
||||||
|
|
||||||
|
- `0x00`: `DESCRIPTION` (Beschreibung des Samples)
|
||||||
|
- `0x01`: `AUTHOR`
|
||||||
|
- `0x10`: `CRC32_RAW`
|
||||||
|
- `0x20`: `FILE_FORMAT` (Info für Host, Player wertet derzeit nicht aus)
|
||||||
|
|
||||||
|
## 5) Value-Format pro Tag
|
||||||
|
|
||||||
|
### 5.1 `0x00` DESCRIPTION
|
||||||
|
|
||||||
|
- `value`: UTF-8-Text
|
||||||
|
- `len`: Anzahl Bytes des UTF-8-Texts
|
||||||
|
|
||||||
|
### 5.2 `0x01` AUTHOR
|
||||||
|
|
||||||
|
- `value`: UTF-8-Text
|
||||||
|
- `len`: Anzahl Bytes des UTF-8-Texts
|
||||||
|
|
||||||
|
### 5.3 `0x10` CRC32_RAW
|
||||||
|
|
||||||
|
- `value`: `uint32_t crc32` (4 Byte, Little Endian)
|
||||||
|
- `len`: **muss 4** sein
|
||||||
|
|
||||||
|
### 5.4 `0x20` FILE_FORMAT
|
||||||
|
|
||||||
|
- `value`:
|
||||||
|
- `bits_per_sample`: `uint8_t`
|
||||||
|
- `sample_rate`: `uint32_t` (Little Endian)
|
||||||
|
- `len`: **muss 5** sein
|
||||||
|
|
||||||
|
Beispielwerte aktuell oft: `bits_per_sample = 16`, `sample_rate = 16000`.
|
||||||
|
|
||||||
|
## 6) Vorkommen je Typ
|
||||||
|
|
||||||
|
Aktueller Stand: **jeder Tag-Typ darf maximal 1x vorkommen**.
|
||||||
|
|
||||||
|
Empfohlene Host-Regel:
|
||||||
|
|
||||||
|
- Falls ein Typ mehrfach vorkommt, letzte Instanz gewinnt (`last-wins`) und ein Warnhinweis wird geloggt.
|
||||||
|
|
||||||
|
## 7) Validierungsregeln (Host)
|
||||||
|
|
||||||
|
Beim Lesen:
|
||||||
|
|
||||||
|
1. Prüfen, ob Datei mindestens 7 Byte hat.
|
||||||
|
2. Letzte 6 Byte prüfen: `footer_len_le16` + `TAG!`.
|
||||||
|
3. `footer_len` gegen Dateigröße validieren (`6 <= footer_len <= file_size`).
|
||||||
|
4. `tag_version` an Position `file_size - 6 - 1` lesen und validieren.
|
||||||
|
5. Im Metadatenbereich TLV-Einträge lesen, bis Ende erreicht.
|
||||||
|
6. Für bekannte Typen feste Längen prüfen (`CRC32_RAW=4`, `FILE_FORMAT=5`).
|
||||||
|
7. Unbekannte Typen über `len` überspringen.
|
||||||
|
|
||||||
|
Beim Schreiben:
|
||||||
|
|
||||||
|
1. Vorhandene Tags entfernen/ersetzen (audio-Ende bestimmen).
|
||||||
|
2. Neue TLV-Metadaten schreiben.
|
||||||
|
3. `tag_version_u8` schreiben (`0x01`).
|
||||||
|
4. `footer_len_le16` schreiben (inkl. 1+2+4).
|
||||||
|
5. `TAG!` schreiben.
|
||||||
|
5. Datei auf neue Länge truncaten.
|
||||||
|
|
||||||
|
## 8) Beispiel (hex)
|
||||||
|
|
||||||
|
Beispiel mit:
|
||||||
|
|
||||||
|
- DESCRIPTION = "Kick"
|
||||||
|
- AUTHOR = "Edi"
|
||||||
|
- CRC32_RAW = `0x12345678`
|
||||||
|
|
||||||
|
TLV-Daten:
|
||||||
|
|
||||||
|
- `00 04 00 4B 69 63 6B`
|
||||||
|
- `01 03 00 45 64 69`
|
||||||
|
- `10 04 00 78 56 34 12`
|
||||||
|
|
||||||
|
`metadata_len = 7 + 6 + 7 = 20 (0x0014)`
|
||||||
|
|
||||||
|
`footer_len = 20 + 1 + 2 + 4 = 27 (0x001B)`
|
||||||
|
|
||||||
|
Footer-Ende:
|
||||||
|
|
||||||
|
- `01 1B 00 54 41 47 21`
|
||||||
|
|
||||||
|
## 9) Hinweis zur aktuellen Firmware
|
||||||
|
|
||||||
|
Die Firmware verarbeitet Tag-Payload direkt binär (Chunk-Streaming über das Protokoll). Das dateiinterne Format entspricht direkt diesem Dokument.
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
VERSION_MAJOR = 0
|
VERSION_MAJOR = 0
|
||||||
VERSION_MINOR = 1
|
VERSION_MINOR = 2
|
||||||
PATCHLEVEL = 14
|
PATCHLEVEL = 19
|
||||||
VERSION_TWEAK = 0
|
VERSION_TWEAK = 0
|
||||||
|
#if (IS_ENABLED(CONFIG_LOG))
|
||||||
EXTRAVERSION = debug
|
EXTRAVERSION = debug
|
||||||
|
#else
|
||||||
|
EXTRAVERSION = 0
|
||||||
|
#endif
|
||||||
@@ -29,6 +29,7 @@ CONFIG_USB_DEVICE_PRODUCT="Edi's 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
|
||||||
|
CONFIG_USB_DEVICE_LOG_LEVEL_OFF=y
|
||||||
CONFIG_USB_DEVICE_INITIALIZE_AT_BOOT=n
|
CONFIG_USB_DEVICE_INITIALIZE_AT_BOOT=n
|
||||||
CONFIG_USB_DEVICE_STACK_NEXT=n
|
CONFIG_USB_DEVICE_STACK_NEXT=n
|
||||||
|
|
||||||
@@ -47,13 +48,14 @@ CONFIG_FLASH_MAP=y
|
|||||||
CONFIG_STREAM_FLASH=y
|
CONFIG_STREAM_FLASH=y
|
||||||
CONFIG_IMG_MANAGER=y
|
CONFIG_IMG_MANAGER=y
|
||||||
CONFIG_MCUBOOT_IMG_MANAGER=y
|
CONFIG_MCUBOOT_IMG_MANAGER=y
|
||||||
|
CONFIG_MCUBOOT_UTIL_LOG_LEVEL_ERR=y
|
||||||
|
|
||||||
# --- HWINFO und CRC ---
|
# --- HWINFO und CRC ---
|
||||||
CONFIG_HWINFO=y
|
CONFIG_HWINFO=y
|
||||||
CONFIG_CRC=y
|
CONFIG_CRC=y
|
||||||
|
|
||||||
# --- Debugging & Sicherheit ---
|
# --- Debugging & Sicherheit ---
|
||||||
CONFIG_ASSERT=y
|
CONFIG_ASSERT=n
|
||||||
CONFIG_HW_STACK_PROTECTION=y
|
CONFIG_HW_STACK_PROTECTION=y
|
||||||
CONFIG_RESET_ON_FATAL_ERROR=y
|
CONFIG_RESET_ON_FATAL_ERROR=y
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
#define AUDIO_WORD_WIDTH 16
|
#define AUDIO_WORD_WIDTH 16
|
||||||
#define AUDIO_SAMPLE_RATE 16000
|
#define AUDIO_SAMPLE_RATE 16000
|
||||||
|
|
||||||
LOG_MODULE_REGISTER(audio, LOG_LEVEL_DBG);
|
LOG_MODULE_REGISTER(audio, LOG_LEVEL_INF);
|
||||||
|
|
||||||
/* Dauer eines Blocks in ms (4096 Bytes / (16kHz * 2 Kanäle * 2 Bytes)) = 64 ms */
|
/* Dauer eines Blocks in ms (4096 Bytes / (16kHz * 2 Kanäle * 2 Bytes)) = 64 ms */
|
||||||
#define BLOCK_DURATION_MS ((AUDIO_BLOCK_SIZE * 1000) / (AUDIO_SAMPLE_RATE * 2 * (AUDIO_WORD_WIDTH / 8)))
|
#define BLOCK_DURATION_MS ((AUDIO_BLOCK_SIZE * 1000) / (AUDIO_SAMPLE_RATE * 2 * (AUDIO_WORD_WIDTH / 8)))
|
||||||
|
|||||||
@@ -5,9 +5,15 @@
|
|||||||
#include <zephyr/pm/device.h>
|
#include <zephyr/pm/device.h>
|
||||||
|
|
||||||
#include <fs.h>
|
#include <fs.h>
|
||||||
#include <utils.h>
|
|
||||||
|
|
||||||
LOG_MODULE_REGISTER(buzz_fs, LOG_LEVEL_DBG);
|
LOG_MODULE_REGISTER(buzz_fs, LOG_LEVEL_INF);
|
||||||
|
|
||||||
|
#define TAG_MAGIC "TAG!"
|
||||||
|
#define TAG_MAGIC_LEN 4U
|
||||||
|
#define TAG_LEN_FIELD_LEN 2U
|
||||||
|
#define TAG_VERSION_LEN 1U
|
||||||
|
#define TAG_FOOTER_V1_LEN (TAG_VERSION_LEN + TAG_LEN_FIELD_LEN + TAG_MAGIC_LEN)
|
||||||
|
#define TAG_FORMAT_VERSION 0x01
|
||||||
|
|
||||||
#define STORAGE_PARTITION_ID FIXED_PARTITION_ID(littlefs_storage)
|
#define STORAGE_PARTITION_ID FIXED_PARTITION_ID(littlefs_storage)
|
||||||
#define SLOT1_ID FIXED_PARTITION_ID(slot1_partition)
|
#define SLOT1_ID FIXED_PARTITION_ID(slot1_partition)
|
||||||
@@ -162,6 +168,15 @@ int fs_pm_statvfs(const char *path, struct fs_statvfs *stat)
|
|||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int fs_pm_stat(const char *path, struct fs_dirent *entry)
|
||||||
|
{
|
||||||
|
LOG_DBG("PM Getting stat for '%s'", path);
|
||||||
|
fs_pm_flash_resume();
|
||||||
|
int rc = fs_stat(path, entry);
|
||||||
|
fs_pm_flash_suspend();
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
int fs_pm_mkdir(const char *path)
|
int fs_pm_mkdir(const char *path)
|
||||||
{
|
{
|
||||||
LOG_DBG("PM Creating directory '%s'", path);
|
LOG_DBG("PM Creating directory '%s'", path);
|
||||||
@@ -171,28 +186,91 @@ int fs_pm_mkdir(const char *path)
|
|||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
ssize_t fs_get_audio_data_len(struct fs_file_t *fp) {
|
int fs_pm_rename(const char *old_path, const char *new_path)
|
||||||
|
{
|
||||||
|
LOG_DBG("PM Renaming '%s' to '%s'", old_path, new_path);
|
||||||
|
fs_pm_flash_resume();
|
||||||
|
int rc = fs_rename(old_path, new_path);
|
||||||
|
fs_pm_flash_suspend();
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int fs_get_tag_bounds(struct fs_file_t *fp, off_t file_size,
|
||||||
|
size_t *audio_limit, size_t *payload_len, bool *has_tag)
|
||||||
|
{
|
||||||
uint8_t footer[6];
|
uint8_t footer[6];
|
||||||
|
uint16_t tag_len;
|
||||||
|
|
||||||
|
if (audio_limit == NULL || payload_len == NULL || has_tag == NULL) {
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
*has_tag = false;
|
||||||
|
*audio_limit = (size_t)file_size;
|
||||||
|
*payload_len = 0U;
|
||||||
|
|
||||||
|
if (file_size < (off_t)TAG_FOOTER_V1_LEN) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs_seek(fp, -(off_t)(TAG_LEN_FIELD_LEN + TAG_MAGIC_LEN), FS_SEEK_END);
|
||||||
|
if (fs_read(fp, footer, sizeof(footer)) != sizeof(footer)) {
|
||||||
|
fs_seek(fp, 0, FS_SEEK_SET);
|
||||||
|
return -EIO;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (memcmp(&footer[2], TAG_MAGIC, TAG_MAGIC_LEN) != 0) {
|
||||||
|
fs_seek(fp, 0, FS_SEEK_SET);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
tag_len = (uint16_t)footer[0] | ((uint16_t)footer[1] << 8);
|
||||||
|
if (tag_len > (uint16_t)file_size || tag_len < TAG_FOOTER_V1_LEN) {
|
||||||
|
fs_seek(fp, 0, FS_SEEK_SET);
|
||||||
|
return -EBADMSG;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t tag_version = 0;
|
||||||
|
fs_seek(fp, -(off_t)TAG_FOOTER_V1_LEN, FS_SEEK_END);
|
||||||
|
if (fs_read(fp, &tag_version, 1) != 1) {
|
||||||
|
fs_seek(fp, 0, FS_SEEK_SET);
|
||||||
|
return -EIO;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tag_version != TAG_FORMAT_VERSION) {
|
||||||
|
fs_seek(fp, 0, FS_SEEK_SET);
|
||||||
|
return -ENOTSUP;
|
||||||
|
}
|
||||||
|
|
||||||
|
*has_tag = true;
|
||||||
|
*audio_limit = (size_t)file_size - tag_len;
|
||||||
|
*payload_len = tag_len - TAG_FOOTER_V1_LEN;
|
||||||
|
|
||||||
|
fs_seek(fp, 0, FS_SEEK_SET);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ssize_t fs_get_audio_data_len(struct fs_file_t *fp) {
|
||||||
off_t file_size;
|
off_t file_size;
|
||||||
|
size_t audio_limit = 0U;
|
||||||
|
size_t payload_len = 0U;
|
||||||
|
bool has_tag = false;
|
||||||
|
|
||||||
fs_seek(fp, 0, FS_SEEK_END);
|
fs_seek(fp, 0, FS_SEEK_END);
|
||||||
file_size = fs_tell(fp);
|
file_size = fs_tell(fp);
|
||||||
|
|
||||||
|
if (file_size < 0) {
|
||||||
fs_seek(fp, 0, FS_SEEK_SET);
|
fs_seek(fp, 0, FS_SEEK_SET);
|
||||||
if (file_size < 6) return file_size;
|
return -EIO;
|
||||||
|
}
|
||||||
|
|
||||||
fs_seek(fp, -6, FS_SEEK_END);
|
if (fs_get_tag_bounds(fp, file_size, &audio_limit, &payload_len, &has_tag) < 0) {
|
||||||
if (fs_read(fp, footer, 6) == 6) {
|
|
||||||
if (memcmp(&footer[2], "TAG!", 4) == 0) {
|
|
||||||
uint16_t tag_len = footer[0] | (footer[1] << 8);
|
|
||||||
if (tag_len <= file_size) {
|
|
||||||
fs_seek(fp, 0, FS_SEEK_SET);
|
fs_seek(fp, 0, FS_SEEK_SET);
|
||||||
return file_size - tag_len;
|
return -EIO;
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fs_seek(fp, 0, FS_SEEK_SET);
|
fs_seek(fp, 0, FS_SEEK_SET);
|
||||||
return file_size;
|
return has_tag ? (ssize_t)audio_limit : file_size;
|
||||||
}
|
}
|
||||||
|
|
||||||
ssize_t fs_read_audio(struct fs_file_t *fp, void *buffer, size_t len, size_t audio_limit) {
|
ssize_t fs_read_audio(struct fs_file_t *fp, void *buffer, size_t len, size_t audio_limit) {
|
||||||
@@ -206,90 +284,87 @@ ssize_t fs_read_audio(struct fs_file_t *fp, void *buffer, size_t len, size_t aud
|
|||||||
return fs_read(fp, buffer, to_read);
|
return fs_read(fp, buffer, to_read);
|
||||||
}
|
}
|
||||||
|
|
||||||
int fs_write_hex_tag(struct fs_file_t *fp, const char *hex_str) {
|
int fs_tag_open_read(struct fs_file_t *fp, uint8_t *version, size_t *payload_len)
|
||||||
size_t hex_len = strlen(hex_str);
|
{
|
||||||
|
if (fp == NULL || version == NULL || payload_len == NULL) {
|
||||||
// Ein Hex-String muss eine gerade Anzahl an Zeichen haben
|
|
||||||
if (hex_len % 2 != 0) return -EINVAL;
|
|
||||||
|
|
||||||
size_t payload_len = hex_len / 2;
|
|
||||||
uint16_t total_footer_len = (uint16_t)(payload_len + 6);
|
|
||||||
|
|
||||||
// 1. Audio-Ende bestimmen und dorthin seeken
|
|
||||||
size_t audio_limit = fs_get_audio_data_len(fp);
|
|
||||||
fs_seek(fp, audio_limit, FS_SEEK_SET);
|
|
||||||
|
|
||||||
// 2. Payload Byte für Byte konvertieren und schreiben
|
|
||||||
for (size_t i = 0; i < hex_len; i += 2) {
|
|
||||||
int high = hex2int(hex_str[i]);
|
|
||||||
int low = hex2int(hex_str[i+1]);
|
|
||||||
|
|
||||||
if (high < 0 || low < 0) return -EINVAL; // Ungültiges Hex-Zeichen
|
|
||||||
|
|
||||||
uint8_t byte = (uint8_t)((high << 4) | low);
|
|
||||||
fs_write(fp, &byte, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Die 2 Bytes Länge schreiben (Little Endian)
|
|
||||||
uint8_t len_bytes[2];
|
|
||||||
len_bytes[0] = (uint8_t)(total_footer_len & 0xFF);
|
|
||||||
len_bytes[1] = (uint8_t)((total_footer_len >> 8) & 0xFF);
|
|
||||||
fs_write(fp, len_bytes, 2);
|
|
||||||
|
|
||||||
// 4. Magic Bytes schreiben
|
|
||||||
fs_write(fp, "TAG!", 4);
|
|
||||||
|
|
||||||
// 5. Datei am aktuellen Punkt abschneiden
|
|
||||||
off_t current_pos = fs_tell(fp);
|
|
||||||
return fs_truncate(fp, current_pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
int fs_read_hex_tag(struct fs_file_t *fp, char *hex_str, size_t hex_str_size) {
|
|
||||||
if (hex_str == NULL || hex_str_size == 0) {
|
|
||||||
return -EINVAL;
|
return -EINVAL;
|
||||||
}
|
}
|
||||||
|
|
||||||
hex_str[0] = '\0';
|
|
||||||
|
|
||||||
// Dateigröße ermitteln
|
|
||||||
fs_seek(fp, 0, FS_SEEK_END);
|
fs_seek(fp, 0, FS_SEEK_END);
|
||||||
off_t file_size = fs_tell(fp);
|
off_t file_size = fs_tell(fp);
|
||||||
|
size_t audio_limit = 0U;
|
||||||
|
size_t payload_size = 0U;
|
||||||
|
bool has_tag = false;
|
||||||
|
|
||||||
// Audio-Limit finden (Anfang des Payloads)
|
if (file_size < 0) {
|
||||||
size_t audio_limit = fs_get_audio_data_len(fp);
|
|
||||||
|
|
||||||
// Prüfen, ob überhaupt ein Tag existiert (audio_limit < file_size)
|
|
||||||
if (audio_limit >= file_size) {
|
|
||||||
// Kein Tag vorhanden -> leerer String
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Die Payload-Länge ist: Gesamtgröße - Audio - 6 Bytes (Länge + Magic)
|
|
||||||
size_t payload_len = file_size - audio_limit - 6;
|
|
||||||
|
|
||||||
if ((payload_len * 2U) + 1U > hex_str_size) {
|
|
||||||
return -ENOMEM; // Nicht genug Platz im Zielpuffer
|
|
||||||
}
|
|
||||||
|
|
||||||
// Zum Anfang des Payloads springen
|
|
||||||
fs_seek(fp, audio_limit, FS_SEEK_SET);
|
|
||||||
|
|
||||||
uint8_t byte;
|
|
||||||
for (size_t i = 0; i < payload_len; i++) {
|
|
||||||
if (fs_read(fp, &byte, 1) != 1) {
|
|
||||||
return -EIO;
|
return -EIO;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Jedes Byte als zwei Hex-Zeichen in den Zielpuffer schreiben
|
int rc = fs_get_tag_bounds(fp, file_size, &audio_limit, &payload_size, &has_tag);
|
||||||
hex_str[i * 2] = int2hex(byte >> 4);
|
if (rc < 0) {
|
||||||
hex_str[i * 2 + 1] = int2hex(byte & 0x0F);
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
hex_str[payload_len * 2] = '\0';
|
if (!has_tag) {
|
||||||
|
return -ENOENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs_seek(fp, audio_limit, FS_SEEK_SET);
|
||||||
|
*version = TAG_FORMAT_VERSION;
|
||||||
|
*payload_len = payload_size;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ssize_t fs_tag_read_chunk(struct fs_file_t *fp, void *buffer, size_t len)
|
||||||
|
{
|
||||||
|
return fs_read(fp, buffer, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
int fs_tag_open_write(struct fs_file_t *fp)
|
||||||
|
{
|
||||||
|
ssize_t audio_limit = fs_get_audio_data_len(fp);
|
||||||
|
if (audio_limit < 0) {
|
||||||
|
return (int)audio_limit;
|
||||||
|
}
|
||||||
|
fs_seek(fp, audio_limit, FS_SEEK_SET);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ssize_t fs_tag_write_chunk(struct fs_file_t *fp, const void *buffer, size_t len)
|
||||||
|
{
|
||||||
|
return fs_write(fp, buffer, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
int fs_tag_finish_write(struct fs_file_t *fp, uint8_t version, size_t payload_len)
|
||||||
|
{
|
||||||
|
if (version != TAG_FORMAT_VERSION) {
|
||||||
|
return -ENOTSUP;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t total_footer_len = payload_len + TAG_FOOTER_V1_LEN;
|
||||||
|
if (total_footer_len > UINT16_MAX) {
|
||||||
|
return -EFBIG;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs_write(fp, &version, 1) != 1) {
|
||||||
|
return -EIO;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t len_bytes[2];
|
||||||
|
len_bytes[0] = (uint8_t)(total_footer_len & 0xFFU);
|
||||||
|
len_bytes[1] = (uint8_t)((total_footer_len >> 8) & 0xFFU);
|
||||||
|
if (fs_write(fp, len_bytes, sizeof(len_bytes)) != sizeof(len_bytes)) {
|
||||||
|
return -EIO;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs_write(fp, TAG_MAGIC, TAG_MAGIC_LEN) != TAG_MAGIC_LEN) {
|
||||||
|
return -EIO;
|
||||||
|
}
|
||||||
|
|
||||||
|
off_t current_pos = fs_tell(fp);
|
||||||
|
return fs_truncate(fp, current_pos);
|
||||||
|
}
|
||||||
|
|
||||||
int flash_get_slot_info(slot_info_t *info) {
|
int flash_get_slot_info(slot_info_t *info) {
|
||||||
if (slot1_info.size != 0) {
|
if (slot1_info.size != 0) {
|
||||||
*info = slot1_info;
|
*info = slot1_info;
|
||||||
|
|||||||
@@ -74,6 +74,15 @@ int fs_pm_unlink(const char *path);
|
|||||||
*/
|
*/
|
||||||
int fs_pm_statvfs(const char *path, struct fs_statvfs *stat);
|
int fs_pm_statvfs(const char *path, struct fs_statvfs *stat);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Wrapper around fs_stat that handles power management for the flash
|
||||||
|
* Resumes the flash before stat and suspends it afterwards
|
||||||
|
* @param path Path to file or directory
|
||||||
|
* @param entry Pointer to fs_dirent structure to receive metadata
|
||||||
|
* @return 0 on success, negative error code on failure
|
||||||
|
*/
|
||||||
|
int fs_pm_stat(const char *path, struct fs_dirent *entry);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Wrapper around fs_mkdir that handles power management for the flash
|
* @brief Wrapper around fs_mkdir that handles power management for the flash
|
||||||
* Resumes the flash before creating the directory and suspends it afterwards
|
* Resumes the flash before creating the directory and suspends it afterwards
|
||||||
@@ -82,6 +91,15 @@ int fs_pm_statvfs(const char *path, struct fs_statvfs *stat);
|
|||||||
*/
|
*/
|
||||||
int fs_pm_mkdir(const char *path);
|
int fs_pm_mkdir(const char *path);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Wrapper around fs_rename that handles power management for the flash
|
||||||
|
* Resumes the flash before renaming and suspends it afterwards
|
||||||
|
* @param old_path Current path of the file or directory
|
||||||
|
* @param new_path New path for the file or directory
|
||||||
|
* @return 0 on success, negative error code on failure
|
||||||
|
*/
|
||||||
|
int fs_pm_rename(const char *old_path, const char *new_path);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Gets the length of the audio data in a file, accounting for any metadata tags
|
* @brief Gets the length of the audio data in a file, accounting for any metadata tags
|
||||||
* @param fp Pointer to an open fs_file_t structure representing the audio file
|
* @param fp Pointer to an open fs_file_t structure representing the audio file
|
||||||
@@ -100,21 +118,47 @@ int fs_get_audio_data_len(struct fs_file_t *fp);
|
|||||||
int fs_read_audio(struct fs_file_t *fp, void *buffer, size_t len, size_t audio_limit);
|
int fs_read_audio(struct fs_file_t *fp, void *buffer, size_t len, size_t audio_limit);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Writes a hexadecimal string as a metadata tag at the end of an audio file
|
* @brief Positions file pointer at start of tag payload if tags exist.
|
||||||
* @param fp Pointer to an open fs_file_t structure representing the audio file
|
* @param fp Pointer to an open fs_file_t structure representing the audio file
|
||||||
* @param hex_str Null-terminated string containing hexadecimal characters (0-9, a-f, A-F)
|
* @param version Pointer to receive tag format version
|
||||||
* @return 0 on success, negative error code on failure
|
* @param payload_len Pointer to receive tag payload length in bytes
|
||||||
|
* @return 0 on success, -ENOENT if no tags exist, negative error code on failure
|
||||||
*/
|
*/
|
||||||
int fs_write_hex_tag(struct fs_file_t *fp, const char *hex_str);
|
int fs_tag_open_read(struct fs_file_t *fp, uint8_t *version, size_t *payload_len);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Reads a hexadecimal string from a metadata tag at the end of an audio file
|
* @brief Reads a chunk from current tag payload position.
|
||||||
|
* @param fp Pointer to an open fs_file_t positioned in tag payload
|
||||||
|
* @param buffer Destination buffer
|
||||||
|
* @param len Maximum bytes to read
|
||||||
|
* @return Number of bytes read, 0 at payload end, negative error code on failure
|
||||||
|
*/
|
||||||
|
ssize_t fs_tag_read_chunk(struct fs_file_t *fp, void *buffer, size_t len);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Positions file pointer for tag payload overwrite at end of audio data.
|
||||||
* @param fp Pointer to an open fs_file_t structure representing the audio file
|
* @param fp Pointer to an open fs_file_t structure representing the audio file
|
||||||
* @param hex_str Buffer to be filled with the hexadecimal string (must be large enough to hold the data)
|
|
||||||
* @param hex_str_size Size of the hex_str buffer
|
|
||||||
* @return 0 on success, negative error code on failure
|
* @return 0 on success, negative error code on failure
|
||||||
*/
|
*/
|
||||||
int fs_read_hex_tag(struct fs_file_t *fp, char *hex_str, size_t hex_str_size);
|
int fs_tag_open_write(struct fs_file_t *fp);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Writes a raw tag payload chunk.
|
||||||
|
* @param fp Pointer to an open fs_file_t positioned for tag payload write
|
||||||
|
* @param buffer Source buffer
|
||||||
|
* @param len Number of bytes to write
|
||||||
|
* @return Number of bytes written, negative error code on failure
|
||||||
|
*/
|
||||||
|
ssize_t fs_tag_write_chunk(struct fs_file_t *fp, const void *buffer, size_t len);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Finalizes tags by appending version + footer and truncating file.
|
||||||
|
* @param fp Pointer to an open fs_file_t structure representing the audio file
|
||||||
|
* @param version Tag format version to write
|
||||||
|
* @param payload_len Tag payload length in bytes
|
||||||
|
* @return 0 on success, negative error code on failure
|
||||||
|
*/
|
||||||
|
int fs_tag_finish_write(struct fs_file_t *fp, uint8_t version, size_t payload_len);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Retrieves information about the firmware slot, such as start address and size
|
* @brief Retrieves information about the firmware slot, such as start address and size
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,29 +1,53 @@
|
|||||||
#ifndef PROTOCOL_H
|
#ifndef PROTOCOL_H
|
||||||
#define PROTOCOL_H
|
#define PROTOCOL_H
|
||||||
|
|
||||||
|
#include <zephyr/kernel.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#define PROTOCOL_MAX_PATH_LEN 32U
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
PS_WAITING_FOR_COMMAND,
|
PS_WAIT_SYNC = 0,
|
||||||
PS_READING_COMMAND,
|
PS_READ_HEADER,
|
||||||
PS_READING_PARAMETERS,
|
PS_READ_PAYLOAD,
|
||||||
PS_WAITING_FOR_END_OF_LINE,
|
PS_READ_PAYLOAD_CRC,
|
||||||
} protocol_state_t;
|
} protocol_state_t;
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
CMD_INVALID = 0,
|
CMD_INVALID = 0,
|
||||||
CMD_INFO,
|
CMD_GET_PROTOCOL_VERSION = 0x00,
|
||||||
CMD_LS,
|
CMD_GET_FIRMWARE_STATUS = 0x01,
|
||||||
CMD_PUT_BINARY_FILE,
|
CMD_GET_FLASH_STATUS = 0x02,
|
||||||
CMD_MKDIR,
|
CMD_CONFIRM_FIRMWARE = 0x03,
|
||||||
CMD_RM,
|
CMD_REBOOT = 0x04,
|
||||||
CMD_CONFIRM,
|
CMD_LIST_DIR = 0x10,
|
||||||
CMD_REBOOT,
|
CMD_CHECK_FILE_CRC = 0x11,
|
||||||
CMD_PLAY,
|
CMD_MKDIR = 0x12,
|
||||||
CMD_SET_TAG,
|
CMD_RM = 0x13,
|
||||||
CMD_GET_TAG,
|
CMD_PUT_FILE_START = 0x14,
|
||||||
CMD_CHECK,
|
CMD_PUT_FILE_CHUNK = 0x15,
|
||||||
/* Weitere Kommandos folgen hier */
|
CMD_PUT_FILE_END = 0x16,
|
||||||
|
CMD_PUT_FW_START = 0x17,
|
||||||
|
CMD_STAT = 0x18,
|
||||||
|
CMD_RENAME = 0x19,
|
||||||
|
CMD_RM_R = 0x1A,
|
||||||
|
CMD_GET_FILE = 0x1B,
|
||||||
|
CMD_GET_TAG_BLOB = 0x20,
|
||||||
|
CMD_SET_TAG_BLOB_START = 0x21,
|
||||||
|
CMD_SET_TAG_BLOB_CHUNK = 0x22,
|
||||||
|
CMD_SET_TAG_BLOB_END = 0x23,
|
||||||
} protocol_cmd_t;
|
} protocol_cmd_t;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
FRAME_REQ = 0x01,
|
||||||
|
FRAME_RESP_ACK = 0x10,
|
||||||
|
FRAME_RESP_DATA = 0x11,
|
||||||
|
FRAME_RESP_STREAM_START = 0x12,
|
||||||
|
FRAME_RESP_STREAM_CHUNK = 0x13,
|
||||||
|
FRAME_RESP_STREAM_END = 0x14,
|
||||||
|
FRAME_RESP_ERROR = 0x7F,
|
||||||
|
} protocol_frame_type_t;
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
P_ERR_NONE = 0x00,
|
P_ERR_NONE = 0x00,
|
||||||
P_ERR_INVALID_COMMAND = 0x01,
|
P_ERR_INVALID_COMMAND = 0x01,
|
||||||
@@ -47,4 +71,7 @@ typedef enum {
|
|||||||
P_ERR_BUSY = 0x31,
|
P_ERR_BUSY = 0x31,
|
||||||
P_ERR_INTERNAL = 0x32,
|
P_ERR_INTERNAL = 0x32,
|
||||||
} protocol_error_t;
|
} protocol_error_t;
|
||||||
|
|
||||||
|
void protocol_thread_entry(void *p1, void *p2, void *p3);
|
||||||
|
|
||||||
#endif // PROTOCOL_H
|
#endif // PROTOCOL_H
|
||||||
@@ -51,15 +51,3 @@ uint8_t get_reboot_status(void)
|
|||||||
}
|
}
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
int hex2int(char c) {
|
|
||||||
if (c >= '0' && c <= '9') return c - '0';
|
|
||||||
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
|
||||||
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
|
||||||
return -1; // Fehlerhaftes Zeichen
|
|
||||||
}
|
|
||||||
|
|
||||||
char int2hex(uint8_t i) {
|
|
||||||
if (i < 10) return '0' + i;
|
|
||||||
return 'a' + (i - 10);
|
|
||||||
}
|
|
||||||
@@ -22,18 +22,4 @@ void reboot_with_status(uint8_t status_code);
|
|||||||
*/
|
*/
|
||||||
uint8_t get_reboot_status();
|
uint8_t get_reboot_status();
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Converts a hexadecimal character to its integer value.
|
|
||||||
* @param c The hexadecimal character (0-9, a-f, A-F) to convert.
|
|
||||||
* @return The integer value of the hexadecimal character, or -1 if the character is not a valid hexadecimal digit.
|
|
||||||
*/
|
|
||||||
int hex2int(char c);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Converts an integer value to its hexadecimal character representation.
|
|
||||||
* @param i The integer value to convert (0-15).
|
|
||||||
* @return The hexadecimal character representation of the integer value.
|
|
||||||
*/
|
|
||||||
char int2hex(uint8_t i);
|
|
||||||
|
|
||||||
#endif // UTILS_H
|
#endif // UTILS_H
|
||||||
|
|||||||
Reference in New Issue
Block a user