Compare commits
3 Commits
e7d6d288a8
...
binproto
| Author | SHA1 | Date | |
|---|---|---|---|
| f85143d7e5 | |||
| 4f3fbff258 | |||
| b665cb5def |
@@ -1,135 +0,0 @@
|
|||||||
# buzzer.py
|
|
||||||
import argparse
|
|
||||||
import sys
|
|
||||||
from core.config import load_config
|
|
||||||
from core.connection import BuzzerConnection, BuzzerError
|
|
||||||
from core.commands import info, ls, put, mkdir, rm, confirm, reboot, play, check, get_tag
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = argparse.ArgumentParser(description="Edis Buzzer Host Tool")
|
|
||||||
|
|
||||||
# Globale Argumente (gelten für alle Befehle)
|
|
||||||
parser.add_argument("-p", "--port", type=str, help="Serielle Schnittstelle (z.B. COM15)")
|
|
||||||
parser.add_argument("-b", "--baudrate", type=int, help="Verbindungsgeschwindigkeit")
|
|
||||||
parser.add_argument("-t", "--timeout", type=float, help="Timeout in Sekunden (Standard: 5.0)")
|
|
||||||
|
|
||||||
# Subkommandos einrichten
|
|
||||||
subparsers = parser.add_subparsers(dest="command", help="Verfügbare Befehle")
|
|
||||||
|
|
||||||
# Befehl: info (expliziter Aufruf, obwohl es ohnehin immer angezeigt wird)
|
|
||||||
subparsers.add_parser("info", help="Zeigt nur die Systeminformationen an")
|
|
||||||
|
|
||||||
# Befehl: ls
|
|
||||||
ls_parser = subparsers.add_parser("ls", help="Listet Dateien und Verzeichnisse auf")
|
|
||||||
ls_parser.add_argument("path", nargs="?", default="/", help="Zielpfad (Standard: /)")
|
|
||||||
ls_parser.add_argument("-r", "--recursive", action="store_true", help="Rekursiv auflisten")
|
|
||||||
|
|
||||||
# Befehl: put
|
|
||||||
put_parser = subparsers.add_parser("put", help="Lädt eine oder mehrere Dateien auf den Controller hoch")
|
|
||||||
put_parser.add_argument("sources", nargs="+", help="Lokale Quelldatei(en) oder Wildcards (z.B. *.raw)")
|
|
||||||
put_parser.add_argument("target", type=str, help="Zielpfad auf dem Controller (Verzeichnis muss mit '/' enden)")
|
|
||||||
|
|
||||||
# Befehl: mkdir
|
|
||||||
mkdir_parser = subparsers.add_parser("mkdir", help="Erstellt ein neues Verzeichnis")
|
|
||||||
mkdir_parser.add_argument("path", type=str, help="Pfad des neuen Verzeichnisses (z.B. /lfs/a/neu)")
|
|
||||||
|
|
||||||
# Befehl: rm
|
|
||||||
rm_parser = subparsers.add_parser("rm", help="Löscht eine Datei oder ein Verzeichnis")
|
|
||||||
rm_parser.add_argument("path", type=str, help="Pfad der zu löschenden Datei/Ordner")
|
|
||||||
rm_parser.add_argument("-r", "--recursive", action="store_true", help="Ordnerinhalte rekursiv löschen")
|
|
||||||
|
|
||||||
# Befehl: play
|
|
||||||
play_parser = subparsers.add_parser("play", help="Spielt eine Datei auf dem Controller ab")
|
|
||||||
play_parser.add_argument("path", type=str, help="Pfad der abzuspielenden Datei (z.B. /lfs/a/neu)")
|
|
||||||
|
|
||||||
# Befehl: check
|
|
||||||
check_parser = subparsers.add_parser("check", help="Holt die CRC32 einer Datei und zeigt sie an")
|
|
||||||
check_parser.add_argument("path", type=str, help="Pfad der zu prüfenden Datei (z.B. /lfs/a/neu)")
|
|
||||||
|
|
||||||
# Befehl: confirm
|
|
||||||
confirm_parser = subparsers.add_parser("confirm", help="Bestätigt die aktuell laufende Firmware")
|
|
||||||
|
|
||||||
# Befehl: reboot
|
|
||||||
reboot_parser = subparsers.add_parser("reboot", help="Startet den Buzzer neu")
|
|
||||||
|
|
||||||
# Befehl: get_tag
|
|
||||||
get_tag_parser = subparsers.add_parser("get_tag", help="Holt die Tags einer Datei")
|
|
||||||
get_tag_parser.add_argument("path", type=str, help="Pfad der Datei (z.B. /lfs/a/neu)")
|
|
||||||
|
|
||||||
# Argumente parsen
|
|
||||||
args = parser.parse_args()
|
|
||||||
config = load_config(args)
|
|
||||||
|
|
||||||
print("--- Aktuelle Verbindungsparameter ---------------------")
|
|
||||||
print(f"Port: {config.get('port', 'Nicht definiert')}")
|
|
||||||
print(f"Baudrate: {config.get('baudrate')}")
|
|
||||||
print(f"Timeout: {config.get('timeout')}s")
|
|
||||||
print("-" * 55)
|
|
||||||
|
|
||||||
if not config.get("port"):
|
|
||||||
print("Abbruch: Es muss ein Port in der config.yaml oder via --port definiert werden.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
try:
|
|
||||||
with BuzzerConnection(config) as conn:
|
|
||||||
# 1. Immer die Info holen und anzeigen
|
|
||||||
sys_info = info.execute(conn)
|
|
||||||
|
|
||||||
# Neu: Status auslesen und Farbe zuweisen (Grün für CONFIRMED, Gelb für UNCONFIRMED)
|
|
||||||
status = sys_info.get("image_status", "UNKNOWN")
|
|
||||||
status_color = "\033[32m" if status == "CONFIRMED" else "\033[33m"
|
|
||||||
|
|
||||||
# Neu: Die print-Anweisung enthält nun den formatierten Status
|
|
||||||
print(f"Buzzer Firmware: v{sys_info['app_version']} [{status_color}{status}\033[0m] (Protokoll v{sys_info['protocol_version']})")
|
|
||||||
print(f"LittleFS Status: {sys_info['used_kb']:.1f} KB / {sys_info['total_kb']:.1f} KB belegt ({sys_info['percent_used']:.1f}%)")
|
|
||||||
print("-" * 55)
|
|
||||||
# 2. Spezifisches Kommando ausführen
|
|
||||||
if args.command == "ls":
|
|
||||||
print(f"Inhalt von '{args.path}':\n")
|
|
||||||
tree = ls.get_file_tree(conn, target_path=args.path, recursive=args.recursive)
|
|
||||||
if not tree:
|
|
||||||
print(" (Leer)")
|
|
||||||
else:
|
|
||||||
ls.print_tree(tree, path=args.path )
|
|
||||||
elif args.command == "put":
|
|
||||||
put.execute(conn, sources=args.sources, target=args.target)
|
|
||||||
elif args.command == "mkdir":
|
|
||||||
mkdir.execute(conn, path=args.path)
|
|
||||||
elif args.command == "rm":
|
|
||||||
rm.execute(conn, path=args.path, recursive=args.recursive)
|
|
||||||
elif args.command == "confirm":
|
|
||||||
confirm.execute(conn)
|
|
||||||
elif args.command == "reboot":
|
|
||||||
reboot.execute(conn)
|
|
||||||
elif args.command == "play":
|
|
||||||
play.execute(conn, path=args.path)
|
|
||||||
elif args.command == "check":
|
|
||||||
CRC32 = check.execute(conn, path=args.path)
|
|
||||||
if CRC32:
|
|
||||||
print(f"CRC32 von '{args.path}': 0x{CRC32['crc32']:08x}")
|
|
||||||
else:
|
|
||||||
print(f"Fehler: Keine CRC32-Information für '{args.path}' erhalten.")
|
|
||||||
elif args.command == "get_tag":
|
|
||||||
tags = get_tag.execute(conn, path=args.path)
|
|
||||||
if tags:
|
|
||||||
print(f"Tags von '{args.path}':")
|
|
||||||
for key, value in tags.items():
|
|
||||||
print(f" {key}: {value}")
|
|
||||||
else:
|
|
||||||
print(f"Fehler: Keine Tags für '{args.path}' erhalten.")
|
|
||||||
elif args.command == "info" or args.command is None:
|
|
||||||
# Wurde kein Befehl oder explizit 'info' angegeben, sind wir hier schon fertig
|
|
||||||
pass
|
|
||||||
|
|
||||||
except TimeoutError as e:
|
|
||||||
print(f"Fehler: {e}")
|
|
||||||
sys.exit(1)
|
|
||||||
except BuzzerError as e:
|
|
||||||
print(f"Buzzer hat die Aktion abgelehnt: {e}")
|
|
||||||
sys.exit(1)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Verbindungsfehler auf {config.get('port')}: {e}")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# config.yaml
|
|
||||||
serial:
|
|
||||||
port: "COM17"
|
|
||||||
baudrate: 250000
|
|
||||||
timeout: 10
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# config.yaml
|
|
||||||
serial:
|
|
||||||
port: "COM17"
|
|
||||||
baudrate: 250000
|
|
||||||
timeout: 20
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# core/commands/check.py
|
|
||||||
from core.connection import BuzzerError
|
|
||||||
|
|
||||||
def execute(conn, path: str) -> dict:
|
|
||||||
"""Holt die CRC32 einer datei und gibt sie als Int zurück."""
|
|
||||||
lines = conn.send_command("check " + path)
|
|
||||||
if not lines:
|
|
||||||
raise BuzzerError("Keine Antwort auf 'check' empfangen.")
|
|
||||||
|
|
||||||
parts = lines[0].split()
|
|
||||||
if len(parts) != 1:
|
|
||||||
raise BuzzerError(f"Unerwartetes Check-Format: {lines[0]}")
|
|
||||||
|
|
||||||
crc32 = int(parts[0], 16)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"crc32": crc32
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
# core/commands/mkdir.py
|
|
||||||
from core.connection import BuzzerError
|
|
||||||
|
|
||||||
def execute(conn):
|
|
||||||
"""Confirmt die aktuelle Firmware."""
|
|
||||||
try:
|
|
||||||
conn.send_command(f"confirm")
|
|
||||||
print(f"✅ Firmware erfolgreich bestätigt.")
|
|
||||||
except BuzzerError as e:
|
|
||||||
print(f"❌ Fehler beim Bestätigen der Firmware: {e}")
|
|
||||||
@@ -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
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
# core/commands/info.py
|
|
||||||
from core.connection import BuzzerError
|
|
||||||
|
|
||||||
def execute(conn) -> dict:
|
|
||||||
"""Holt die Systeminformationen und gibt sie als strukturiertes Dictionary zurück."""
|
|
||||||
lines = conn.send_command("info")
|
|
||||||
if not lines:
|
|
||||||
raise BuzzerError("Keine Antwort auf 'info' empfangen.")
|
|
||||||
|
|
||||||
parts = lines[0].split(';')
|
|
||||||
# Auf 6 Parameter aktualisiert
|
|
||||||
if len(parts) != 6:
|
|
||||||
raise BuzzerError(f"Unerwartetes Info-Format: {lines[0]}")
|
|
||||||
|
|
||||||
protocol_version = int(parts[0])
|
|
||||||
if protocol_version != 2:
|
|
||||||
raise BuzzerError(f"Inkompatibles Protokoll: Gerät nutzt v{protocol_version}, Host erwartet v2.")
|
|
||||||
|
|
||||||
app_version = parts[1]
|
|
||||||
f_frsize = int(parts[2])
|
|
||||||
f_blocks = int(parts[3])
|
|
||||||
f_bfree = int(parts[4])
|
|
||||||
image_status = parts[5].strip() # CONFIRMED oder UNCONFIRMED
|
|
||||||
|
|
||||||
total_kb = (f_blocks * f_frsize) / 1024
|
|
||||||
free_kb = (f_bfree * f_frsize) / 1024
|
|
||||||
used_kb = total_kb - free_kb
|
|
||||||
percent_used = (used_kb / total_kb) * 100 if total_kb > 0 else 0
|
|
||||||
|
|
||||||
return {
|
|
||||||
"protocol_version": protocol_version,
|
|
||||||
"app_version": app_version,
|
|
||||||
"total_kb": total_kb,
|
|
||||||
"free_kb": free_kb,
|
|
||||||
"used_kb": used_kb,
|
|
||||||
"percent_used": percent_used,
|
|
||||||
"image_status": image_status
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
# core/commands/ls.py
|
|
||||||
from core.connection import BuzzerError
|
|
||||||
|
|
||||||
def get_file_tree(conn, target_path="/", recursive=False) -> list:
|
|
||||||
"""
|
|
||||||
Liest das Dateisystem aus und gibt eine hierarchische Baumstruktur zurück.
|
|
||||||
"""
|
|
||||||
if not target_path.endswith('/'):
|
|
||||||
target_path += '/'
|
|
||||||
|
|
||||||
cmd_path = target_path.rstrip('/') if target_path != '/' else '/'
|
|
||||||
|
|
||||||
try:
|
|
||||||
lines = conn.send_command(f"ls {cmd_path}")
|
|
||||||
except BuzzerError as e:
|
|
||||||
return [{"type": "E", "name": f"Fehler beim Lesen: {e}", "path": target_path}]
|
|
||||||
|
|
||||||
nodes = []
|
|
||||||
if not lines:
|
|
||||||
return nodes
|
|
||||||
|
|
||||||
for line in lines:
|
|
||||||
parts = line.split(',', 2)
|
|
||||||
if len(parts) != 3:
|
|
||||||
continue
|
|
||||||
|
|
||||||
entry_type, entry_size, entry_name = parts
|
|
||||||
node = {
|
|
||||||
"type": entry_type,
|
|
||||||
"name": entry_name,
|
|
||||||
"path": f"{target_path}{entry_name}"
|
|
||||||
}
|
|
||||||
|
|
||||||
if entry_type == 'D':
|
|
||||||
if recursive:
|
|
||||||
# Rekursiver Aufruf auf dem Host für Unterverzeichnisse
|
|
||||||
node["children"] = get_file_tree(conn, f"{target_path}{entry_name}/", recursive=True)
|
|
||||||
else:
|
|
||||||
node["children"] = []
|
|
||||||
elif entry_type == 'F':
|
|
||||||
node["size"] = int(entry_size)
|
|
||||||
|
|
||||||
nodes.append(node)
|
|
||||||
|
|
||||||
return nodes
|
|
||||||
|
|
||||||
def print_tree(nodes, prefix="", path=""):
|
|
||||||
"""
|
|
||||||
Gibt die Baumstruktur optisch formatiert auf der Konsole aus.
|
|
||||||
"""
|
|
||||||
if path:
|
|
||||||
if path == "/":
|
|
||||||
display_path = "💾 "+"/ (Root)"
|
|
||||||
else:
|
|
||||||
display_path = "📁 " + path
|
|
||||||
print(f"{prefix}{display_path}")
|
|
||||||
for i, node in enumerate(nodes):
|
|
||||||
is_last = (i == len(nodes) - 1)
|
|
||||||
connector = " └─" if is_last else " ├─"
|
|
||||||
|
|
||||||
if node["type"] == 'D':
|
|
||||||
print(f"{prefix}{connector}📁 {node['name']}")
|
|
||||||
extension = " " if is_last else " │ "
|
|
||||||
if "children" in node and node["children"]:
|
|
||||||
print_tree(node["children"], prefix + extension)
|
|
||||||
elif node["type"] == 'F':
|
|
||||||
size_kb = node["size"] / 1024
|
|
||||||
# \033[90m macht den Text dunkelgrau, \033[0m setzt die Farbe zurück
|
|
||||||
print(f"{prefix}{connector}📄 {node['name']} \033[90m({size_kb:.1f} KB)\033[0m")
|
|
||||||
elif node["type"] == 'E':
|
|
||||||
print(f"{prefix}{connector}❌ \033[31m{node['name']}\033[0m")
|
|
||||||
|
|
||||||
def get_flat_file_list(nodes) -> list:
|
|
||||||
"""
|
|
||||||
Wandelt die Baumstruktur in eine flache Liste von Dateipfaden um.
|
|
||||||
Wird von 'rm -r' benötigt, um nacheinander alle Dateien zu löschen.
|
|
||||||
"""
|
|
||||||
flat_list = []
|
|
||||||
for node in nodes:
|
|
||||||
if node["type"] == 'F':
|
|
||||||
flat_list.append(node)
|
|
||||||
elif node["type"] == 'D' and "children" in node:
|
|
||||||
flat_list.extend(get_flat_file_list(node["children"]))
|
|
||||||
return flat_list
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
# core/commands/mkdir.py
|
|
||||||
from core.connection import BuzzerError
|
|
||||||
|
|
||||||
def execute(conn, path: str):
|
|
||||||
"""Erstellt ein Verzeichnis auf dem Controller."""
|
|
||||||
try:
|
|
||||||
conn.send_command(f"mkdir {path}")
|
|
||||||
print(f"📁 Verzeichnis '{path}' erfolgreich erstellt.")
|
|
||||||
except BuzzerError as e:
|
|
||||||
print(f"❌ Fehler beim Erstellen von '{path}': {e}")
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
# core/commands/mkdir.py
|
|
||||||
from core.connection import BuzzerError
|
|
||||||
|
|
||||||
def execute(conn, path: str):
|
|
||||||
"""Spielt eine Datei auf dem Controller ab."""
|
|
||||||
try:
|
|
||||||
conn.send_command(f"play {path}")
|
|
||||||
print(f"▶️ Datei '{path}' wird abgespielt.")
|
|
||||||
except BuzzerError as e:
|
|
||||||
print(f"❌ Fehler beim Abspielen von '{path}': {e}")
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
import os
|
|
||||||
import zlib
|
|
||||||
import glob
|
|
||||||
import time
|
|
||||||
import sys
|
|
||||||
from core.connection import BuzzerError
|
|
||||||
|
|
||||||
def get_file_crc32(filepath: str) -> int:
|
|
||||||
"""Berechnet die IEEE CRC32-Prüfsumme einer Datei in Chunks."""
|
|
||||||
crc = 0
|
|
||||||
with open(filepath, 'rb') as f:
|
|
||||||
while chunk := f.read(4096):
|
|
||||||
crc = zlib.crc32(chunk, crc)
|
|
||||||
return crc & 0xFFFFFFFF
|
|
||||||
|
|
||||||
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:
|
|
||||||
print("Keine gültigen Dateien gefunden.")
|
|
||||||
return
|
|
||||||
|
|
||||||
total_size_all = sum(os.path.getsize(f) for f in resolved_files)
|
|
||||||
sent_all = 0
|
|
||||||
start_time_all = time.monotonic()
|
|
||||||
is_target_dir = target.endswith('/')
|
|
||||||
|
|
||||||
for filepath in resolved_files:
|
|
||||||
filename = os.path.basename(filepath)
|
|
||||||
filesize = os.path.getsize(filepath)
|
|
||||||
crc32 = get_file_crc32(filepath)
|
|
||||||
dest_path = f"{target}{filename}" if is_target_dir else target
|
|
||||||
|
|
||||||
print(f"Sende 📄 {filename} ({filesize/1024:.1f} KB) -> {dest_path}")
|
|
||||||
|
|
||||||
start_time_file = time.monotonic()
|
|
||||||
sent_file = 0
|
|
||||||
|
|
||||||
def progress_handler(chunk_len):
|
|
||||||
nonlocal sent_file, sent_all
|
|
||||||
sent_file += chunk_len
|
|
||||||
sent_all += chunk_len
|
|
||||||
|
|
||||||
elapsed = time.monotonic() - start_time_file
|
|
||||||
speed = (sent_file / 1024) / elapsed if elapsed > 0 else 0
|
|
||||||
|
|
||||||
# Prozentberechnungen
|
|
||||||
perc_file = (sent_file / filesize) * 100
|
|
||||||
perc_all = (sent_all / total_size_all) * 100
|
|
||||||
|
|
||||||
# ETA (Basierend auf Gesamtgeschwindigkeit)
|
|
||||||
elapsed_all = time.monotonic() - start_time_all
|
|
||||||
avg_speed_all = sent_all / elapsed_all if elapsed_all > 0 else 0
|
|
||||||
eta_sec = (total_size_all - sent_all) / avg_speed_all if avg_speed_all > 0 else 0
|
|
||||||
eta_str = f"{int(eta_sec // 60):02d}:{int(eta_sec % 60):02d}"
|
|
||||||
|
|
||||||
# Ausgabezeile (\r überschreibt die aktuelle Zeile)
|
|
||||||
sys.stdout.write(
|
|
||||||
f"\r \033[90mProg: {perc_file:3.0f}% | Gesamt: {perc_all:3.0f}% | "
|
|
||||||
f"{speed:6.1f} KB/s | ETA: {eta_str}\033[0m"
|
|
||||||
)
|
|
||||||
sys.stdout.flush()
|
|
||||||
|
|
||||||
try:
|
|
||||||
cmd = f"put {dest_path};{filesize};{crc32}\n"
|
|
||||||
conn.serial.write(cmd.encode('utf-8'))
|
|
||||||
conn.serial.flush()
|
|
||||||
|
|
||||||
# Binärtransfer mit unserem Handler
|
|
||||||
conn.send_binary(filepath, progress_callback=progress_handler)
|
|
||||||
|
|
||||||
# Zeile nach Erfolg abschließen
|
|
||||||
print(f"\r \033[32mFertig: {filename} übertragen. \033[0m")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"\n ❌ \033[31mFehler: {e}\033[0m")
|
|
||||||
|
|
||||||
total_duration = time.monotonic() - start_time_all
|
|
||||||
total_kb = total_size_all / 1024
|
|
||||||
avg_speed = total_kb / total_duration if total_duration > 0 else 0
|
|
||||||
|
|
||||||
print(f"\nÜbertragung abgeschlossen: {total_kb:.1f} KB in {total_duration:.1f}s ({avg_speed:.1f} KB/s)")
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
# core/commands/mkdir.py
|
|
||||||
from core.connection import BuzzerError
|
|
||||||
|
|
||||||
def execute(conn):
|
|
||||||
"""Startet den Buzzer neu."""
|
|
||||||
try:
|
|
||||||
conn.send_command(f"reboot")
|
|
||||||
print(f"🔄 Buzzer wird neu gestartet.")
|
|
||||||
except BuzzerError as e:
|
|
||||||
print(f"❌ Fehler beim Neustarten des Buzzers: {e}")
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
# core/commands/rm.py
|
|
||||||
import fnmatch
|
|
||||||
import posixpath
|
|
||||||
from core.connection import BuzzerError
|
|
||||||
from core.commands.ls import get_file_tree
|
|
||||||
|
|
||||||
def _delete_recursive(conn, nodes):
|
|
||||||
"""Löscht Knoten Bottom-Up (erst Dateien/Unterordner, dann den Ordner selbst)"""
|
|
||||||
for node in nodes:
|
|
||||||
if node["type"] == 'D':
|
|
||||||
if "children" in node and node["children"]:
|
|
||||||
_delete_recursive(conn, node["children"])
|
|
||||||
_try_rm(conn, node["path"], is_dir=True)
|
|
||||||
elif node["type"] == 'F':
|
|
||||||
_try_rm(conn, node["path"], is_dir=False)
|
|
||||||
|
|
||||||
def _try_rm(conn, path, is_dir=False):
|
|
||||||
icon = "📁" if is_dir else "📄"
|
|
||||||
try:
|
|
||||||
conn.send_command(f"rm {path}")
|
|
||||||
print(f" 🗑️ {icon} Gelöscht: {path}")
|
|
||||||
except BuzzerError as e:
|
|
||||||
print(f" ❌ Fehler bei {path}: {e}")
|
|
||||||
|
|
||||||
def execute(conn, path: str, recursive: bool = False):
|
|
||||||
"""Löscht eine Datei, ein Verzeichnis oder löst Wildcards (*) auf."""
|
|
||||||
|
|
||||||
# 1. Wildcard-Behandlung (z.B. /lfs/a/* oder *.wav)
|
|
||||||
if '*' in path or '?' in path:
|
|
||||||
dirname, pattern = posixpath.split(path)
|
|
||||||
if not dirname:
|
|
||||||
dirname = "/"
|
|
||||||
|
|
||||||
print(f"Suche nach Dateien passend zu '{pattern}' in '{dirname}'...")
|
|
||||||
tree = get_file_tree(conn, target_path=dirname, recursive=False)
|
|
||||||
|
|
||||||
# Fehler beim Verzeichnis-Lesen abfangen
|
|
||||||
if len(tree) == 1 and tree[0].get("type") == "E":
|
|
||||||
print(f"❌ Verzeichnis '{dirname}' nicht gefunden.")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Filtern mit fnmatch (funktioniert wie in der Linux-Shell)
|
|
||||||
matches = [node for node in tree if node.get("type") == "F" and fnmatch.fnmatch(node["name"], pattern)]
|
|
||||||
|
|
||||||
if not matches:
|
|
||||||
print(f"Keine passenden Dateien für '{path}' gefunden.")
|
|
||||||
return
|
|
||||||
|
|
||||||
for match in matches:
|
|
||||||
_try_rm(conn, match["path"], is_dir=False)
|
|
||||||
|
|
||||||
return # Fertig mit Wildcard-Löschen
|
|
||||||
|
|
||||||
# 2. Rekursives Löschen (-r)
|
|
||||||
if recursive:
|
|
||||||
print(f"Sammle Dateibaum für rekursives Löschen von '{path}'...")
|
|
||||||
tree = get_file_tree(conn, target_path=path, recursive=True)
|
|
||||||
|
|
||||||
if len(tree) == 1 and tree[0].get("type") == "E":
|
|
||||||
print(f"❌ Pfad nicht gefunden oder Fehler: {tree[0]['name']}")
|
|
||||||
return
|
|
||||||
|
|
||||||
if not tree:
|
|
||||||
print(f"Ordner '{path}' ist bereits leer.")
|
|
||||||
else:
|
|
||||||
_delete_recursive(conn, tree)
|
|
||||||
|
|
||||||
# 3. Standard-Löschen (Einzeldatei oder am Ende der Rekursion der leere Ordner)
|
|
||||||
try:
|
|
||||||
conn.send_command(f"rm {path}")
|
|
||||||
print(f"🗑️ '{path}' erfolgreich gelöscht.")
|
|
||||||
except BuzzerError as e:
|
|
||||||
print(f"❌ Fehler beim Löschen von '{path}': {e}")
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
# core/config.py
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
DEFAULT_CONFIG = {
|
|
||||||
"port": None,
|
|
||||||
"baudrate": 115200,
|
|
||||||
"timeout": 5.0
|
|
||||||
}
|
|
||||||
|
|
||||||
def load_config(cli_args=None):
|
|
||||||
config = DEFAULT_CONFIG.copy()
|
|
||||||
|
|
||||||
cwd_config = os.path.join(os.getcwd(), "config.yaml")
|
|
||||||
script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
|
|
||||||
script_config = os.path.join(script_dir, "config.yaml")
|
|
||||||
|
|
||||||
yaml_path = cwd_config if os.path.exists(cwd_config) else script_config if os.path.exists(script_config) else None
|
|
||||||
|
|
||||||
if yaml_path:
|
|
||||||
try:
|
|
||||||
with open(yaml_path, "r", encoding="utf-8") as f:
|
|
||||||
yaml_data = yaml.safe_load(f)
|
|
||||||
if yaml_data and "serial" in yaml_data:
|
|
||||||
config["port"] = yaml_data["serial"].get("port", config["port"])
|
|
||||||
config["baudrate"] = yaml_data["serial"].get("baudrate", config["baudrate"])
|
|
||||||
config["timeout"] = yaml_data["serial"].get("timeout", config["timeout"])
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Fehler beim Laden der Konfigurationsdatei {yaml_path}: {e}")
|
|
||||||
|
|
||||||
if cli_args:
|
|
||||||
if getattr(cli_args, "port", None) is not None:
|
|
||||||
config["port"] = cli_args.port
|
|
||||||
if getattr(cli_args, "baudrate", None) is not None:
|
|
||||||
config["baudrate"] = cli_args.baudrate
|
|
||||||
if getattr(cli_args, "timeout", None) is not None:
|
|
||||||
config["timeout"] = cli_args.timeout
|
|
||||||
|
|
||||||
return config
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
# core/connection.py
|
|
||||||
import serial
|
|
||||||
import time
|
|
||||||
import os
|
|
||||||
|
|
||||||
PROTOCOL_ERROR_MESSAGES = {
|
|
||||||
0x01: "Ungültiger Befehl.",
|
|
||||||
0x02: "Ungültige Parameter.",
|
|
||||||
0x03: "Befehl oder Parameter sind zu lang.",
|
|
||||||
0x10: "Datei oder Verzeichnis wurde nicht gefunden.",
|
|
||||||
0x11: "Ziel existiert bereits.",
|
|
||||||
0x12: "Pfad ist kein Verzeichnis.",
|
|
||||||
0x13: "Pfad ist ein Verzeichnis.",
|
|
||||||
0x14: "Zugriff verweigert.",
|
|
||||||
0x15: "Kein freier Speicher mehr vorhanden.",
|
|
||||||
0x16: "Datei ist zu groß.",
|
|
||||||
0x20: "Allgemeiner Ein-/Ausgabefehler auf dem Gerät.",
|
|
||||||
0x21: "Zeitüberschreitung auf dem Gerät.",
|
|
||||||
0x22: "CRC-Prüfung fehlgeschlagen (Daten beschädigt).",
|
|
||||||
0x23: "Übertragung wurde vom Gerät abgebrochen.",
|
|
||||||
0x30: "Befehl wird vom Gerät nicht unterstützt.",
|
|
||||||
0x31: "Gerät ist beschäftigt.",
|
|
||||||
0x32: "Interner Gerätefehler.",
|
|
||||||
}
|
|
||||||
|
|
||||||
class BuzzerError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class BuzzerConnection:
|
|
||||||
def __init__(self, config):
|
|
||||||
self.port = config.get("port")
|
|
||||||
self.baudrate = config.get("baudrate", 115200)
|
|
||||||
self.timeout = config.get("timeout", 5.0)
|
|
||||||
self.serial = None
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
if not self.port:
|
|
||||||
raise ValueError("Kein serieller Port konfiguriert.")
|
|
||||||
|
|
||||||
# write_timeout verhindert endloses Blockieren auf inaktiven Ports
|
|
||||||
self.serial = serial.Serial(
|
|
||||||
port=self.port,
|
|
||||||
baudrate=self.baudrate,
|
|
||||||
timeout=self.timeout,
|
|
||||||
write_timeout=self.timeout
|
|
||||||
)
|
|
||||||
self.serial.reset_input_buffer()
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
||||||
if self.serial and self.serial.is_open:
|
|
||||||
self.serial.close()
|
|
||||||
|
|
||||||
def _parse_controller_error(self, line: str) -> str:
|
|
||||||
code_str = line.split(" ", 1)[1].strip() if " " in line else ""
|
|
||||||
try:
|
|
||||||
code = int(code_str, 10)
|
|
||||||
except ValueError:
|
|
||||||
return f"Controller meldet einen unbekannten Fehler: '{line}'"
|
|
||||||
|
|
||||||
message = PROTOCOL_ERROR_MESSAGES.get(code, "Unbekannter Fehlercode vom Gerät.")
|
|
||||||
return f"Controller-Fehler {code} (0x{code:02X}): {message}"
|
|
||||||
|
|
||||||
def send_command(self, command: str, custom_timeout: float = None) -> list:
|
|
||||||
eff_timeout = custom_timeout if custom_timeout is not None else self.timeout
|
|
||||||
self.serial.reset_input_buffer()
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.serial.write(f"{command}\n".encode('utf-8'))
|
|
||||||
self.serial.flush()
|
|
||||||
except serial.SerialTimeoutException:
|
|
||||||
raise TimeoutError(f"Schreib-Timeout am Port {self.port}. Ist das Gerät blockiert?")
|
|
||||||
|
|
||||||
lines = []
|
|
||||||
start_time = time.monotonic()
|
|
||||||
|
|
||||||
while (time.monotonic() - start_time) < eff_timeout:
|
|
||||||
if self.serial.in_waiting > 0:
|
|
||||||
try:
|
|
||||||
line = self.serial.readline().decode('utf-8', errors='ignore').strip()
|
|
||||||
if not line:
|
|
||||||
continue
|
|
||||||
if line == "OK":
|
|
||||||
return lines
|
|
||||||
elif line.startswith("ERR"):
|
|
||||||
raise BuzzerError(self._parse_controller_error(line))
|
|
||||||
else:
|
|
||||||
lines.append(line)
|
|
||||||
except BuzzerError:
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
|
||||||
raise BuzzerError(f"Fehler beim Lesen der Antwort: {e}")
|
|
||||||
else:
|
|
||||||
time.sleep(0.01)
|
|
||||||
|
|
||||||
raise TimeoutError(f"Lese-Timeout ({eff_timeout}s) beim Warten auf Antwort für: '{command}'")
|
|
||||||
|
|
||||||
def send_binary(self, filepath: str, chunk_size: int = 4096, timeout: float = 10.0, progress_callback=None):
|
|
||||||
"""
|
|
||||||
Überträgt eine Binärdatei in Chunks, nachdem das READY-Signal empfangen wurde.
|
|
||||||
"""
|
|
||||||
# 1. Warte auf die READY-Bestätigung vom Controller
|
|
||||||
start_time = time.time()
|
|
||||||
ready = False
|
|
||||||
while (time.time() - start_time) < timeout:
|
|
||||||
if self.serial.in_waiting > 0:
|
|
||||||
line = self.serial.readline().decode('utf-8', errors='ignore').strip()
|
|
||||||
if line == "READY":
|
|
||||||
ready = True
|
|
||||||
break
|
|
||||||
elif line.startswith("ERR"):
|
|
||||||
raise BuzzerError(f"Fehler vor Binärtransfer: {self._parse_controller_error(line)}")
|
|
||||||
time.sleep(0.01)
|
|
||||||
|
|
||||||
if not ready:
|
|
||||||
raise TimeoutError("Kein READY-Signal vom Controller empfangen.")
|
|
||||||
|
|
||||||
# 2. Sende die Datei in Blöcken
|
|
||||||
file_size = os.path.getsize(filepath)
|
|
||||||
bytes_sent = 0
|
|
||||||
|
|
||||||
with open(filepath, 'rb') as f:
|
|
||||||
while bytes_sent < file_size:
|
|
||||||
# 1. Nicht blockierende Fehlerprüfung vor jedem Chunk
|
|
||||||
if self.serial.in_waiting > 0:
|
|
||||||
line = self.serial.readline().decode('utf-8', errors='ignore').strip()
|
|
||||||
if line.startswith("ERR"):
|
|
||||||
raise BuzzerError(f"Controller hat Transfer abgebrochen: {self._parse_controller_error(line)}")
|
|
||||||
|
|
||||||
# 2. Chunk lesen und schreiben
|
|
||||||
chunk = f.read(chunk_size)
|
|
||||||
if not chunk:
|
|
||||||
break
|
|
||||||
|
|
||||||
self.serial.write(chunk)
|
|
||||||
# WICHTIG: self.serial.flush() hier entfernen.
|
|
||||||
# Dies verhindert den Deadlock mit dem OS-USB-Puffer.
|
|
||||||
|
|
||||||
bytes_sent += len(chunk)
|
|
||||||
|
|
||||||
# 3. Callback für UI
|
|
||||||
if progress_callback:
|
|
||||||
progress_callback(len(chunk))
|
|
||||||
|
|
||||||
# 3. Warte auf das finale OK (oder ERR bei CRC/Schreib-Fehlern)
|
|
||||||
start_time = time.time()
|
|
||||||
while (time.time() - start_time) < timeout:
|
|
||||||
if self.serial.in_waiting > 0:
|
|
||||||
line = self.serial.readline().decode('utf-8', errors='ignore').strip()
|
|
||||||
if line == "OK":
|
|
||||||
return True
|
|
||||||
elif line.startswith("ERR"):
|
|
||||||
raise BuzzerError(f"Fehler beim Speichern der Binärdatei: {self._parse_controller_error(line)}")
|
|
||||||
time.sleep(0.01)
|
|
||||||
|
|
||||||
raise TimeoutError("Zeitüberschreitung nach Binärtransfer (kein OK empfangen).")
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
def hex_to_bytearray(hex_string):
|
|
||||||
"""
|
|
||||||
Wandelt einen Hex-String (z.B. "deadbeef") in ein bytearray um.
|
|
||||||
Entfernt vorher Leerzeichen und prüft auf Gültigkeit.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Whitespace entfernen (falls vorhanden)
|
|
||||||
clean_hex = hex_string.strip().replace(" ", "")
|
|
||||||
|
|
||||||
# Konvertierung
|
|
||||||
return bytearray.fromhex(clean_hex)
|
|
||||||
|
|
||||||
except ValueError as e:
|
|
||||||
print(f"Fehler bei der Konvertierung: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def string_to_hexstring(text):
|
|
||||||
"""
|
|
||||||
Wandelt einen String in einen UTF-8-kodierten Hex-String um.
|
|
||||||
"""
|
|
||||||
# 1. String zu UTF-8 Bytes
|
|
||||||
utf8_bytes = text.encode('utf-8')
|
|
||||||
|
|
||||||
# 2. Bytes zu Hex-String
|
|
||||||
return utf8_bytes.hex()
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
pyyaml
|
|
||||||
pyserial
|
|
||||||
@@ -9,9 +9,12 @@ target_sources(app PRIVATE
|
|||||||
src/io.c
|
src/io.c
|
||||||
src/audio.c
|
src/audio.c
|
||||||
src/usb.c
|
src/usb.c
|
||||||
|
src/uart.c
|
||||||
src/protocol.c
|
src/protocol.c
|
||||||
src/utils.c
|
src/utils.c
|
||||||
|
src/settings.c
|
||||||
)
|
)
|
||||||
zephyr_include_directories(src)
|
|
||||||
|
zephyr_include_directories(include)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
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.
|
||||||
|
|
||||||
122
firmware/Tags.md
Normal file
122
firmware/Tags.md
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
# Edi's Buzzer - Metadata Tags Format
|
||||||
|
|
||||||
|
## Architektur-Übersicht
|
||||||
|
Die Metadaten werden transparent an das Ende der rohen Audio-Daten angehängt. Das Format basiert auf einer strikten **Little-Endian** Byte-Reihenfolge und nutzt eine erweiterbare **TLV-Struktur** (Type-Length-Value) für die eigentlichen Datenblöcke.
|
||||||
|
|
||||||
|
Das physische Layout einer Datei im Flash-Speicher sieht wie folgt aus:
|
||||||
|
`[Audio-Rohdaten] [TLV-Block 1] ... [TLV-Block N] [Footer (8 Bytes)]`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Footer-Struktur
|
||||||
|
Der Footer liegt exakt auf den letzten 8 Bytes der Datei (EOF - 8). Er dient als Ankerpunkt für den Parser, um die Metadaten rückwärts aus der Datei zu extrahieren. Das 8-Byte-Alignment stellt speichersichere Casts auf 32-Bit-ARM-Architekturen sicher.
|
||||||
|
|
||||||
|
| Offset | Feld | Typ | Beschreibung |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| 0 | `total_size` | `uint16_t` | Gesamtgröße in Bytes (Summe aller TLV-Blöcke + 8 Bytes Footer). |
|
||||||
|
| 2 | `version` | `uint16_t` | Format-Version. Aktuell `0x0001`. |
|
||||||
|
| 4 | `magic` | `char[4]` | Fixe Signatur: `"TAG!"` (Hex: `54 41 47 21`). |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. TLV-Header (Type-Length-Value)
|
||||||
|
Jeder Metadaten-Block beginnt mit einem exakt 4 Bytes großen Header. Unbekannte Typen können vom Controller durch einen relativen Sprung (`fs_seek` um `length` Bytes) übersprungen werden.
|
||||||
|
|
||||||
|
| Offset | Feld | Typ | Beschreibung |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| 0 | `type` | `uint8_t` | Definiert den Inhalt des Blocks (siehe Typen-Definitionen). |
|
||||||
|
| 1 | `index` | `uint8_t` | Erlaubt die Fragmentierung großer Datensätze (z.B. bei JSON > 64 KB). Standard: `0x00`. |
|
||||||
|
| 2 | `length` | `uint16_t` | Größe der folgenden Payload in Bytes (ohne diesen Header). |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Typen-Definitionen
|
||||||
|
|
||||||
|
### Type `0x00`: Binary System Metadata
|
||||||
|
Dieser Typ gruppiert maschinenlesbare, binäre Systeminformationen. Die Unterscheidung erfolgt über das `Index`-Feld.
|
||||||
|
|
||||||
|
#### Index `0x00`: Audio Format
|
||||||
|
Dieser Block konfiguriert den I2S-Treiber vor der Wiedergabe.
|
||||||
|
* **Typ:** `0x00`
|
||||||
|
* **Index:** `0x00`
|
||||||
|
* **Länge:** `0x0008` (8 Bytes)
|
||||||
|
* **Payload:** `[codec: 1 Byte] [bit_depth: 1 Byte] [reserved: 2 Bytes] [samplerate: 4 Bytes]`
|
||||||
|
|
||||||
|
#### Index `0x01`: Audio CRC32
|
||||||
|
Speichert die CRC32-Prüfsumme (IEEE) der reinen Audiodaten (vom Dateianfang bis zum Beginn des ersten TLV-Blocks). Dient Synchronisations-Tools für einen schnellen Integritäts- und Abgleich-Check, ohne die gesamte Datei neu hashen zu müssen.
|
||||||
|
* **Typ:** `0x00`
|
||||||
|
* **Index:** `0x01`
|
||||||
|
* **Länge:** `0x0004` (4 Bytes)
|
||||||
|
* **Payload:** `uint32_t` (Little-Endian)
|
||||||
|
|
||||||
|
### Type `0x10`: JSON Metadata
|
||||||
|
Dieser Block enthält Metadaten, die primär für das Host-System (z. B. das Python-Tool) zur Verwaltung, Kategorisierung und Anzeige bestimmt sind. Der Mikrocontroller ignoriert und überspringt diesen Block während der Audiowiedergabe.
|
||||||
|
|
||||||
|
* **Typ:** `0x10`
|
||||||
|
* **Länge:** Variabel
|
||||||
|
* **Payload:** UTF-8-kodierter JSON-String (ohne Null-Terminator).
|
||||||
|
|
||||||
|
#### Standardisierte JSON-Schlüssel
|
||||||
|
Die nachfolgenden Schlüssel (Keys) sind im Basis-Standard definiert. Die Integration weiterer, proprietärer Schlüssel ist technisch möglich. Es wird jedoch empfohlen, dies mit Vorsicht zu handhaben, da zukünftige Standardisierungen diese Schlüsselnamen belegen könnten (Namenskollision).
|
||||||
|
|
||||||
|
| Schlüssel | Datentyp | Beschreibung |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `t` | String | Titel der Audiodatei |
|
||||||
|
| `a` | String | Autor oder Ersteller |
|
||||||
|
| `r` | String | Bemerkungen (Remarks) oder Beschreibung |
|
||||||
|
| `c` | Array of Strings | Kategorien zur Gruppierung |
|
||||||
|
| `dc` | String | Erstellungsdatum (Date Created), idealerweise nach ISO 8601 |
|
||||||
|
| `ds` | String | Speicher- oder Änderungsdatum (Date Saved), idealerweise nach ISO 8601 |
|
||||||
|
|
||||||
|
**Beispiel-Payload:**
|
||||||
|
Ein vollständiger JSON-Datensatz gemäß dieser Spezifikation hat folgendes Format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"t": "Testaufnahme System A",
|
||||||
|
"a": "Entwickler-Team",
|
||||||
|
"r": "Überprüfung der Mikrofon-Aussteuerung.",
|
||||||
|
"c": ["Test", "Audio", "V1"],
|
||||||
|
"dc": "2026-03-05T13:00:00Z",
|
||||||
|
"ds": "2026-03-05T13:10:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
*(Hinweis zur Skalierbarkeit: Für zukünftige Erweiterungen können dedizierte TLV-Typen definiert werden, wie beispielsweise 0x11 für GZIP-komprimierte JSON-Daten oder 0x20 für binäre Bilddaten wie PNG-Cover).*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Lese-Algorithmus (Parser-Logik)
|
||||||
|
|
||||||
|
Der Controller extrahiert die Hardware-Parameter nach folgendem Ablauf:
|
||||||
|
|
||||||
|
1. **Footer lokalisieren:** * Gehe zu `EOF - 8`. Lese 8 Bytes in das `tag_footer_t` Struct.
|
||||||
|
* Validiere `magic == "TAG!"` und `version == 0x0001` (unter Berücksichtigung von Little-Endian Konvertierung via `sys_le16_to_cpu`).
|
||||||
|
2. **Grenzen berechnen:**
|
||||||
|
* Lese `total_size`.
|
||||||
|
* Die reinen Audiodaten enden bei `audio_limit = EOF - total_size`.
|
||||||
|
* Gehe zur Position `audio_limit`.
|
||||||
|
3. **TLV-Blöcke iterieren:**
|
||||||
|
* Solange die aktuelle Leseposition kleiner als `EOF - 8` ist:
|
||||||
|
* Lese 4 Bytes in den `tlv_header_t`.
|
||||||
|
* Wenn `type == 0x00`: Lese die nächsten 8 Bytes in das `tlv_audio_format_t` Struct.
|
||||||
|
* Wenn `type != 0x00`: Führe `fs_seek(header.length, FS_SEEK_CUR)` aus.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Hex-Beispiel
|
||||||
|
|
||||||
|
Eine fiktive Datei enthält Audio-Daten. Es soll ein PCM-Mono Format (16 Bit, 16 kHz) sowie ein kurzes JSON `{"t":"A"}` (9 Bytes) angehängt werden.
|
||||||
|
|
||||||
|
**1. TLV 0x00 (Audio Format):**
|
||||||
|
* Header: `00 00 08 00` (Type 0, Index 0, Length 8)
|
||||||
|
* Payload: `00 10 00 00 80 3E 00 00` (Mono, 16-Bit, Reserved, 16000 Hz)
|
||||||
|
|
||||||
|
**2. TLV 0x10 (JSON):**
|
||||||
|
* Header: `10 00 09 00` (Type 16, Index 0, Length 9)
|
||||||
|
* Payload: `7B 22 74 22 3A 22 41 22 7D` (`{"t":"A"}`)
|
||||||
|
|
||||||
|
**3. Footer:**
|
||||||
|
* Total Size: `2D 00` (45 Bytes = 12 Bytes Audio-TLV + 13 Bytes JSON-TLV + 12 Bytes Padding/Zusatz + 8 Bytes Footer) -> *Hinweis: Size ist in diesem Konstrukt abhängig vom genauen Payload.*
|
||||||
|
* Version: `01 00`
|
||||||
|
* Magic: `54 41 47 21` (`TAG!`)
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
VERSION_MAJOR = 0
|
VERSION_MAJOR = 0
|
||||||
VERSION_MINOR = 1
|
VERSION_MINOR = 3
|
||||||
PATCHLEVEL = 14
|
PATCHLEVEL = 5
|
||||||
VERSION_TWEAK = 0
|
VERSION_TWEAK = 0
|
||||||
|
#if (IS_ENABLED(CONFIG_LOG))
|
||||||
EXTRAVERSION = debug
|
EXTRAVERSION = debug
|
||||||
|
#else
|
||||||
|
EXTRAVERSION = 0
|
||||||
|
#endif
|
||||||
@@ -3,11 +3,44 @@
|
|||||||
|
|
||||||
#include <zephyr/fs/fs.h>
|
#include <zephyr/fs/fs.h>
|
||||||
|
|
||||||
|
#define MAX_PATH_LEN 32U
|
||||||
|
|
||||||
typedef struct slot_info_t {
|
typedef struct slot_info_t {
|
||||||
size_t start_addr;
|
size_t start_addr;
|
||||||
size_t size;
|
size_t size;
|
||||||
} slot_info_t;
|
} slot_info_t;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
FS_MSG_START,
|
||||||
|
FS_MSG_CHUNK,
|
||||||
|
FS_MSG_EOF,
|
||||||
|
FS_MSG_ABORT
|
||||||
|
} fs_msg_type_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
fs_msg_type_t type;
|
||||||
|
|
||||||
|
/* Die Union spart RAM, da Start- und Chunk-Parameter
|
||||||
|
nie gleichzeitig im selben Message-Paket benötigt werden. */
|
||||||
|
union {
|
||||||
|
/* Payload für FS_MSG_START */
|
||||||
|
struct {
|
||||||
|
/* Der String wird sicher in die Queue kopiert */
|
||||||
|
char filename[MAX_PATH_LEN];
|
||||||
|
uint32_t expected_size;
|
||||||
|
uint32_t start_position;
|
||||||
|
} start;
|
||||||
|
|
||||||
|
/* Payload für FS_MSG_CHUNK */
|
||||||
|
struct {
|
||||||
|
void *slab_ptr;
|
||||||
|
uint32_t chunk_size;
|
||||||
|
} chunk;
|
||||||
|
};
|
||||||
|
} fs_msg_t;
|
||||||
|
|
||||||
|
extern struct k_msgq fs_msgq;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Initializes the filesystem by mounting it
|
* @brief Initializes the filesystem by mounting it
|
||||||
*/
|
*/
|
||||||
@@ -74,6 +107,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 +124,30 @@ 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 Recursively creates directories for the given path, ensuring the flash is active during the operation
|
||||||
|
* @param path Path to the directory to create (can include multiple levels, e.g. "/dir1/dir2/dir3")
|
||||||
|
* @return 0 on success, negative error code on failure
|
||||||
|
*/
|
||||||
|
int fs_pm_mkdir_recursive(char *path);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Recursively removes a directory and all its contents, ensuring the flash is active during the operation
|
||||||
|
* @param path Path to the directory to remove
|
||||||
|
* @param max_len Maximum length of the path buffer
|
||||||
|
* @return 0 on success, negative error code on failure
|
||||||
|
*/
|
||||||
|
int fs_pm_rm_recursive(char *path, size_t max_len);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @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 +166,33 @@ 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 structure representing the audio file
|
* @param fp Pointer to an open fs_file_t positioned in tag payload
|
||||||
* @param hex_str Buffer to be filled with the hexadecimal string (must be large enough to hold the data)
|
* @param buffer Destination buffer
|
||||||
* @param hex_str_size Size of the hex_str buffer
|
* @param len Maximum bytes to read
|
||||||
* @return 0 on success, negative error code on failure
|
* @return Number of bytes read, 0 at payload end, negative error code on failure
|
||||||
*/
|
*/
|
||||||
int fs_read_hex_tag(struct fs_file_t *fp, char *hex_str, size_t hex_str_size);
|
ssize_t fs_tag_read_chunk(struct fs_file_t *fp, void *buffer, size_t len);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Setzt die Synchronisation für einen neuen Dateitransfer zurück.
|
||||||
|
*/
|
||||||
|
void fs_reset_transfer_sync(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Blockiert den aufrufenden Thread, bis der FS-Thread den Transfer
|
||||||
|
* (EOF oder ABORT) vollständig auf dem Flash abgeschlossen hat.
|
||||||
|
*/
|
||||||
|
void fs_wait_for_transfer_complete(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @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
|
||||||
@@ -138,4 +216,21 @@ int flash_init_firmware_upload(void);
|
|||||||
*/
|
*/
|
||||||
int flash_write_firmware_block(const uint8_t *buffer, size_t length, bool is_last_block);
|
int flash_write_firmware_block(const uint8_t *buffer, size_t length, bool is_last_block);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Gets the page size of the internal flash, which is needed for proper write operations
|
||||||
|
* @return Page size in bytes
|
||||||
|
*/
|
||||||
|
size_t fs_get_internal_flash_page_size(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Gets the size of the firmware slot, which is needed for proper write operations
|
||||||
|
* @return Size in bytes
|
||||||
|
*/
|
||||||
|
size_t fs_get_fw_slot_size(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Gets the page size of the external flash, which is needed for proper write operations
|
||||||
|
* @return Page size in bytes
|
||||||
|
*/
|
||||||
|
size_t fs_get_external_flash_page_size(void);
|
||||||
#endif // FS_H
|
#endif // FS_H
|
||||||
90
firmware/include/protocol.h
Normal file
90
firmware/include/protocol.h
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
#ifndef PROTOCOL_H
|
||||||
|
#define PROTOCOL_H
|
||||||
|
|
||||||
|
#include <zephyr/kernel.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#define PROTOCOL_MAX_PATH_LEN 32U
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
PS_WAIT_SYNC = 0,
|
||||||
|
PS_READ_FRAME_TYPE,
|
||||||
|
PS_READ_REQ,
|
||||||
|
PS_READ_REQ_DATA,
|
||||||
|
} protocol_state_t;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
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_STAT = 0x18,
|
||||||
|
CMD_RENAME = 0x19,
|
||||||
|
|
||||||
|
CMD_PUT_FILE = 0x20,
|
||||||
|
CMD_PUT_FW = 0x21,
|
||||||
|
CMD_GET_FILE = 0x22,
|
||||||
|
CMD_PUT_TAGS = 0x24,
|
||||||
|
CMD_GET_TAGS = 0x25,
|
||||||
|
|
||||||
|
CMD_PLAY = 0x30,
|
||||||
|
CMD_STOP = 0x31,
|
||||||
|
|
||||||
|
CMD_SET_SETTING = 0x40,
|
||||||
|
CMD_GET_SETTING = 0x41,
|
||||||
|
} protocol_cmd_t;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
FRAME_REQ = 0x01,
|
||||||
|
FRAME_REQ_DATA = 0x02,
|
||||||
|
FRAME_RESP_ACK = 0x10,
|
||||||
|
FRAME_RESP_DATA = 0x11,
|
||||||
|
FRAME_RESP_STREAM_START = 0x12,
|
||||||
|
// FRAME_RESP_STREAM_CHUNK = 0x13,
|
||||||
|
FRAME_RESP_STREAM_END = 0x14,
|
||||||
|
FRAME_RESP_LIST_START = 0x15,
|
||||||
|
FRAME_RESP_LIST_CHUNK = 0x16,
|
||||||
|
FRAME_RESP_LIST_END = 0x17,
|
||||||
|
FRAME_RESP_ERROR = 0xFF,
|
||||||
|
} protocol_frame_type_t;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
P_ERR_NONE = 0x00,
|
||||||
|
P_ERR_INVALID_COMMAND = 0x01,
|
||||||
|
P_ERR_INVALID_PARAMETERS = 0x02,
|
||||||
|
P_ERR_COMMAND_TOO_LONG = 0x03,
|
||||||
|
|
||||||
|
P_ERR_FILE_NOT_FOUND = 0x10,
|
||||||
|
P_ERR_ALREADY_EXISTS = 0x11,
|
||||||
|
P_ERR_NOT_A_DIRECTORY = 0x12,
|
||||||
|
P_ERR_IS_A_DIRECTORY = 0x13,
|
||||||
|
P_ERR_ACCESS_DENIED = 0x14,
|
||||||
|
P_ERR_NO_SPACE = 0x15,
|
||||||
|
P_ERR_FILE_TOO_LARGE = 0x16,
|
||||||
|
|
||||||
|
P_ERR_IO = 0x20,
|
||||||
|
P_ERR_TIMEOUT = 0x21,
|
||||||
|
P_ERR_CRC_MISMATCH = 0x22,
|
||||||
|
P_ERR_TRANSFER_ABORTED = 0x23,
|
||||||
|
|
||||||
|
P_ERR_NOT_SUPPORTED = 0x30,
|
||||||
|
P_ERR_BUSY = 0x31,
|
||||||
|
P_ERR_INTERNAL = 0x32,
|
||||||
|
} protocol_error_t;
|
||||||
|
|
||||||
|
typedef enum
|
||||||
|
{
|
||||||
|
FW_STATUS_CONFIRMED = 0x00,
|
||||||
|
FW_STATUS_PENDING = 0x01,
|
||||||
|
FW_STATUS_TESTING = 0x02,
|
||||||
|
} firmware_status_t;
|
||||||
|
|
||||||
|
void protocol_thread_entry(void *p1, void *p2, void *p3);
|
||||||
|
|
||||||
|
#endif // PROTOCOL_H
|
||||||
28
firmware/include/settings.h
Normal file
28
firmware/include/settings.h
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#ifndef BUZZER_SETTINGS_H
|
||||||
|
#define BUZZER_SETTINGS_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
/* Struktur für den direkten Lesezugriff aus dem RAM (Zero-Latency) */
|
||||||
|
typedef struct {
|
||||||
|
uint8_t audio_vol; /* 0..100 */
|
||||||
|
bool play_norepeat; /* true = 1, false = 0 */
|
||||||
|
uint32_t storage_interval_s; /* 0..7200 Sekunden */
|
||||||
|
} app_settings_t;
|
||||||
|
|
||||||
|
/* Globale Instanz für den direkten Lesezugriff */
|
||||||
|
extern app_settings_t app_settings;
|
||||||
|
|
||||||
|
/* Initialisiert das Settings-Subsystem, NVS und lädt die gespeicherten Werte */
|
||||||
|
int app_settings_init(void);
|
||||||
|
|
||||||
|
/* Setter: Aktualisieren den RAM-Wert und starten/verlängern den Speichern-Timer */
|
||||||
|
void app_settings_set_audio_vol(uint8_t vol);
|
||||||
|
void app_settings_set_play_norepeat(bool norepeat);
|
||||||
|
void app_settings_set_storage_interval(uint32_t interval_s);
|
||||||
|
|
||||||
|
/* Forciert sofortiges Speichern aller anstehenden Werte (Aufruf z.B. vor CMD_REBOOT) */
|
||||||
|
void app_settings_save_pending_now(void);
|
||||||
|
|
||||||
|
#endif /* BUZZER_SETTINGS_H */
|
||||||
8
firmware/include/uart.h
Normal file
8
firmware/include/uart.h
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#ifndef _UART_H_
|
||||||
|
#define _UART_H_
|
||||||
|
|
||||||
|
int uart_init(void);
|
||||||
|
int uart_write(const uint8_t *data, size_t len, k_timeout_t timeout);
|
||||||
|
int uart_write_string(const char *str, k_timeout_t timeout);
|
||||||
|
int uart_read(uint8_t *buffer, size_t max_len, k_timeout_t timeout);
|
||||||
|
#endif /* _UART_H_ */
|
||||||
8
firmware/include/usb.h
Normal file
8
firmware/include/usb.h
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#ifndef USB_H_
|
||||||
|
#define USB_H_
|
||||||
|
|
||||||
|
int usb_init(void);
|
||||||
|
void usb_wait_for_dtr(void);
|
||||||
|
bool usb_dtr_active(void);
|
||||||
|
|
||||||
|
#endif /* USB_H_ */
|
||||||
@@ -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
|
||||||
@@ -3,7 +3,7 @@ mcuboot:
|
|||||||
size: 0xC000
|
size: 0xC000
|
||||||
region: flash_primary
|
region: flash_primary
|
||||||
|
|
||||||
# Primary Slot: Start bleibt 0xC000, Größe jetzt 200KB (0x32000)
|
# Primary Slot: Start bleibt 0xC000, Größe 200KB (0x32000)
|
||||||
mcuboot_primary:
|
mcuboot_primary:
|
||||||
address: 0xC000
|
address: 0xC000
|
||||||
size: 0x32000
|
size: 0x32000
|
||||||
@@ -26,7 +26,13 @@ mcuboot_secondary:
|
|||||||
size: 0x32000
|
size: 0x32000
|
||||||
region: flash_primary
|
region: flash_primary
|
||||||
|
|
||||||
# External Flash bleibt unverändert
|
# NVS storage am Ende des Flashs, 16KB (0x4000)
|
||||||
|
settings_storage:
|
||||||
|
address: 0xFC000
|
||||||
|
size: 0x4000
|
||||||
|
region: flash_primary
|
||||||
|
|
||||||
|
# External Flash
|
||||||
littlefs_storage:
|
littlefs_storage:
|
||||||
address: 0x0
|
address: 0x0
|
||||||
size: 0x800000
|
size: 0x800000
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# --- GPIO & Logging ---
|
# --- GPIO & Logging ---
|
||||||
CONFIG_GPIO=y
|
CONFIG_GPIO=y
|
||||||
CONFIG_LOG=y
|
CONFIG_LOG=y
|
||||||
CONFIG_POLL=y
|
CONFIG_POLL=n
|
||||||
|
|
||||||
# --- Power Management (Fix für HAS_PM & Policy) ---
|
# --- Power Management (Fix für HAS_PM & Policy) ---
|
||||||
# CONFIG_PM=y
|
# CONFIG_PM=y
|
||||||
@@ -13,24 +13,25 @@ CONFIG_FLASH_MAP=y
|
|||||||
CONFIG_FILE_SYSTEM=y
|
CONFIG_FILE_SYSTEM=y
|
||||||
CONFIG_FILE_SYSTEM_LITTLEFS=y
|
CONFIG_FILE_SYSTEM_LITTLEFS=y
|
||||||
CONFIG_FILE_SYSTEM_MKFS=y
|
CONFIG_FILE_SYSTEM_MKFS=y
|
||||||
CONFIG_CRC=y
|
CONFIG_FS_LITTLEFS_READ_SIZE=256
|
||||||
CONFIG_FS_LITTLEFS_READ_SIZE=64
|
|
||||||
CONFIG_FS_LITTLEFS_PROG_SIZE=256
|
CONFIG_FS_LITTLEFS_PROG_SIZE=256
|
||||||
CONFIG_FS_LITTLEFS_CACHE_SIZE=512
|
CONFIG_FS_LITTLEFS_CACHE_SIZE=4096
|
||||||
CONFIG_FS_LITTLEFS_LOOKAHEAD_SIZE=128
|
CONFIG_FS_LITTLEFS_LOOKAHEAD_SIZE=256
|
||||||
CONFIG_FS_LITTLEFS_BLOCK_CYCLES=512
|
CONFIG_FS_LITTLEFS_BLOCK_CYCLES=512
|
||||||
CONFIG_MAIN_STACK_SIZE=2048
|
CONFIG_MAIN_STACK_SIZE=2048
|
||||||
|
|
||||||
|
# --- NVS & Settings (für die Speicherung von Konfigurationen) ---
|
||||||
|
CONFIG_NVS=y
|
||||||
|
CONFIG_SETTINGS=y
|
||||||
|
CONFIG_SETTINGS_NVS=y
|
||||||
|
|
||||||
# --- USB Device & CDC ACM ---
|
# --- USB Device & CDC ACM ---
|
||||||
CONFIG_USB_DEVICE_STACK=y
|
CONFIG_USB_DEVICE_STACK_NEXT=y
|
||||||
CONFIG_DEPRECATION_TEST=y
|
CONFIG_USBD_CDC_ACM_CLASS=y
|
||||||
CONFIG_USB_DEVICE_MANUFACTURER="Eduard Iten"
|
CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT=n
|
||||||
CONFIG_USB_DEVICE_PRODUCT="Edi's Buzzer"
|
CONFIG_USBD_LOG_LEVEL_ERR=y
|
||||||
CONFIG_USB_DEVICE_PID=0x0001
|
CONFIG_UDC_DRIVER_LOG_LEVEL_ERR=y
|
||||||
CONFIG_USB_DRIVER_LOG_LEVEL_ERR=y
|
CONFIG_USBD_CDC_ACM_LOG_LEVEL_OFF=y
|
||||||
CONFIG_USB_DEVICE_LOG_LEVEL_ERR=y
|
|
||||||
CONFIG_USB_DEVICE_INITIALIZE_AT_BOOT=n
|
|
||||||
CONFIG_USB_DEVICE_STACK_NEXT=n
|
|
||||||
|
|
||||||
# --- UART (für USB-CDC) ---
|
# --- UART (für USB-CDC) ---
|
||||||
CONFIG_SERIAL=y
|
CONFIG_SERIAL=y
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <audio.h>
|
#include <audio.h>
|
||||||
#include <fs.h>
|
#include <fs.h>
|
||||||
#include <io.h>
|
#include <io.h>
|
||||||
|
#include <settings.h>
|
||||||
|
|
||||||
#define AUDIO_THREAD_STACK_SIZE 2048
|
#define AUDIO_THREAD_STACK_SIZE 2048
|
||||||
#define AUDIO_THREAD_PRIORITY 5
|
#define AUDIO_THREAD_PRIORITY 5
|
||||||
@@ -19,7 +20,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)))
|
||||||
@@ -47,7 +48,6 @@ K_SEM_DEFINE(audio_ready_sem, 0, 1);
|
|||||||
static const struct device *const i2s_dev = DEVICE_DT_GET(I2S_NODE);
|
static const struct device *const i2s_dev = DEVICE_DT_GET(I2S_NODE);
|
||||||
static const struct gpio_dt_spec amp_en_dev = GPIO_DT_SPEC_GET(AUDIO_AMP_ENABLE_NODE, gpios);
|
static const struct gpio_dt_spec amp_en_dev = GPIO_DT_SPEC_GET(AUDIO_AMP_ENABLE_NODE, gpios);
|
||||||
|
|
||||||
static volatile int current_volume = 8;
|
|
||||||
static volatile bool abort_playback = false;
|
static volatile bool abort_playback = false;
|
||||||
static char next_random_filename[64] = {0};
|
static char next_random_filename[64] = {0};
|
||||||
|
|
||||||
@@ -57,6 +57,8 @@ static char cached_404_path[] = "/lfs/sys/404";
|
|||||||
static struct k_mutex i2s_lock;
|
static struct k_mutex i2s_lock;
|
||||||
static struct k_work audio_stop_work;
|
static struct k_work audio_stop_work;
|
||||||
|
|
||||||
|
static uint32_t last_played_index = 0xFFFFFFFF;
|
||||||
|
|
||||||
static void audio_stop_work_handler(struct k_work *work)
|
static void audio_stop_work_handler(struct k_work *work)
|
||||||
{
|
{
|
||||||
ARG_UNUSED(work);
|
ARG_UNUSED(work);
|
||||||
@@ -89,6 +91,52 @@ void i2s_resume(void)
|
|||||||
k_mutex_unlock(&i2s_lock);
|
k_mutex_unlock(&i2s_lock);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int get_random_file(char *out_filename, size_t max_len)
|
||||||
|
{
|
||||||
|
if (audio_file_count == 0)
|
||||||
|
{
|
||||||
|
/* Fallback auf System-Sound, wenn Ordner leer */
|
||||||
|
strncpy(out_filename, cached_404_path, max_len);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t target_index;
|
||||||
|
|
||||||
|
/* Random-Index generieren mit optionalem No-Repeat-Schutz */
|
||||||
|
if (app_settings.play_norepeat && audio_file_count > 1) {
|
||||||
|
do {
|
||||||
|
target_index = k_cycle_get_32() % audio_file_count;
|
||||||
|
} while (target_index == last_played_index);
|
||||||
|
} else {
|
||||||
|
target_index = k_cycle_get_32() % audio_file_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
last_played_index = target_index;
|
||||||
|
|
||||||
|
struct fs_dir_t dirp;
|
||||||
|
struct fs_dirent entry;
|
||||||
|
uint32_t current_index = 0;
|
||||||
|
|
||||||
|
fs_dir_t_init(&dirp);
|
||||||
|
if (fs_pm_opendir(&dirp, AUDIO_PATH) < 0)
|
||||||
|
return -ENOENT;
|
||||||
|
|
||||||
|
while (fs_readdir(&dirp, &entry) == 0 && entry.name[0] != '\0')
|
||||||
|
{
|
||||||
|
if (entry.type == FS_DIR_ENTRY_FILE)
|
||||||
|
{
|
||||||
|
if (current_index == target_index)
|
||||||
|
{
|
||||||
|
snprintf(out_filename, max_len, "%s/%s", AUDIO_PATH, entry.name);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
current_index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fs_pm_closedir(&dirp);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
void audio_refresh_file_count(void)
|
void audio_refresh_file_count(void)
|
||||||
{
|
{
|
||||||
static struct fs_dir_t dirp;
|
static struct fs_dir_t dirp;
|
||||||
@@ -112,6 +160,7 @@ void audio_refresh_file_count(void)
|
|||||||
fs_pm_closedir(&dirp);
|
fs_pm_closedir(&dirp);
|
||||||
audio_file_count = count;
|
audio_file_count = count;
|
||||||
LOG_INF("Audio cache refreshed: %u files found in %s", count, AUDIO_PATH);
|
LOG_INF("Audio cache refreshed: %u files found in %s", count, AUDIO_PATH);
|
||||||
|
get_random_file(next_random_filename, sizeof(next_random_filename));
|
||||||
}
|
}
|
||||||
|
|
||||||
static void wait_for_i2s_drain(void)
|
static void wait_for_i2s_drain(void)
|
||||||
@@ -130,40 +179,6 @@ static void wait_for_i2s_drain(void)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int get_random_file(char *out_filename, size_t max_len)
|
|
||||||
{
|
|
||||||
if (audio_file_count == 0)
|
|
||||||
{
|
|
||||||
/* Fallback auf System-Sound, wenn Ordner leer */
|
|
||||||
strncpy(out_filename, cached_404_path, max_len);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct fs_dir_t dirp;
|
|
||||||
struct fs_dirent entry;
|
|
||||||
uint32_t target_index = k_cycle_get_32() % audio_file_count;
|
|
||||||
uint32_t current_index = 0;
|
|
||||||
|
|
||||||
fs_dir_t_init(&dirp);
|
|
||||||
if (fs_pm_opendir(&dirp, AUDIO_PATH) < 0)
|
|
||||||
return -ENOENT;
|
|
||||||
|
|
||||||
while (fs_readdir(&dirp, &entry) == 0 && entry.name[0] != '\0')
|
|
||||||
{
|
|
||||||
if (entry.type == FS_DIR_ENTRY_FILE)
|
|
||||||
{
|
|
||||||
if (current_index == target_index)
|
|
||||||
{
|
|
||||||
snprintf(out_filename, max_len, "%s/%s", AUDIO_PATH, entry.name);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
current_index++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fs_pm_closedir(&dirp);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void audio_system_ready(void)
|
void audio_system_ready(void)
|
||||||
{
|
{
|
||||||
k_sem_give(&audio_ready_sem);
|
k_sem_give(&audio_ready_sem);
|
||||||
@@ -250,8 +265,8 @@ void audio_thread(void *arg1, void *arg2, void *arg3)
|
|||||||
|
|
||||||
bool trigger_started = false;
|
bool trigger_started = false;
|
||||||
int queued_blocks = 0;
|
int queued_blocks = 0;
|
||||||
uint8_t factor = MIN(255, current_volume * 0xFF / 100);
|
uint8_t factor = MIN(255, app_settings.audio_vol * 0xFF / 100);
|
||||||
LOG_INF("Volume factor: %u (for volume %d%%)", factor, current_volume);
|
LOG_INF("Volume factor: %u (for volume %d%%)", factor, app_settings.audio_vol);
|
||||||
|
|
||||||
while (!abort_playback)
|
while (!abort_playback)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,19 +1,32 @@
|
|||||||
#include <zephyr/fs/littlefs.h>
|
#include <zephyr/fs/littlefs.h>
|
||||||
|
#include <zephyr/sys/byteorder.h>
|
||||||
#include <zephyr/drivers/flash.h>
|
#include <zephyr/drivers/flash.h>
|
||||||
|
#include <zephyr/storage/flash_map.h>
|
||||||
#include <zephyr/dfu/flash_img.h>
|
#include <zephyr/dfu/flash_img.h>
|
||||||
#include <zephyr/dfu/mcuboot.h>
|
#include <zephyr/dfu/mcuboot.h>
|
||||||
#include <zephyr/pm/device.h>
|
#include <zephyr/pm/device.h>
|
||||||
|
|
||||||
#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 FS_THREAD_STACK_SIZE 2048
|
||||||
|
#define FS_THREAD_PRIORITY 6
|
||||||
|
|
||||||
|
#define FS_MSGQ_MAX_ITEMS 4
|
||||||
|
#define FS_SLAB_BUF_SIZE 4096
|
||||||
|
#define TAG_FORMAT_VERSION 0x0001
|
||||||
|
#define TAG_MAGIC "TAG!"
|
||||||
|
|
||||||
#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)
|
||||||
|
|
||||||
FS_LITTLEFS_DECLARE_DEFAULT_CONFIG(fs_storage_data);
|
FS_LITTLEFS_DECLARE_DEFAULT_CONFIG(fs_storage_data);
|
||||||
|
|
||||||
|
K_MEM_SLAB_DEFINE(file_buffer_slab, FS_SLAB_BUF_SIZE, FS_MSGQ_MAX_ITEMS, 4);
|
||||||
|
K_MSGQ_DEFINE(fs_msgq, sizeof(fs_msg_t), FS_MSGQ_MAX_ITEMS, 4);
|
||||||
|
K_SEM_DEFINE(fs_transfer_done_sem, 0, 1);
|
||||||
|
|
||||||
#define QSPI_FLASH_NODE DT_ALIAS(qspi_flash)
|
#define QSPI_FLASH_NODE DT_ALIAS(qspi_flash)
|
||||||
#if !DT_NODE_EXISTS(QSPI_FLASH_NODE)
|
#if !DT_NODE_EXISTS(QSPI_FLASH_NODE)
|
||||||
#error "QSPI Flash alias not defined in devicetree"
|
#error "QSPI Flash alias not defined in devicetree"
|
||||||
@@ -26,6 +39,8 @@ static struct k_mutex flash_pm_lock;
|
|||||||
static struct slot_info_t slot1_info;
|
static struct slot_info_t slot1_info;
|
||||||
static struct flash_img_context flash_ctx;
|
static struct flash_img_context flash_ctx;
|
||||||
|
|
||||||
|
extern struct k_mem_slab file_buffer_slab;
|
||||||
|
|
||||||
static struct fs_mount_t fs_storage_mnt = {
|
static struct fs_mount_t fs_storage_mnt = {
|
||||||
.type = FS_LITTLEFS,
|
.type = FS_LITTLEFS,
|
||||||
.fs_data = &fs_storage_data,
|
.fs_data = &fs_storage_data,
|
||||||
@@ -33,6 +48,17 @@ static struct fs_mount_t fs_storage_mnt = {
|
|||||||
.mnt_point = "/lfs",
|
.mnt_point = "/lfs",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
FS_STATE_IDLE,
|
||||||
|
FS_STATE_RECEIVING
|
||||||
|
} fs_thread_state_t;
|
||||||
|
|
||||||
|
typedef struct __attribute__((packed)) {
|
||||||
|
uint16_t total_size;
|
||||||
|
uint16_t version;
|
||||||
|
uint8_t magic[4];
|
||||||
|
} tag_footer_t;
|
||||||
|
|
||||||
int fs_init(void) {
|
int fs_init(void) {
|
||||||
int rc = fs_mount(&fs_storage_mnt);
|
int rc = fs_mount(&fs_storage_mnt);
|
||||||
if (rc < 0) {
|
if (rc < 0) {
|
||||||
@@ -114,10 +140,7 @@ int fs_pm_close(struct fs_file_t *file)
|
|||||||
{
|
{
|
||||||
LOG_DBG("PM Closing file");
|
LOG_DBG("PM Closing file");
|
||||||
int rc = fs_close(file);
|
int rc = fs_close(file);
|
||||||
if (rc == 0)
|
fs_pm_flash_suspend();
|
||||||
{
|
|
||||||
fs_pm_flash_suspend();
|
|
||||||
}
|
|
||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,10 +160,7 @@ int fs_pm_closedir(struct fs_dir_t *dirp)
|
|||||||
{
|
{
|
||||||
LOG_DBG("PM Closing directory");
|
LOG_DBG("PM Closing directory");
|
||||||
int rc = fs_closedir(dirp);
|
int rc = fs_closedir(dirp);
|
||||||
if (rc == 0)
|
fs_pm_flash_suspend();
|
||||||
{
|
|
||||||
fs_pm_flash_suspend();
|
|
||||||
}
|
|
||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,6 +182,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 +200,238 @@ int fs_pm_mkdir(const char *path)
|
|||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
int fs_pm_rm_recursive(char *path_buf, size_t max_len)
|
||||||
|
{
|
||||||
|
struct fs_dirent entry;
|
||||||
|
struct fs_dir_t dir;
|
||||||
|
int rc;
|
||||||
|
|
||||||
|
fs_pm_flash_resume();
|
||||||
|
|
||||||
|
/* 1. Stat prüfen: Ist es eine Datei? */
|
||||||
|
rc = fs_stat(path_buf, &entry);
|
||||||
|
if (rc != 0) {
|
||||||
|
fs_pm_flash_suspend();
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Wenn es eine Datei ist, direkt löschen und beenden */
|
||||||
|
if (entry.type == FS_DIR_ENTRY_FILE) {
|
||||||
|
rc = fs_unlink(path_buf);
|
||||||
|
fs_pm_flash_suspend();
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 2. Es ist ein Verzeichnis. Schleife bis es leer ist. */
|
||||||
|
size_t orig_len = strlen(path_buf);
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
fs_dir_t_init(&dir);
|
||||||
|
rc = fs_opendir(&dir, path_buf);
|
||||||
|
if (rc != 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool found_something = false;
|
||||||
|
|
||||||
|
/* Genau EINEN löschbaren Eintrag suchen */
|
||||||
|
while (1) {
|
||||||
|
rc = fs_readdir(&dir, &entry);
|
||||||
|
if (rc != 0 || entry.name[0] == '\0') {
|
||||||
|
break; /* Ende oder Fehler */
|
||||||
|
}
|
||||||
|
if (strcmp(entry.name, ".") == 0 || strcmp(entry.name, "..") == 0) {
|
||||||
|
continue; /* Ignorieren */
|
||||||
|
}
|
||||||
|
|
||||||
|
found_something = true;
|
||||||
|
break; /* Treffer! Schleife abbrechen. */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* WICHTIG: Das Verzeichnis SOFORT schließen, BEVOR wir rekurieren!
|
||||||
|
* Damit geben wir das File-Handle (NUM_DIRS) an Zephyr zurück. */
|
||||||
|
fs_closedir(&dir);
|
||||||
|
|
||||||
|
if (!found_something || rc != 0) {
|
||||||
|
break; /* Verzeichnis ist nun restlos leer */
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t name_len = strlen(entry.name);
|
||||||
|
if (orig_len + 1 + name_len >= max_len) {
|
||||||
|
rc = -ENAMETOOLONG;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pfad für das gefundene Kindelement bauen */
|
||||||
|
path_buf[orig_len] = '/';
|
||||||
|
strcpy(&path_buf[orig_len + 1], entry.name);
|
||||||
|
|
||||||
|
/* Rekursiver Aufruf für das Kind */
|
||||||
|
rc = fs_pm_rm_recursive(path_buf, max_len);
|
||||||
|
|
||||||
|
/* Puffer sofort wieder auf unser Verzeichnis zurückschneiden */
|
||||||
|
path_buf[orig_len] = '\0';
|
||||||
|
|
||||||
|
if (rc != 0) {
|
||||||
|
break; /* Abbruch, falls beim Löschen des Kindes ein Fehler auftrat */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 3. Das nun restlos leere Verzeichnis selbst löschen */
|
||||||
|
if (rc == 0) {
|
||||||
|
rc = fs_unlink(path_buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs_pm_flash_suspend();
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
int fs_pm_mkdir_recursive(char *path)
|
||||||
|
{
|
||||||
|
int rc = 0;
|
||||||
|
struct fs_dirent entry;
|
||||||
|
char *p = path;
|
||||||
|
|
||||||
|
/* Führenden Slash überspringen, falls vorhanden (z. B. bei "/lfs") */
|
||||||
|
if (*p == '/') {
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Flash für den gesamten Durchlauf aktivieren */
|
||||||
|
fs_pm_flash_resume();
|
||||||
|
|
||||||
|
while (*p != '\0') {
|
||||||
|
if (*p == '/') {
|
||||||
|
*p = '\0'; /* String temporär am aktuellen Slash terminieren */
|
||||||
|
|
||||||
|
/* Prüfen, ob dieser Pfadabschnitt bereits existiert */
|
||||||
|
rc = fs_stat(path, &entry);
|
||||||
|
|
||||||
|
if (rc == -ENOENT) {
|
||||||
|
/* Existiert nicht -> anlegen */
|
||||||
|
rc = fs_mkdir(path);
|
||||||
|
if (rc != 0) {
|
||||||
|
*p = '/'; /* Bei Fehler Slash wiederherstellen und abbrechen */
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (rc == 0) {
|
||||||
|
/* Existiert -> prüfen, ob es ein Verzeichnis ist */
|
||||||
|
if (entry.type != FS_DIR_ENTRY_DIR) {
|
||||||
|
rc = -ENOTDIR;
|
||||||
|
*p = '/';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/* Anderer Dateisystemfehler */
|
||||||
|
*p = '/';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = '/'; /* Slash für den nächsten Schleifendurchlauf wiederherstellen */
|
||||||
|
}
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Letztes Element verarbeiten, falls der Pfad nicht mit '/' endet */
|
||||||
|
if (rc == 0 && p > path && *(p - 1) != '/') {
|
||||||
|
rc = fs_stat(path, &entry);
|
||||||
|
if (rc == -ENOENT) {
|
||||||
|
rc = fs_mkdir(path);
|
||||||
|
} else if (rc == 0) {
|
||||||
|
if (entry.type != FS_DIR_ENTRY_DIR) {
|
||||||
|
rc = -ENOTDIR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Flash am Ende wieder in den Suspend schicken */
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
tag_footer_t footer;
|
||||||
|
|
||||||
|
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)sizeof(tag_footer_t)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Den 8-Byte-Footer direkt in das Struct einlesen */
|
||||||
|
fs_seek(fp, -(off_t)sizeof(tag_footer_t), FS_SEEK_END);
|
||||||
|
if (fs_read(fp, &footer, sizeof(tag_footer_t)) != sizeof(tag_footer_t)) {
|
||||||
|
fs_seek(fp, 0, FS_SEEK_SET);
|
||||||
|
return -EIO;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 1. Signatur prüfen */
|
||||||
|
if (memcmp(footer.magic, TAG_MAGIC, 4) != 0) {
|
||||||
|
fs_seek(fp, 0, FS_SEEK_SET);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 2. Endianness konvertieren */
|
||||||
|
uint16_t tag_version = sys_le16_to_cpu(footer.version);
|
||||||
|
uint16_t tag_len = sys_le16_to_cpu(footer.total_size);
|
||||||
|
|
||||||
|
/* 3. Version und Größe validieren */
|
||||||
|
if (tag_version != TAG_FORMAT_VERSION) {
|
||||||
|
fs_seek(fp, 0, FS_SEEK_SET);
|
||||||
|
return -ENOTSUP;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tag_len > (uint16_t)file_size || tag_len < sizeof(tag_footer_t)) {
|
||||||
|
fs_seek(fp, 0, FS_SEEK_SET);
|
||||||
|
return -EBADMSG;
|
||||||
|
}
|
||||||
|
|
||||||
|
*has_tag = true;
|
||||||
|
*audio_limit = (size_t)file_size - tag_len;
|
||||||
|
*payload_len = tag_len - sizeof(tag_footer_t);
|
||||||
|
|
||||||
|
fs_seek(fp, 0, FS_SEEK_SET);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
ssize_t fs_get_audio_data_len(struct fs_file_t *fp) {
|
ssize_t fs_get_audio_data_len(struct fs_file_t *fp) {
|
||||||
uint8_t footer[6];
|
|
||||||
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);
|
||||||
|
|
||||||
fs_seek(fp, 0, FS_SEEK_SET);
|
if (file_size < 0) {
|
||||||
if (file_size < 6) return file_size;
|
fs_seek(fp, 0, FS_SEEK_SET);
|
||||||
|
return -EIO;
|
||||||
fs_seek(fp, -6, FS_SEEK_END);
|
|
||||||
if (fs_read(fp, footer, 6) == 6) {
|
|
||||||
if (memcmp(&footer[2], "TAG!", 4) == 0) {
|
|
||||||
uint16_t tag_len = footer[0] | (footer[1] << 8);
|
|
||||||
if (tag_len <= file_size) {
|
|
||||||
fs_seek(fp, 0, FS_SEEK_SET);
|
|
||||||
return file_size - tag_len;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fs_get_tag_bounds(fp, file_size, &audio_limit, &payload_len, &has_tag) < 0) {
|
||||||
|
fs_seek(fp, 0, FS_SEEK_SET);
|
||||||
|
return -EIO;
|
||||||
|
}
|
||||||
|
|
||||||
fs_seek(fp, 0, FS_SEEK_SET);
|
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 +445,42 @@ 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);
|
return -EIO;
|
||||||
|
|
||||||
// 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)
|
int rc = fs_get_tag_bounds(fp, file_size, &audio_limit, &payload_size, &has_tag);
|
||||||
size_t payload_len = file_size - audio_limit - 6;
|
if (rc < 0) {
|
||||||
|
return rc;
|
||||||
if ((payload_len * 2U) + 1U > hex_str_size) {
|
}
|
||||||
return -ENOMEM; // Nicht genug Platz im Zielpuffer
|
|
||||||
|
if (!has_tag) {
|
||||||
|
return -ENOENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Zum Anfang des Payloads springen
|
|
||||||
fs_seek(fp, audio_limit, FS_SEEK_SET);
|
fs_seek(fp, audio_limit, FS_SEEK_SET);
|
||||||
|
*version = TAG_FORMAT_VERSION;
|
||||||
uint8_t byte;
|
*payload_len = payload_size;
|
||||||
for (size_t i = 0; i < payload_len; i++) {
|
|
||||||
if (fs_read(fp, &byte, 1) != 1) {
|
|
||||||
return -EIO;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Jedes Byte als zwei Hex-Zeichen in den Zielpuffer schreiben
|
|
||||||
hex_str[i * 2] = int2hex(byte >> 4);
|
|
||||||
hex_str[i * 2 + 1] = int2hex(byte & 0x0F);
|
|
||||||
}
|
|
||||||
|
|
||||||
hex_str[payload_len * 2] = '\0';
|
|
||||||
|
|
||||||
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 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;
|
||||||
@@ -342,3 +533,187 @@ int flash_write_firmware_block(const uint8_t *buffer, size_t length, bool is_las
|
|||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
size_t fs_get_external_flash_page_size(void) {
|
||||||
|
const struct flash_area *fa;
|
||||||
|
const struct device *dev;
|
||||||
|
struct flash_pages_info info;
|
||||||
|
int rc;
|
||||||
|
|
||||||
|
rc = flash_area_open(STORAGE_PARTITION_ID, &fa);
|
||||||
|
if (rc != 0) {
|
||||||
|
LOG_ERR("Failed to open flash area for page size retrieval");
|
||||||
|
return 256; // Fallback to a common page size
|
||||||
|
}
|
||||||
|
|
||||||
|
dev = flash_area_get_device(fa);
|
||||||
|
if (dev == NULL) {
|
||||||
|
flash_area_close(fa);
|
||||||
|
LOG_ERR("Failed to get flash device for page size retrieval");
|
||||||
|
return 256; // Fallback to a common page size
|
||||||
|
}
|
||||||
|
|
||||||
|
rc = flash_get_page_info_by_offs(dev, fa->fa_off, &info);
|
||||||
|
flash_area_close(fa);
|
||||||
|
|
||||||
|
if (rc != 0) {
|
||||||
|
LOG_ERR("Failed to get flash page info: %d", rc);
|
||||||
|
return 256; // Fallback to a common page size
|
||||||
|
}
|
||||||
|
|
||||||
|
return info.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t fs_get_fw_slot_size(void) {
|
||||||
|
const struct flash_area *fa;
|
||||||
|
int rc;
|
||||||
|
|
||||||
|
rc = flash_area_open(SLOT1_ID, &fa);
|
||||||
|
if (rc != 0) {
|
||||||
|
LOG_ERR("Failed to open flash area for slot size retrieval");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t slot_size = fa->fa_size;
|
||||||
|
flash_area_close(fa);
|
||||||
|
return slot_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t fs_get_internal_flash_page_size(void) {
|
||||||
|
const struct flash_area *fa;
|
||||||
|
const struct device *dev;
|
||||||
|
struct flash_pages_info info;
|
||||||
|
int rc;
|
||||||
|
|
||||||
|
rc = flash_area_open(SLOT1_ID, &fa);
|
||||||
|
if (rc != 0) {
|
||||||
|
LOG_ERR("Failed to open flash area for page size retrieval");
|
||||||
|
return 256; // Fallback to a common page size
|
||||||
|
}
|
||||||
|
|
||||||
|
dev = flash_area_get_device(fa);
|
||||||
|
if (dev == NULL) {
|
||||||
|
flash_area_close(fa);
|
||||||
|
LOG_ERR("Failed to get flash device for page size retrieval");
|
||||||
|
return 256; // Fallback to a common page size
|
||||||
|
}
|
||||||
|
|
||||||
|
rc = flash_get_page_info_by_offs(dev, fa->fa_off, &info);
|
||||||
|
flash_area_close(fa);
|
||||||
|
|
||||||
|
if (rc != 0) {
|
||||||
|
LOG_ERR("Failed to get flash page info: %d", rc);
|
||||||
|
return 256; // Fallback to a common page size
|
||||||
|
}
|
||||||
|
|
||||||
|
return info.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
void fs_reset_transfer_sync(void)
|
||||||
|
{
|
||||||
|
k_sem_reset(&fs_transfer_done_sem);
|
||||||
|
}
|
||||||
|
|
||||||
|
void fs_wait_for_transfer_complete(void)
|
||||||
|
{
|
||||||
|
k_sem_take(&fs_transfer_done_sem, K_FOREVER);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void fs_thread_entry(void *p1, void *p2, void *p3)
|
||||||
|
{
|
||||||
|
ARG_UNUSED(p1);
|
||||||
|
ARG_UNUSED(p2);
|
||||||
|
ARG_UNUSED(p3);
|
||||||
|
|
||||||
|
LOG_INF("Filesystem thread started");
|
||||||
|
fs_thread_state_t state = FS_STATE_IDLE;
|
||||||
|
fs_msg_t msg;
|
||||||
|
struct fs_file_t current_file;
|
||||||
|
fs_file_t_init(¤t_file);
|
||||||
|
char current_filename[MAX_PATH_LEN] = {0};
|
||||||
|
|
||||||
|
while (1)
|
||||||
|
{
|
||||||
|
k_timeout_t wait_time = (state == FS_STATE_IDLE) ? K_FOREVER : K_SECONDS(1);
|
||||||
|
int rc = k_msgq_get(&fs_msgq, &msg, wait_time);
|
||||||
|
|
||||||
|
if (rc == -EAGAIN)
|
||||||
|
{
|
||||||
|
if (state == FS_STATE_RECEIVING)
|
||||||
|
{
|
||||||
|
LOG_WRN("FS Transfer Timeout. Aborting and dropping file.");
|
||||||
|
fs_pm_close(¤t_file);
|
||||||
|
fs_pm_unlink(current_filename);
|
||||||
|
state = FS_STATE_IDLE;
|
||||||
|
k_sem_give(&fs_transfer_done_sem);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (state)
|
||||||
|
{
|
||||||
|
case FS_STATE_IDLE:
|
||||||
|
if (msg.type == FS_MSG_START)
|
||||||
|
{
|
||||||
|
strncpy(current_filename, msg.start.filename, MAX_PATH_LEN - 1);
|
||||||
|
current_filename[MAX_PATH_LEN - 1] = '\0';
|
||||||
|
|
||||||
|
/* Bei Position 0 (Neuer Datei-Upload) die alte Datei restlos löschen */
|
||||||
|
if (msg.start.start_position == 0) {
|
||||||
|
fs_pm_unlink(current_filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
rc = fs_pm_open(¤t_file, current_filename, FS_O_CREATE | FS_O_WRITE);
|
||||||
|
if (rc == 0) {
|
||||||
|
if (msg.start.start_position > 0) {
|
||||||
|
fs_seek(¤t_file, msg.start.start_position, FS_SEEK_SET);
|
||||||
|
}
|
||||||
|
state = FS_STATE_RECEIVING;
|
||||||
|
} else {
|
||||||
|
LOG_ERR("Failed to open %s: %d", current_filename, rc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (msg.type == FS_MSG_CHUNK)
|
||||||
|
{
|
||||||
|
/* Chunks im IDLE-Status (z.B. nach Fehler) direkt verwerfen */
|
||||||
|
if (msg.chunk.slab_ptr != NULL)
|
||||||
|
{
|
||||||
|
k_mem_slab_free(&file_buffer_slab, msg.chunk.slab_ptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (msg.type == FS_MSG_EOF || msg.type == FS_MSG_ABORT)
|
||||||
|
{
|
||||||
|
/* Verhindert Deadlocks, falls das Öffnen fehlgeschlagen war */
|
||||||
|
k_sem_give(&fs_transfer_done_sem);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FS_STATE_RECEIVING:
|
||||||
|
if (msg.type == FS_MSG_CHUNK)
|
||||||
|
{
|
||||||
|
if (msg.chunk.slab_ptr != NULL)
|
||||||
|
{
|
||||||
|
fs_write(¤t_file, msg.chunk.slab_ptr, msg.chunk.chunk_size);
|
||||||
|
k_mem_slab_free(&file_buffer_slab, msg.chunk.slab_ptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (msg.type == FS_MSG_EOF)
|
||||||
|
{
|
||||||
|
fs_pm_close(¤t_file);
|
||||||
|
state = FS_STATE_IDLE;
|
||||||
|
k_sem_give(&fs_transfer_done_sem);
|
||||||
|
}
|
||||||
|
else if (msg.type == FS_MSG_ABORT)
|
||||||
|
{
|
||||||
|
fs_pm_close(¤t_file);
|
||||||
|
fs_pm_unlink(current_filename);
|
||||||
|
state = FS_STATE_IDLE;
|
||||||
|
k_sem_give(&fs_transfer_done_sem);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
K_THREAD_DEFINE(fs, FS_THREAD_STACK_SIZE, fs_thread_entry,
|
||||||
|
NULL, NULL, NULL, FS_THREAD_PRIORITY, 0, 0);
|
||||||
@@ -15,6 +15,8 @@
|
|||||||
#include <io.h>
|
#include <io.h>
|
||||||
#include <usb.h>
|
#include <usb.h>
|
||||||
#include <utils.h>
|
#include <utils.h>
|
||||||
|
#include <uart.h>
|
||||||
|
#include <settings.h>
|
||||||
|
|
||||||
LOG_MODULE_REGISTER(main, LOG_LEVEL_INF);
|
LOG_MODULE_REGISTER(main, LOG_LEVEL_INF);
|
||||||
|
|
||||||
@@ -38,6 +40,13 @@ int main(void)
|
|||||||
|
|
||||||
int rc;
|
int rc;
|
||||||
|
|
||||||
|
rc = app_settings_init();
|
||||||
|
if (rc < 0)
|
||||||
|
{
|
||||||
|
LOG_ERR("Settings initialization failed: %d", rc);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
rc = fs_init();
|
rc = fs_init();
|
||||||
if (rc < 0)
|
if (rc < 0)
|
||||||
{
|
{
|
||||||
@@ -52,13 +61,21 @@ int main(void)
|
|||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
rc = usb_cdc_acm_init();
|
rc = usb_init();
|
||||||
if (rc < 0)
|
if (rc < 0)
|
||||||
{
|
{
|
||||||
LOG_ERR("USB initialization failed: %d", rc);
|
LOG_ERR("USB initialization failed: %d", rc);
|
||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rc = uart_init();
|
||||||
|
if (rc < 0)
|
||||||
|
{
|
||||||
|
LOG_ERR("UART initialization failed: %d", rc);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
rc = io_init();
|
rc = io_init();
|
||||||
if (rc < 0)
|
if (rc < 0)
|
||||||
{
|
{
|
||||||
@@ -81,6 +98,7 @@ int main(void)
|
|||||||
{
|
{
|
||||||
LOG_INF("Firmware image already confirmed. No need to confirm again.");
|
LOG_INF("Firmware image already confirmed. No need to confirm again.");
|
||||||
}
|
}
|
||||||
|
|
||||||
while (1)
|
while (1)
|
||||||
{
|
{
|
||||||
k_sleep(K_FOREVER);
|
k_sleep(K_FOREVER);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,50 +0,0 @@
|
|||||||
#ifndef PROTOCOL_H
|
|
||||||
#define PROTOCOL_H
|
|
||||||
|
|
||||||
typedef enum {
|
|
||||||
PS_WAITING_FOR_COMMAND,
|
|
||||||
PS_READING_COMMAND,
|
|
||||||
PS_READING_PARAMETERS,
|
|
||||||
PS_WAITING_FOR_END_OF_LINE,
|
|
||||||
} protocol_state_t;
|
|
||||||
|
|
||||||
typedef enum {
|
|
||||||
CMD_INVALID = 0,
|
|
||||||
CMD_INFO,
|
|
||||||
CMD_LS,
|
|
||||||
CMD_PUT_BINARY_FILE,
|
|
||||||
CMD_MKDIR,
|
|
||||||
CMD_RM,
|
|
||||||
CMD_CONFIRM,
|
|
||||||
CMD_REBOOT,
|
|
||||||
CMD_PLAY,
|
|
||||||
CMD_SET_TAG,
|
|
||||||
CMD_GET_TAG,
|
|
||||||
CMD_CHECK,
|
|
||||||
/* Weitere Kommandos folgen hier */
|
|
||||||
} protocol_cmd_t;
|
|
||||||
|
|
||||||
typedef enum {
|
|
||||||
P_ERR_NONE = 0x00,
|
|
||||||
P_ERR_INVALID_COMMAND = 0x01,
|
|
||||||
P_ERR_INVALID_PARAMETERS = 0x02,
|
|
||||||
P_ERR_COMMAND_TOO_LONG = 0x03,
|
|
||||||
|
|
||||||
P_ERR_FILE_NOT_FOUND = 0x10,
|
|
||||||
P_ERR_ALREADY_EXISTS = 0x11,
|
|
||||||
P_ERR_NOT_A_DIRECTORY = 0x12,
|
|
||||||
P_ERR_IS_A_DIRECTORY = 0x13,
|
|
||||||
P_ERR_ACCESS_DENIED = 0x14,
|
|
||||||
P_ERR_NO_SPACE = 0x15,
|
|
||||||
P_ERR_FILE_TOO_LARGE = 0x16,
|
|
||||||
|
|
||||||
P_ERR_IO = 0x20,
|
|
||||||
P_ERR_TIMEOUT = 0x21,
|
|
||||||
P_ERR_CRC_MISMATCH = 0x22,
|
|
||||||
P_ERR_TRANSFER_ABORTED = 0x23,
|
|
||||||
|
|
||||||
P_ERR_NOT_SUPPORTED = 0x30,
|
|
||||||
P_ERR_BUSY = 0x31,
|
|
||||||
P_ERR_INTERNAL = 0x32,
|
|
||||||
} protocol_error_t;
|
|
||||||
#endif // PROTOCOL_H
|
|
||||||
1195
firmware/src/protocol_old.c
Normal file
1195
firmware/src/protocol_old.c
Normal file
File diff suppressed because it is too large
Load Diff
166
firmware/src/settings.c
Normal file
166
firmware/src/settings.c
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
#include "settings.h"
|
||||||
|
#include <zephyr/kernel.h>
|
||||||
|
#include <zephyr/settings/settings.h>
|
||||||
|
#include <zephyr/logging/log.h>
|
||||||
|
|
||||||
|
LOG_MODULE_REGISTER(app_settings, LOG_LEVEL_DBG);
|
||||||
|
|
||||||
|
/* Initialisierung mit Standardwerten als Fallback */
|
||||||
|
app_settings_t app_settings = {
|
||||||
|
.audio_vol = 50,
|
||||||
|
.play_norepeat = false,
|
||||||
|
.storage_interval_s = 3600
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Flags zur Markierung ungespeicherter Änderungen */
|
||||||
|
static bool dirty_audio_vol = false;
|
||||||
|
static bool dirty_play_norepeat = false;
|
||||||
|
static bool dirty_storage_interval = false;
|
||||||
|
|
||||||
|
/* Workqueue-Objekt für das asynchrone Speichern */
|
||||||
|
static struct k_work_delayable save_work;
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* Lese-Handler für Zephyr (Aufruf beim Booten durch settings_load) */
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static int audio_settings_set(const char *name, size_t len, settings_read_cb read_cb, void *cb_arg)
|
||||||
|
{
|
||||||
|
if (settings_name_steq(name, "vol", NULL)) {
|
||||||
|
return read_cb(cb_arg, &app_settings.audio_vol, sizeof(app_settings.audio_vol));
|
||||||
|
}
|
||||||
|
return -ENOENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int play_settings_set(const char *name, size_t len, settings_read_cb read_cb, void *cb_arg)
|
||||||
|
{
|
||||||
|
if (settings_name_steq(name, "norepeat", NULL)) {
|
||||||
|
uint8_t val = 0;
|
||||||
|
int rc = read_cb(cb_arg, &val, sizeof(val));
|
||||||
|
if (rc >= 0) {
|
||||||
|
app_settings.play_norepeat = (val > 0);
|
||||||
|
}
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
return -ENOENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int sys_settings_set(const char *name, size_t len, settings_read_cb read_cb, void *cb_arg)
|
||||||
|
{
|
||||||
|
if (settings_name_steq(name, "storage_interval", NULL)) {
|
||||||
|
return read_cb(cb_arg, &app_settings.storage_interval_s, sizeof(app_settings.storage_interval_s));
|
||||||
|
}
|
||||||
|
return -ENOENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Registrierung der Namespaces für das automatische Laden */
|
||||||
|
SETTINGS_STATIC_HANDLER_DEFINE(audio, "audio", NULL, audio_settings_set, NULL, NULL);
|
||||||
|
SETTINGS_STATIC_HANDLER_DEFINE(play, "play", NULL, play_settings_set, NULL, NULL);
|
||||||
|
SETTINGS_STATIC_HANDLER_DEFINE(sys, "settings", NULL, sys_settings_set, NULL, NULL);
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* Schreib-Logik (Asynchron über System Workqueue) */
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static void save_work_handler(struct k_work *work)
|
||||||
|
{
|
||||||
|
if (dirty_audio_vol) {
|
||||||
|
settings_save_one("audio/vol", &app_settings.audio_vol, sizeof(app_settings.audio_vol));
|
||||||
|
dirty_audio_vol = false;
|
||||||
|
LOG_DBG("NVS Write: audio/vol = %d", app_settings.audio_vol);
|
||||||
|
}
|
||||||
|
if (dirty_play_norepeat) {
|
||||||
|
uint8_t val = app_settings.play_norepeat ? 1 : 0;
|
||||||
|
settings_save_one("play/norepeat", &val, sizeof(val));
|
||||||
|
dirty_play_norepeat = false;
|
||||||
|
LOG_DBG("NVS Write: play/norepeat = %d", val);
|
||||||
|
}
|
||||||
|
if (dirty_storage_interval) {
|
||||||
|
settings_save_one("settings/storage_interval", &app_settings.storage_interval_s, sizeof(app_settings.storage_interval_s));
|
||||||
|
dirty_storage_interval = false;
|
||||||
|
LOG_DBG("NVS Write: settings/storage_interval = %d", app_settings.storage_interval_s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void schedule_save(void)
|
||||||
|
{
|
||||||
|
if (app_settings.storage_interval_s == 0) {
|
||||||
|
/* Direkter Schreibvorgang, Work sofort einreihen */
|
||||||
|
k_work_cancel_delayable(&save_work);
|
||||||
|
k_work_submit(&save_work.work);
|
||||||
|
} else {
|
||||||
|
/* Timer neustarten (überschreibt laufenden Countdown) */
|
||||||
|
k_work_reschedule(&save_work, K_SECONDS(app_settings.storage_interval_s));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void app_settings_save_pending_now(void)
|
||||||
|
{
|
||||||
|
struct k_work_sync sync;
|
||||||
|
k_work_cancel_delayable_sync(&save_work, &sync);
|
||||||
|
save_work_handler(&save_work.work);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* Setter (API für das Protokoll) */
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
void app_settings_set_audio_vol(uint8_t vol)
|
||||||
|
{
|
||||||
|
if (vol > 100) vol = 100;
|
||||||
|
|
||||||
|
if (app_settings.audio_vol != vol) {
|
||||||
|
app_settings.audio_vol = vol;
|
||||||
|
dirty_audio_vol = true;
|
||||||
|
schedule_save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void app_settings_set_play_norepeat(bool norepeat)
|
||||||
|
{
|
||||||
|
if (app_settings.play_norepeat != norepeat) {
|
||||||
|
app_settings.play_norepeat = norepeat;
|
||||||
|
dirty_play_norepeat = true;
|
||||||
|
schedule_save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void app_settings_set_storage_interval(uint32_t interval_s)
|
||||||
|
{
|
||||||
|
if (interval_s > 7200) interval_s = 7200;
|
||||||
|
|
||||||
|
if (app_settings.storage_interval_s != interval_s) {
|
||||||
|
app_settings.storage_interval_s = interval_s;
|
||||||
|
dirty_storage_interval = true;
|
||||||
|
schedule_save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* Initialisierung */
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
int app_settings_init(void)
|
||||||
|
{
|
||||||
|
int err;
|
||||||
|
|
||||||
|
k_work_init_delayable(&save_work, save_work_handler);
|
||||||
|
|
||||||
|
err = settings_subsys_init();
|
||||||
|
if (err) {
|
||||||
|
LOG_ERR("settings_subsys_init failed (err %d)", err);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Lädt alle Werte aus dem NVS in den RAM */
|
||||||
|
err = settings_load();
|
||||||
|
if (err) {
|
||||||
|
LOG_ERR("settings_load failed (err %d)", err);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INF("Settings init ok. Vol=%d, NoRepeat=%d, Interval=%d",
|
||||||
|
app_settings.audio_vol, app_settings.play_norepeat, app_settings.storage_interval_s);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
135
firmware/src/uart.c
Normal file
135
firmware/src/uart.c
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
// uart.c
|
||||||
|
#include <zephyr/kernel.h>
|
||||||
|
#include <zephyr/logging/log.h>
|
||||||
|
#include <zephyr/drivers/uart.h>
|
||||||
|
#include <zephyr/sys/ring_buffer.h>
|
||||||
|
|
||||||
|
LOG_MODULE_REGISTER(uart, LOG_LEVEL_INF);
|
||||||
|
|
||||||
|
#define RX_RING_BUF_SIZE 1024
|
||||||
|
#define TX_RING_BUF_SIZE 1024
|
||||||
|
|
||||||
|
const struct device *const uart_dev = DEVICE_DT_GET_ONE(zephyr_cdc_acm_uart);
|
||||||
|
|
||||||
|
RING_BUF_ITEM_DECLARE(rx_ringbuf, RX_RING_BUF_SIZE);
|
||||||
|
RING_BUF_ITEM_DECLARE(tx_ringbuf, TX_RING_BUF_SIZE);
|
||||||
|
K_SEM_DEFINE(tx_done_sem, 0, 1);
|
||||||
|
K_SEM_DEFINE(rx_ready_sem, 0, 1);
|
||||||
|
|
||||||
|
static void uart_isr(const struct device *dev, void *user_data)
|
||||||
|
{
|
||||||
|
ARG_UNUSED(user_data);
|
||||||
|
|
||||||
|
if (!uart_irq_update(dev))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uart_irq_rx_ready(dev))
|
||||||
|
{
|
||||||
|
uint8_t *data_ptr;
|
||||||
|
uint32_t claimed_len;
|
||||||
|
int recv_len;
|
||||||
|
|
||||||
|
claimed_len = ring_buf_put_claim(&rx_ringbuf, &data_ptr, RX_RING_BUF_SIZE);
|
||||||
|
|
||||||
|
if (claimed_len > 0)
|
||||||
|
{
|
||||||
|
recv_len = uart_fifo_read(dev, data_ptr, claimed_len);
|
||||||
|
ring_buf_put_finish(&rx_ringbuf, recv_len);
|
||||||
|
if (recv_len > 0)
|
||||||
|
{
|
||||||
|
k_sem_give(&rx_ready_sem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
uart_irq_rx_disable(dev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (uart_irq_tx_ready(dev))
|
||||||
|
{
|
||||||
|
uint8_t *data_ptr;
|
||||||
|
uint32_t claim_len;
|
||||||
|
int written;
|
||||||
|
|
||||||
|
claim_len = ring_buf_get_claim(&tx_ringbuf, &data_ptr, ring_buf_size_get(&tx_ringbuf));
|
||||||
|
|
||||||
|
if (claim_len > 0)
|
||||||
|
{
|
||||||
|
written = uart_fifo_fill(dev, data_ptr, claim_len);
|
||||||
|
ring_buf_get_finish(&tx_ringbuf, written);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
uart_irq_tx_disable(dev);
|
||||||
|
}
|
||||||
|
k_sem_give(&tx_done_sem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int uart_init(void)
|
||||||
|
{
|
||||||
|
if (!device_is_ready(uart_dev))
|
||||||
|
{
|
||||||
|
LOG_ERR("UART device not ready");
|
||||||
|
return -ENODEV;
|
||||||
|
}
|
||||||
|
|
||||||
|
uart_irq_callback_set(uart_dev, uart_isr);
|
||||||
|
uart_irq_rx_enable(uart_dev);
|
||||||
|
|
||||||
|
LOG_INF("UART device initialized");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int uart_write(const uint8_t *data, size_t len, k_timeout_t timeout)
|
||||||
|
{
|
||||||
|
size_t written_total = 0;
|
||||||
|
k_sem_reset(&tx_done_sem);
|
||||||
|
|
||||||
|
while (written_total < len)
|
||||||
|
{
|
||||||
|
uint32_t written = ring_buf_put(&tx_ringbuf, &data[written_total], len - written_total);
|
||||||
|
written_total += written;
|
||||||
|
|
||||||
|
if (written > 0)
|
||||||
|
{
|
||||||
|
uart_irq_tx_enable(uart_dev);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (written_total < len)
|
||||||
|
{
|
||||||
|
int ret = k_sem_take(&tx_done_sem, timeout);
|
||||||
|
if (ret != 0)
|
||||||
|
{
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return written_total;
|
||||||
|
}
|
||||||
|
|
||||||
|
int uart_write_string(const char *str, k_timeout_t timeout)
|
||||||
|
{
|
||||||
|
return uart_write((const uint8_t *)str, strlen(str), timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
int uart_read(uint8_t *data, size_t len, k_timeout_t timeout)
|
||||||
|
{
|
||||||
|
uint32_t read_len = ring_buf_get(&rx_ringbuf, data, len);
|
||||||
|
|
||||||
|
if (read_len == 0 && !K_TIMEOUT_EQ(timeout, K_NO_WAIT)) {
|
||||||
|
k_sem_reset(&rx_ready_sem);
|
||||||
|
if (ring_buf_is_empty(&rx_ringbuf)) {
|
||||||
|
if (k_sem_take(&rx_ready_sem, timeout) != 0) {
|
||||||
|
return -ETIMEDOUT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
read_len = ring_buf_get(&rx_ringbuf, data, len);
|
||||||
|
}
|
||||||
|
if (read_len > 0) {
|
||||||
|
uart_irq_rx_enable(uart_dev);
|
||||||
|
}
|
||||||
|
return read_len;
|
||||||
|
}
|
||||||
@@ -1,209 +1,181 @@
|
|||||||
#include <zephyr/kernel.h>
|
#include <zephyr/device.h>
|
||||||
#include <zephyr/logging/log.h>
|
|
||||||
#include <zephyr/usb/usb_device.h>
|
|
||||||
#include <zephyr/drivers/uart.h>
|
#include <zephyr/drivers/uart.h>
|
||||||
#include <zephyr/sys/ring_buffer.h> /* NEU */
|
#include <errno.h>
|
||||||
|
#include <zephyr/logging/log.h>
|
||||||
|
#include <zephyr/usb/usbd.h>
|
||||||
|
|
||||||
#include <io.h>
|
#include "usb.h"
|
||||||
|
|
||||||
|
#define USB_MANUFACTURER_STRING "Iten Engineering"
|
||||||
|
#define USB_PRODUCT_STRING "Edis Buzzer"
|
||||||
|
#define USB_DEVICE_VID 0x1209
|
||||||
|
#define USB_DEVICE_PID 0xEDED
|
||||||
|
|
||||||
LOG_MODULE_REGISTER(usb, LOG_LEVEL_INF);
|
LOG_MODULE_REGISTER(usb, LOG_LEVEL_INF);
|
||||||
|
|
||||||
K_SEM_DEFINE(usb_rx_sem, 0, 1);
|
K_SEM_DEFINE(dtr_active_sem, 0, 1);
|
||||||
K_SEM_DEFINE(usb_tx_sem, 0, 1);
|
static uint32_t dtr_active = 0U;
|
||||||
|
|
||||||
#define UART_NODE DT_ALIAS(usb_uart)
|
USBD_DEVICE_DEFINE(cdc_acm_serial,
|
||||||
const struct device *cdc_dev = DEVICE_DT_GET(UART_NODE);
|
DEVICE_DT_GET(DT_NODELABEL(zephyr_udc0)),
|
||||||
|
USB_DEVICE_VID, USB_DEVICE_PID);
|
||||||
|
|
||||||
/* NEU: Ringbuffer für stabilen asynchronen USB-Empfang */
|
USBD_DESC_LANG_DEFINE(cdc_acm_lang);
|
||||||
#define RX_RING_BUF_SIZE 5*1024 /* 8 KB Ringpuffer für eingehende USB-Daten */
|
USBD_DESC_MANUFACTURER_DEFINE(cdc_acm_mfr, USB_MANUFACTURER_STRING);
|
||||||
RING_BUF_DECLARE(rx_ringbuf, RX_RING_BUF_SIZE);
|
USBD_DESC_PRODUCT_DEFINE(cdc_acm_product, USB_PRODUCT_STRING);
|
||||||
|
IF_ENABLED(CONFIG_HWINFO, (USBD_DESC_SERIAL_NUMBER_DEFINE(cdc_acm_sn)));
|
||||||
|
|
||||||
static void cdc_acm_irq_cb(const struct device *dev, void *user_data)
|
USBD_DESC_CONFIG_DEFINE(fs_cfg_desc, "FS Configuration");
|
||||||
|
USBD_DESC_CONFIG_DEFINE(hs_cfg_desc, "HS Configuration");
|
||||||
|
|
||||||
|
USBD_CONFIGURATION_DEFINE(fs_config, 0U, 125U, &fs_cfg_desc);
|
||||||
|
USBD_CONFIGURATION_DEFINE(hs_config, 0U, 125U, &hs_cfg_desc);
|
||||||
|
|
||||||
|
static void fix_code_triple(struct usbd_context *uds_ctx, enum usbd_speed speed)
|
||||||
{
|
{
|
||||||
ARG_UNUSED(user_data);
|
if (IS_ENABLED(CONFIG_USBD_CDC_ACM_CLASS) ||
|
||||||
|
IS_ENABLED(CONFIG_USBD_CDC_ECM_CLASS) ||
|
||||||
if (!uart_irq_update(dev)) {
|
IS_ENABLED(CONFIG_USBD_CDC_NCM_CLASS) ||
|
||||||
return;
|
IS_ENABLED(CONFIG_USBD_MIDI2_CLASS) ||
|
||||||
}
|
IS_ENABLED(CONFIG_USBD_AUDIO2_CLASS) ||
|
||||||
|
IS_ENABLED(CONFIG_USBD_VIDEO_CLASS)) {
|
||||||
if (uart_irq_rx_ready(dev)) {
|
usbd_device_set_code_triple(uds_ctx, speed,
|
||||||
uint8_t buffer[64];
|
USB_BCC_MISCELLANEOUS, 0x02, 0x01);
|
||||||
uint32_t space = ring_buf_space_get(&rx_ringbuf);
|
} else {
|
||||||
|
usbd_device_set_code_triple(uds_ctx, speed, 0, 0, 0);
|
||||||
if (space == 0) {
|
}
|
||||||
/* Backpressure anwenden: Ringpuffer ist voll.
|
|
||||||
Interrupt deaktivieren, damit Daten im HW-FIFO bleiben
|
|
||||||
und der USB-Stack den Host drosselt (NAK). */
|
|
||||||
uart_irq_rx_disable(dev);
|
|
||||||
} else {
|
|
||||||
/* Nur so viele Daten lesen, wie Platz im Ringpuffer ist */
|
|
||||||
int to_read = MIN(sizeof(buffer), space);
|
|
||||||
int len = uart_fifo_read(dev, buffer, to_read);
|
|
||||||
|
|
||||||
if (len > 0) {
|
|
||||||
ring_buf_put(&rx_ringbuf, buffer, len);
|
|
||||||
k_sem_give(&usb_rx_sem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uart_irq_tx_ready(dev)) {
|
|
||||||
uart_irq_tx_disable(dev);
|
|
||||||
k_sem_give(&usb_tx_sem);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool usb_wait_for_data(k_timeout_t timeout)
|
static void usbd_msg_cb(struct usbd_context *const ctx, const struct usbd_msg *const msg)
|
||||||
{
|
{
|
||||||
if (!ring_buf_is_empty(&rx_ringbuf)) {
|
int err;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Wenn der Puffer leer ist, sicherstellen, dass der RX-Interrupt
|
LOG_DBG("USBD message: %s", usbd_msg_type_string(msg->type));
|
||||||
aktiviert ist, da sonst keine neuen Daten empfangen werden können. */
|
|
||||||
if (device_is_ready(cdc_dev)) {
|
|
||||||
uart_irq_rx_enable(cdc_dev);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (k_sem_take(&usb_rx_sem, timeout) == 0);
|
if (usbd_can_detect_vbus(ctx)) {
|
||||||
|
if (msg->type == USBD_MSG_VBUS_READY) {
|
||||||
|
err = usbd_enable(ctx);
|
||||||
|
if (err) {
|
||||||
|
LOG_ERR("Failed to enable USB device (%d)", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg->type == USBD_MSG_VBUS_REMOVED) {
|
||||||
|
err = usbd_disable(ctx);
|
||||||
|
if (err) {
|
||||||
|
LOG_ERR("Failed to disable USB device (%d)", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg->type == USBD_MSG_CDC_ACM_CONTROL_LINE_STATE) {
|
||||||
|
uint32_t rts = 0U;
|
||||||
|
uint32_t dcd = 0U;
|
||||||
|
uint32_t dsr = 0U;
|
||||||
|
|
||||||
|
if (msg->dev != NULL) {
|
||||||
|
(void)uart_line_ctrl_get(msg->dev, UART_LINE_CTRL_RTS, &rts);
|
||||||
|
(void)uart_line_ctrl_get(msg->dev, UART_LINE_CTRL_DTR, &dtr_active);
|
||||||
|
(void)uart_line_ctrl_get(msg->dev, UART_LINE_CTRL_DCD, &dcd);
|
||||||
|
(void)uart_line_ctrl_get(msg->dev, UART_LINE_CTRL_DSR, &dsr);
|
||||||
|
LOG_DBG("CDC ACM RTS: %u, DTR: %u, DCD: %u, DSR: %u", rts, dtr_active, dcd, dsr);
|
||||||
|
if (dtr_active) {
|
||||||
|
k_sem_give(&dtr_active_sem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int usb_read_char(uint8_t *c)
|
int usb_init(void)
|
||||||
{
|
{
|
||||||
int ret = ring_buf_get(&rx_ringbuf, c, 1);
|
int err;
|
||||||
if (ret > 0 && device_is_ready(cdc_dev)) {
|
|
||||||
/* Platz geschaffen -> Empfang wieder aktivieren */
|
err = usbd_add_descriptor(&cdc_acm_serial, &cdc_acm_lang);
|
||||||
uart_irq_rx_enable(cdc_dev);
|
if (err) {
|
||||||
}
|
LOG_ERR("Failed to add language descriptor (%d)", err);
|
||||||
return ret;
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = usbd_add_descriptor(&cdc_acm_serial, &cdc_acm_mfr);
|
||||||
|
if (err) {
|
||||||
|
LOG_ERR("Failed to add manufacturer descriptor (%d)", err);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = usbd_add_descriptor(&cdc_acm_serial, &cdc_acm_product);
|
||||||
|
if (err) {
|
||||||
|
LOG_ERR("Failed to add product descriptor (%d)", err);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
IF_ENABLED(CONFIG_HWINFO, (
|
||||||
|
err = usbd_add_descriptor(&cdc_acm_serial, &cdc_acm_sn);
|
||||||
|
))
|
||||||
|
if (err) {
|
||||||
|
LOG_ERR("Failed to add serial-number descriptor (%d)", err);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (USBD_SUPPORTS_HIGH_SPEED && usbd_caps_speed(&cdc_acm_serial) == USBD_SPEED_HS) {
|
||||||
|
err = usbd_add_configuration(&cdc_acm_serial, USBD_SPEED_HS, &hs_config);
|
||||||
|
if (err) {
|
||||||
|
LOG_ERR("Failed to add HS configuration (%d)", err);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = usbd_register_class(&cdc_acm_serial, "cdc_acm_0", USBD_SPEED_HS, 1);
|
||||||
|
if (err) {
|
||||||
|
LOG_ERR("Failed to register HS CDC ACM class (%d)", err);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
fix_code_triple(&cdc_acm_serial, USBD_SPEED_HS);
|
||||||
|
}
|
||||||
|
|
||||||
|
err = usbd_add_configuration(&cdc_acm_serial, USBD_SPEED_FS, &fs_config);
|
||||||
|
if (err) {
|
||||||
|
LOG_ERR("Failed to add FS configuration (%d)", err);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = usbd_register_class(&cdc_acm_serial, "cdc_acm_0", USBD_SPEED_FS, 1);
|
||||||
|
if (err) {
|
||||||
|
LOG_ERR("Failed to register FS CDC ACM class (%d)", err);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
fix_code_triple(&cdc_acm_serial, USBD_SPEED_FS);
|
||||||
|
|
||||||
|
err = usbd_msg_register_cb(&cdc_acm_serial, usbd_msg_cb);
|
||||||
|
if (err) {
|
||||||
|
LOG_ERR("Failed to register USBD callback (%d)", err);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = usbd_init(&cdc_acm_serial);
|
||||||
|
if (err) {
|
||||||
|
LOG_ERR("Failed to initialize USBD (%d)", err);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!usbd_can_detect_vbus(&cdc_acm_serial)) {
|
||||||
|
err = usbd_enable(&cdc_acm_serial);
|
||||||
|
if (err) {
|
||||||
|
LOG_ERR("Failed to enable USBD (%d)", err);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INF("USBD CDC ACM initialized");
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int usb_read_buffer(uint8_t *buf, size_t max_len)
|
void usb_wait_for_dtr(void)
|
||||||
{
|
{
|
||||||
int ret = ring_buf_get(&rx_ringbuf, buf, max_len);
|
k_sem_take(&dtr_active_sem, K_FOREVER);
|
||||||
if (ret > 0 && device_is_ready(cdc_dev)) {
|
|
||||||
/* Platz geschaffen -> Empfang wieder aktivieren */
|
|
||||||
uart_irq_rx_enable(cdc_dev);
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void usb_resume_rx(void)
|
bool usb_dtr_active(void)
|
||||||
{
|
{
|
||||||
if (device_is_ready(cdc_dev)) {
|
return dtr_active != 0U;
|
||||||
uart_irq_rx_enable(cdc_dev);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void usb_write_char(uint8_t c)
|
|
||||||
{
|
|
||||||
if (!device_is_ready(cdc_dev)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
uart_poll_out(cdc_dev, c);
|
|
||||||
}
|
|
||||||
|
|
||||||
void usb_write_buffer(const uint8_t *buf, size_t len)
|
|
||||||
{
|
|
||||||
if (!device_is_ready(cdc_dev))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t written;
|
|
||||||
while (len > 0)
|
|
||||||
{
|
|
||||||
written = uart_fifo_fill(cdc_dev, buf, len);
|
|
||||||
|
|
||||||
len -= written;
|
|
||||||
buf += written;
|
|
||||||
|
|
||||||
uart_irq_tx_enable(cdc_dev);
|
|
||||||
|
|
||||||
if (len > 0)
|
|
||||||
{
|
|
||||||
|
|
||||||
if (k_sem_take(&usb_tx_sem, K_MSEC(100)) != 0)
|
|
||||||
{
|
|
||||||
LOG_WRN("USB TX timeout - consumer not reading?");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void usb_flush_rx(void)
|
|
||||||
{
|
|
||||||
uint8_t dummy;
|
|
||||||
if (!device_is_ready(cdc_dev)) return;
|
|
||||||
|
|
||||||
/* Hardware-FIFO leeren, falls Reste vorhanden */
|
|
||||||
while (uart_fifo_read(cdc_dev, &dummy, 1) > 0);
|
|
||||||
|
|
||||||
/* Ringpuffer und Semaphore zurücksetzen */
|
|
||||||
ring_buf_reset(&rx_ringbuf);
|
|
||||||
k_sem_reset(&usb_rx_sem);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void usb_status_cb(enum usb_dc_status_code cb_status, const uint8_t *param)
|
|
||||||
{
|
|
||||||
switch (cb_status) {
|
|
||||||
case USB_DC_CONNECTED:
|
|
||||||
/* VBUS wurde vom Zephyr-Stack erkannt */
|
|
||||||
LOG_DBG("VBUS detected, USB device connected");
|
|
||||||
break;
|
|
||||||
case USB_DC_CONFIGURED:
|
|
||||||
LOG_DBG("USB device configured by host");
|
|
||||||
io_usb_status(true);
|
|
||||||
if (device_is_ready(cdc_dev)) {
|
|
||||||
(void)uart_line_ctrl_set(cdc_dev, UART_LINE_CTRL_DCD, 1);
|
|
||||||
(void)uart_line_ctrl_set(cdc_dev, UART_LINE_CTRL_DSR, 1);
|
|
||||||
|
|
||||||
/* Interrupt-Handler binden und initial aktivieren */
|
|
||||||
uart_irq_callback_set(cdc_dev, cdc_acm_irq_cb);
|
|
||||||
uart_irq_rx_enable(cdc_dev);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case USB_DC_DISCONNECTED:
|
|
||||||
/* Kabel wurde gezogen */
|
|
||||||
LOG_DBG("VBUS removed, USB device disconnected");
|
|
||||||
if (device_is_ready(cdc_dev)) {
|
|
||||||
uart_irq_rx_disable(cdc_dev);
|
|
||||||
}
|
|
||||||
io_usb_status(false);
|
|
||||||
break;
|
|
||||||
case USB_DC_RESET:
|
|
||||||
LOG_DBG("USB bus reset");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int usb_cdc_acm_init(void)
|
|
||||||
{
|
|
||||||
LOG_DBG("Initializing USB Stack...");
|
|
||||||
|
|
||||||
/* Zephyr-Treiber registrieren. Verbraucht keinen Strom ohne VBUS. */
|
|
||||||
int ret = usb_enable(usb_status_cb);
|
|
||||||
if (ret != 0) {
|
|
||||||
LOG_ERR("Failed to enable USB (%d)", ret);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
#if DT_NODE_HAS_STATUS(DT_NODELABEL(cdc_acm_uart0), okay)
|
|
||||||
const struct device *cdc_dev = DEVICE_DT_GET(DT_NODELABEL(cdc_acm_uart0));
|
|
||||||
|
|
||||||
if (!device_is_ready(cdc_dev)) {
|
|
||||||
LOG_ERR("CDC ACM device not ready");
|
|
||||||
return -ENODEV;
|
|
||||||
}
|
|
||||||
|
|
||||||
#else
|
|
||||||
LOG_ERR("CDC ACM UART device not found in devicetree");
|
|
||||||
return -ENODEV;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
LOG_DBG("USB Stack enabled and waiting for VBUS in hardware");
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
#ifndef USB_CDC_ACM_H
|
|
||||||
#define USB_CDC_ACM_H
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Initializes the USB CDC ACM device
|
|
||||||
* @return 0 on success, negative error code on failure
|
|
||||||
*/
|
|
||||||
int usb_cdc_acm_init(void);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Waits until data is available in the USB RX FIFO or the timeout expires
|
|
||||||
* @param timeout Maximum time to wait for data. Use K_FOREVER for infinite wait.
|
|
||||||
* @return true if data is available, false if timeout occurred
|
|
||||||
*/
|
|
||||||
bool usb_wait_for_data(k_timeout_t timeout);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Reads a single character from the USB RX FIFO
|
|
||||||
* @param c Pointer to store the read character
|
|
||||||
* @return 1 if a character was read, 0 if no data was available
|
|
||||||
*/
|
|
||||||
int usb_read_char(uint8_t *c);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Reads a block of data from the USB RX FIFO
|
|
||||||
* @param buf Buffer to store the read data
|
|
||||||
* @param max_len Maximum number of bytes to read
|
|
||||||
* @return Number of bytes read
|
|
||||||
*/
|
|
||||||
int usb_read_buffer(uint8_t *buf, size_t max_len);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Resumes the USB RX interrupt when all data has been read
|
|
||||||
*/
|
|
||||||
void usb_resume_rx(void);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Writes a single character to the USB TX FIFO
|
|
||||||
* @param c Character to write
|
|
||||||
*/
|
|
||||||
void usb_write_char(uint8_t c);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Writes a block of data to the USB TX FIFO
|
|
||||||
* @param buf Buffer containing the data to write
|
|
||||||
* @param len Number of bytes to write
|
|
||||||
*/
|
|
||||||
void usb_write_buffer(const uint8_t *buf, size_t len);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Flushes the USB RX FIFO
|
|
||||||
*/
|
|
||||||
void usb_flush_rx(void);
|
|
||||||
|
|
||||||
#endif // USB_CDC_ACM_H
|
|
||||||
@@ -4,6 +4,8 @@
|
|||||||
#include <zephyr/logging/log_ctrl.h>
|
#include <zephyr/logging/log_ctrl.h>
|
||||||
#include <zephyr/sys/reboot.h>
|
#include <zephyr/sys/reboot.h>
|
||||||
|
|
||||||
|
#include <settings.h>
|
||||||
|
|
||||||
#if IS_ENABLED(CONFIG_SOC_SERIES_NRF52X)
|
#if IS_ENABLED(CONFIG_SOC_SERIES_NRF52X)
|
||||||
#include <hal/nrf_power.h>
|
#include <hal/nrf_power.h>
|
||||||
#elif IS_ENABLED(CONFIG_SOC_SERIES_STM32G0X)
|
#elif IS_ENABLED(CONFIG_SOC_SERIES_STM32G0X)
|
||||||
@@ -17,6 +19,7 @@ LOG_MODULE_REGISTER(utils, LOG_LEVEL_DBG);
|
|||||||
|
|
||||||
void reboot_with_status(uint8_t status)
|
void reboot_with_status(uint8_t status)
|
||||||
{
|
{
|
||||||
|
app_settings_save_pending_now();
|
||||||
#if IS_ENABLED(CONFIG_SOC_SERIES_NRF52X)
|
#if IS_ENABLED(CONFIG_SOC_SERIES_NRF52X)
|
||||||
/* Korrigierter Aufruf mit Register-Index 0 */
|
/* Korrigierter Aufruf mit Register-Index 0 */
|
||||||
nrf_power_gpregret_set(NRF_POWER, REBOOT_STATUS_REG_IDX, (uint32_t)status);
|
nrf_power_gpregret_set(NRF_POWER, REBOOT_STATUS_REG_IDX, (uint32_t)status);
|
||||||
@@ -51,15 +54,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);
|
|
||||||
}
|
|
||||||
BIN
sounds/sys/404
Normal file
BIN
sounds/sys/404
Normal file
Binary file not shown.
BIN
sounds/sys/confirm
Normal file
BIN
sounds/sys/confirm
Normal file
Binary file not shown.
BIN
sounds/sys/update
Normal file
BIN
sounds/sys/update
Normal file
Binary file not shown.
BIN
sounds/sys/voltest
Normal file
BIN
sounds/sys/voltest
Normal file
Binary file not shown.
274
tool/buzz.py
Normal file
274
tool/buzz.py
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
# # Falls buzz.py tief in Unterordnern liegt, stellen wir sicher,
|
||||||
|
# # dass das Hauptverzeichnis im Pfad ist:
|
||||||
|
# sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
def main():
|
||||||
|
try:
|
||||||
|
parser = argparse.ArgumentParser(description="Buzzer Serial Comm Tool")
|
||||||
|
|
||||||
|
# Allgemeine Parameter
|
||||||
|
parser.add_argument("-c", "--config", help="Pfad zur config.yaml (optional)", type=str)
|
||||||
|
parser.add_argument("-d", "--debug", help="Aktiviert detaillierte Hex-Logs", action="store_true")
|
||||||
|
|
||||||
|
# Verbindungsparameter (können auch in config.yaml definiert werden)
|
||||||
|
parser.add_argument("-p", "--port", help="Serieller Port", type=str)
|
||||||
|
parser.add_argument("-b", "--baud", help="Baudrate", type=int)
|
||||||
|
parser.add_argument("-t", "--timeout", help="Timeout in Sekunden", type=float)
|
||||||
|
|
||||||
|
# Subparser für Befehle
|
||||||
|
subparsers = parser.add_subparsers(dest="command", help="Verfügbare Befehle")
|
||||||
|
|
||||||
|
# Befehl: crc32
|
||||||
|
crc32_parser = subparsers.add_parser("crc32", help="CRC32-Checksumme einer Datei oder eines Verzeichnisses berechnen")
|
||||||
|
crc32_parser.add_argument("path", help="Pfad der Datei auf dem Zielsystem")
|
||||||
|
|
||||||
|
# Befehl: flash_info
|
||||||
|
flash_info_parser = subparsers.add_parser("flash_info", help="Informationen über den Flash-Speicher des Controllers abfragen")
|
||||||
|
|
||||||
|
# Befehl: fw_status
|
||||||
|
fw_status_parser = subparsers.add_parser("fw_status", help="Firmware- und Kernel-Status des Controllers abfragen")
|
||||||
|
|
||||||
|
# Befehl: get_file
|
||||||
|
get_file_parser = subparsers.add_parser("get_file", help="Datei vom Zielsystem herunterladen")
|
||||||
|
get_file_parser.add_argument("source_path", help="Pfad der Datei auf dem Zielsystem")
|
||||||
|
get_file_parser.add_argument("dest_path", help="Zielpfad auf dem lokalen System")
|
||||||
|
|
||||||
|
# Befehl: ls
|
||||||
|
ls_parser = subparsers.add_parser("ls", help="Listet Dateien/Ordner in einem Verzeichnis auf")
|
||||||
|
ls_parser.add_argument("path", help="Pfad auf dem Zielsystem")
|
||||||
|
ls_parser.add_argument("-r", "--recursive", help="Rekursiv durch die Verzeichnisse durchsuchen", action="store_true")
|
||||||
|
|
||||||
|
# Befehl: proto
|
||||||
|
proto_parser = subparsers.add_parser("proto", help="Protokollversion des Controllers abfragen")
|
||||||
|
|
||||||
|
# Befehl: put_file
|
||||||
|
put_file_parser = subparsers.add_parser("put_file", help="Datei auf das Zielsystem hochladen")
|
||||||
|
put_file_parser.add_argument("source_path", help="Pfad der Datei auf dem lokalen System")
|
||||||
|
put_file_parser.add_argument("dest_path", help="Zielpfad auf dem Zielsystem")
|
||||||
|
put_file_parser.add_argument("-t", "--tags", help="Optionale JSON Tags für den Upload", type=str)
|
||||||
|
|
||||||
|
# Befehl: get_tags
|
||||||
|
get_tags_parser = subparsers.add_parser("get_tags", help="Tags einer Datei anzeigen")
|
||||||
|
get_tags_parser.add_argument("path", help="Pfad der Datei auf dem Zielsystem")
|
||||||
|
|
||||||
|
# Befehl: put_tags
|
||||||
|
put_tags_parser = subparsers.add_parser("put_tags", help="Tags schreiben")
|
||||||
|
put_tags_parser.add_argument("path", help="Pfad der Datei auf dem Zielsystem")
|
||||||
|
put_tags_parser.add_argument("json", help="JSON String (z.B. '{\"json\": {\"t\": \"Titel\"}}')")
|
||||||
|
put_tags_parser.add_argument("-o", "--overwrite", help="Alle bestehenden JSON-Tags vorher löschen", action="store_true")
|
||||||
|
# Befehl: rename
|
||||||
|
rename_parser = subparsers.add_parser("rename", help="Benennen Sie eine Datei oder einen Ordner auf dem Zielsystem um")
|
||||||
|
rename_parser.add_argument("source_path", help="Aktueller Pfad der Datei/des Ordners auf dem Zielsystem")
|
||||||
|
rename_parser.add_argument("dest_path", help="Neuer Pfad der Datei/des Ordners auf dem Zielsystem")
|
||||||
|
|
||||||
|
# Befehl: rm
|
||||||
|
rm_parser = subparsers.add_parser("rm", help="Entfernt eine Datei oder einen Ordner auf dem Zielsystem")
|
||||||
|
rm_parser.add_argument("path", help="Pfad auf dem Zielsystem")
|
||||||
|
|
||||||
|
# Befehl: stat
|
||||||
|
stat_parser = subparsers.add_parser("stat", help="Informationen zu einer Datei/Ordner")
|
||||||
|
stat_parser.add_argument("path", help="Pfad auf dem Zielsystem")
|
||||||
|
|
||||||
|
# Befehl: put_fw
|
||||||
|
put_fw_parser = subparsers.add_parser("put_fw", help="Firmware-Image auf den Controller hochladen")
|
||||||
|
put_fw_parser.add_argument("file_path", help="Pfad zur Firmware-Datei auf dem lokalen System")
|
||||||
|
|
||||||
|
# Befehl: confirm_fw
|
||||||
|
confirm_fw_parser = subparsers.add_parser("confirm_fw", help="Bestätigt ein als 'Testing' markiertes Firmware-Image, damit es beim permanent wird")
|
||||||
|
|
||||||
|
# Befehl: reboot
|
||||||
|
reboot_parser = subparsers.add_parser("reboot", help="Neustart des Controllers")
|
||||||
|
|
||||||
|
# Befehl: play
|
||||||
|
play_parser = subparsers.add_parser("play", help="Startet die Wiedergabe einer Datei")
|
||||||
|
play_parser.add_argument("path", help="Pfad der Datei auf dem Zielsystem")
|
||||||
|
play_parser.add_argument("-i", "--interrupt", help="Sofortige Wiedergabe (Interrupt)", action="store_true")
|
||||||
|
|
||||||
|
# Befehl: stop
|
||||||
|
stop_parser = subparsers.add_parser("stop", help="Stoppt die aktuelle Wiedergabe")
|
||||||
|
|
||||||
|
# Befehl: set
|
||||||
|
set_parser = subparsers.add_parser("set", help="System-Einstellung setzen")
|
||||||
|
set_parser.add_argument("key", help="Schlüssel (z.B. audio/vol)")
|
||||||
|
set_parser.add_argument("value", help="Wert")
|
||||||
|
|
||||||
|
# Befehl: get
|
||||||
|
get_parser = subparsers.add_parser("get", help="System-Einstellung auslesen")
|
||||||
|
get_parser.add_argument("key", help="Schlüssel (z.B. audio/vol)")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not args.command:
|
||||||
|
parser.print_help()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
from core.config import cfg
|
||||||
|
from core.utils import console, console_err
|
||||||
|
|
||||||
|
if args.config:
|
||||||
|
cfg.custom_path = args.config
|
||||||
|
|
||||||
|
if args.debug:
|
||||||
|
cfg.debug = True
|
||||||
|
|
||||||
|
console.print("[bold blue]Buzzer Tool v1.0[/bold blue]", justify="left")
|
||||||
|
|
||||||
|
settings = cfg.serial_settings
|
||||||
|
|
||||||
|
settings['debug'] = args.debug
|
||||||
|
|
||||||
|
# Überschreibe Einstellungen mit Kommandozeilenparametern, falls vorhanden
|
||||||
|
if args.port:
|
||||||
|
settings['port'] = args.port
|
||||||
|
if args.baud:
|
||||||
|
settings['baudrate'] = args.baud
|
||||||
|
if args.timeout:
|
||||||
|
settings['timeout'] = args.timeout
|
||||||
|
|
||||||
|
# Ausgabe der aktuellen Einstellungen
|
||||||
|
port = settings.get('port')
|
||||||
|
baud = settings.get('baudrate', 'N/A')
|
||||||
|
timeout = settings.get('timeout', 'N/A')
|
||||||
|
|
||||||
|
if not port:
|
||||||
|
console_err.print("[error]Fehler: Kein serieller Port angegeben.[/error]")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if args.debug:
|
||||||
|
console.print(f" • Port: [info]{port}[/info]")
|
||||||
|
console.print(f" • Baud: [info]{baud}[/info]")
|
||||||
|
console.print(f" • Timeout: [info]{timeout:1.2f}s[/info]")
|
||||||
|
console.print("-" * 78)
|
||||||
|
|
||||||
|
from core.serial_conn import SerialBus
|
||||||
|
bus = SerialBus(settings)
|
||||||
|
|
||||||
|
try:
|
||||||
|
bus.open()
|
||||||
|
if args.command == "crc32":
|
||||||
|
from core.cmd.crc32 import crc32
|
||||||
|
cmd = crc32(bus)
|
||||||
|
result = cmd.get(args.path)
|
||||||
|
cmd.print(result, args.path)
|
||||||
|
elif args.command == "get_file":
|
||||||
|
from core.cmd.get_file import get_file
|
||||||
|
cmd = get_file(bus)
|
||||||
|
result = cmd.get(args.source_path, args.dest_path)
|
||||||
|
cmd.print(result)
|
||||||
|
elif args.command == "flash_info":
|
||||||
|
from core.cmd.flash_info import flash_info
|
||||||
|
cmd = flash_info(bus)
|
||||||
|
result = cmd.get()
|
||||||
|
cmd.print(result)
|
||||||
|
elif args.command == "fw_status":
|
||||||
|
from core.cmd.fw_status import fw_status
|
||||||
|
cmd = fw_status(bus)
|
||||||
|
result = cmd.get()
|
||||||
|
cmd.print(result)
|
||||||
|
elif args.command == "ls":
|
||||||
|
from core.cmd.list_dir import list_dir
|
||||||
|
cmd = list_dir(bus)
|
||||||
|
result = cmd.get(args.path, recursive=args.recursive)
|
||||||
|
cmd.print(result, args.path)
|
||||||
|
elif args.command == "proto":
|
||||||
|
from core.cmd.proto import proto
|
||||||
|
cmd = proto(bus)
|
||||||
|
result = cmd.get()
|
||||||
|
cmd.print(result)
|
||||||
|
elif args.command == "put_file":
|
||||||
|
from core.cmd.put_file import put_file
|
||||||
|
cmd = put_file(bus)
|
||||||
|
result = cmd.get(args.source_path, args.dest_path)
|
||||||
|
cmd.print(result)
|
||||||
|
elif args.command == "rename":
|
||||||
|
from core.cmd.rename import rename
|
||||||
|
cmd = rename(bus)
|
||||||
|
result = cmd.get(args.source_path, args.dest_path)
|
||||||
|
cmd.print(result)
|
||||||
|
elif args.command == "rm":
|
||||||
|
from core.cmd.rm import rm
|
||||||
|
cmd = rm(bus)
|
||||||
|
result = cmd.get(args.path)
|
||||||
|
cmd.print(result, args.path)
|
||||||
|
elif args.command == "stat":
|
||||||
|
from core.cmd.stat import stat
|
||||||
|
cmd = stat(bus)
|
||||||
|
result = cmd.get(args.path)
|
||||||
|
cmd.print(result, args.path)
|
||||||
|
elif args.command == "put_file":
|
||||||
|
from core.cmd.put_file import put_file
|
||||||
|
cmd = put_file(bus)
|
||||||
|
result = cmd.get(args.source_path, args.dest_path, cli_tags_json=args.tags)
|
||||||
|
cmd.print(result)
|
||||||
|
elif args.command == "get_tags":
|
||||||
|
from core.cmd.get_tags import get_tags
|
||||||
|
cmd = get_tags(bus)
|
||||||
|
result = cmd.get(args.path)
|
||||||
|
cmd.print(result, args.path)
|
||||||
|
elif args.command == "put_tags":
|
||||||
|
from core.cmd.put_tags import put_tags
|
||||||
|
cmd = put_tags(bus)
|
||||||
|
result = cmd.get(args.path, args.json, overwrite=args.overwrite)
|
||||||
|
cmd.print(result, args.path)
|
||||||
|
elif args.command == "confirm_fw":
|
||||||
|
from core.cmd.fw_confirm import fw_confirm
|
||||||
|
cmd = fw_confirm(bus)
|
||||||
|
result = cmd.get()
|
||||||
|
cmd.print(result)
|
||||||
|
elif args.command == "put_fw":
|
||||||
|
from core.cmd.put_fw import put_fw
|
||||||
|
cmd = put_fw(bus)
|
||||||
|
result = cmd.get(args.file_path)
|
||||||
|
cmd.print(result)
|
||||||
|
elif args.command == "reboot":
|
||||||
|
from core.cmd.reboot import reboot
|
||||||
|
cmd = reboot(bus)
|
||||||
|
result = cmd.get()
|
||||||
|
cmd.print(result)
|
||||||
|
elif args.command == "play":
|
||||||
|
from core.cmd.play import play
|
||||||
|
cmd = play(bus)
|
||||||
|
result = cmd.get(args.path, interrupt=args.interrupt)
|
||||||
|
cmd.print(result, args.path, interrupt=args.interrupt)
|
||||||
|
elif args.command == "stop":
|
||||||
|
from core.cmd.stop import stop
|
||||||
|
cmd = stop(bus)
|
||||||
|
result = cmd.get()
|
||||||
|
cmd.print(result)
|
||||||
|
elif args.command == "set":
|
||||||
|
from core.cmd.set_setting import set_setting
|
||||||
|
cmd = set_setting(bus)
|
||||||
|
result = cmd.get(args.key, args.value)
|
||||||
|
cmd.print(result, args.key, args.value)
|
||||||
|
elif args.command == "get":
|
||||||
|
from core.cmd.get_setting import get_setting
|
||||||
|
cmd = get_setting(bus)
|
||||||
|
result = cmd.get(args.key)
|
||||||
|
cmd.print(result, args.key)
|
||||||
|
finally:
|
||||||
|
bus.close()
|
||||||
|
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
console_err.print(f"[error]Fehler: {e}[/error]")
|
||||||
|
sys.exit(1)
|
||||||
|
except (TimeoutError, IOError, ValueError) as e:
|
||||||
|
console_err.print(f"[bold red]KOMMUNIKATIONSFEHLER:[/bold red] [error_msg]{e}[/error_msg]")
|
||||||
|
sys.exit(1) # Beendet das Script mit Fehlercode 1 für Tests
|
||||||
|
except Exception as e:
|
||||||
|
# Hier fangen wir auch deinen neuen ControllerError ab
|
||||||
|
from core.serial_conn import ControllerError
|
||||||
|
if isinstance(e, ControllerError):
|
||||||
|
console_err.print(f"[bold red]CONTROLLER FEHLER:[/bold red] [error_msg]{e}[/error_msg]")
|
||||||
|
else:
|
||||||
|
console_err.print(f"[bold red]UNERWARTETER FEHLER:[/bold red] [error_msg]{e}[/error_msg]")
|
||||||
|
|
||||||
|
if args.debug:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
4
tool/config.yaml
Normal file
4
tool/config.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
serial:
|
||||||
|
port: "/dev/cu.usbmodem83401"
|
||||||
|
baudrate: 115200
|
||||||
|
timeout: 1.0
|
||||||
36
tool/core/cmd/crc32.py
Normal file
36
tool/core/cmd/crc32.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# tool/core/cmd/crc32.py
|
||||||
|
import struct
|
||||||
|
from core.utils import console, console_err
|
||||||
|
from core.protocol import COMMANDS, ERRORS
|
||||||
|
|
||||||
|
class crc32:
|
||||||
|
def __init__(self, bus):
|
||||||
|
self.bus = bus
|
||||||
|
|
||||||
|
def get(self, path: str):
|
||||||
|
path_bytes = path.encode('utf-8')
|
||||||
|
payload = struct.pack('B', len(path_bytes)) + path_bytes
|
||||||
|
self.bus.send_request(COMMANDS['crc_32'], payload)
|
||||||
|
|
||||||
|
# 1 Byte Type + 4 Byte Size = 5
|
||||||
|
data = self.bus.receive_response(length=8, timeout=5)
|
||||||
|
|
||||||
|
if not data or data.get('type') == 'error':
|
||||||
|
return None
|
||||||
|
|
||||||
|
payload = data['data']
|
||||||
|
crc_value = struct.unpack('<I', payload[0:4])[0]
|
||||||
|
audio_crc_value = struct.unpack('<I', payload[4:8])[0]
|
||||||
|
result = {
|
||||||
|
'crc32': crc_value,
|
||||||
|
'audio_crc32': audio_crc_value
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
def print(self, result, path: str):
|
||||||
|
if not result:
|
||||||
|
return
|
||||||
|
|
||||||
|
console.print(f"[info_title]CRC32[/info_title] für [info]{path}[/info]:")
|
||||||
|
console.print(f" • CRC32 Datei: [info]{result['crc32']:08X}[/info]")
|
||||||
|
console.print(f" • CRC32 Audio: [info]{result['audio_crc32']:08X}[/info]")
|
||||||
53
tool/core/cmd/flash_info.py
Normal file
53
tool/core/cmd/flash_info.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# tool/core/cmd/flash_info.py
|
||||||
|
import struct
|
||||||
|
from core.utils import console, console_err
|
||||||
|
from core.protocol import COMMANDS, ERRORS
|
||||||
|
|
||||||
|
class flash_info:
|
||||||
|
def __init__(self, bus):
|
||||||
|
self.bus = bus
|
||||||
|
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
import struct
|
||||||
|
self.bus.send_request(COMMANDS['get_flash_info'])
|
||||||
|
|
||||||
|
data = self.bus.receive_response(length=21)
|
||||||
|
if not data or data.get('type') == 'error':
|
||||||
|
return None
|
||||||
|
|
||||||
|
payload = data['data']
|
||||||
|
ext_block_size = struct.unpack('<I', payload[0:4])[0]
|
||||||
|
ext_total_blocks = struct.unpack('<I', payload[4:8])[0]
|
||||||
|
ext_free_blocks = struct.unpack('<I', payload[8:12])[0]
|
||||||
|
int_slot_size = struct.unpack('<I', payload[12:16])[0]
|
||||||
|
ext_page_size = struct.unpack('<H', payload[16:18])[0]
|
||||||
|
int_page_size = struct.unpack('<H', payload[18:20])[0]
|
||||||
|
max_path_len = payload[20]
|
||||||
|
|
||||||
|
result = {
|
||||||
|
'ext_block_size': ext_block_size,
|
||||||
|
'ext_total_blocks': ext_total_blocks,
|
||||||
|
'ext_free_blocks': ext_free_blocks,
|
||||||
|
'int_slot_size': int_slot_size,
|
||||||
|
'ext_page_size': ext_page_size,
|
||||||
|
'int_page_size': int_page_size,
|
||||||
|
'max_path_len': max_path_len,
|
||||||
|
'ext_total_size': ext_block_size * ext_total_blocks,
|
||||||
|
'ext_free_size': ext_block_size * ext_free_blocks,
|
||||||
|
'ext_used_size': ext_block_size * (ext_total_blocks - ext_free_blocks)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
def print(self, result):
|
||||||
|
if not result:
|
||||||
|
return
|
||||||
|
|
||||||
|
console.print(f"[info]Flash-Informationen:[/info]")
|
||||||
|
console.print(f" • [info]Externer Flash:[/info] {result['ext_total_size']/1024/1024:.2f} MB ({result['ext_total_blocks']} Blöcke à {result['ext_block_size']} Bytes)")
|
||||||
|
console.print(f" - Belegt: {result['ext_used_size']/1024/1024:.2f} MB ({result['ext_total_blocks'] - result['ext_free_blocks']} Blöcke)")
|
||||||
|
console.print(f" - Frei: {result['ext_free_size']/1024/1024:.2f} MB ({result['ext_free_blocks']} Blöcke)")
|
||||||
|
console.print(f" • [info]FW Flash Slot:[/info] {result['int_slot_size']/1024:.2f} KB")
|
||||||
|
console.print(f" • [info]EXTFLASH Seitengröße:[/info] {result['ext_page_size']} Bytes")
|
||||||
|
console.print(f" • [info]INTFLASH Seitengröße:[/info] {result['int_page_size']} Bytes")
|
||||||
|
console.print(f" • [info]Maximale Pfadlänge:[/info] {result['max_path_len']} Zeichen")
|
||||||
23
tool/core/cmd/fw_confirm.py
Normal file
23
tool/core/cmd/fw_confirm.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
from core.utils import console, console_err
|
||||||
|
from core.protocol import COMMANDS
|
||||||
|
|
||||||
|
class fw_confirm:
|
||||||
|
def __init__(self, bus):
|
||||||
|
self.bus = bus
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
# Fehler 1: Der Key in COMMANDS heißt 'confirm_fw'
|
||||||
|
self.bus.send_request(COMMANDS['confirm_fw'])
|
||||||
|
|
||||||
|
# Fehler 2: Try-Except entfernt, damit der ControllerError (z.B. bei nicht-pending Image)
|
||||||
|
# sauber nach buzz.py durchschlägt.
|
||||||
|
data = self.bus.receive_ack()
|
||||||
|
return data is not None and data.get('type') == 'ack'
|
||||||
|
|
||||||
|
def print(self, result):
|
||||||
|
if result:
|
||||||
|
console.print("✓ Laufende Firmware wurde [info]erfolgreich bestätigt[/info] (Permanent).")
|
||||||
|
else:
|
||||||
|
# Wird im Fehlerfall eigentlich nicht mehr erreicht, da buzz.py abbricht,
|
||||||
|
# bleibt aber als Fallback für leere Antworten.
|
||||||
|
console_err.print("❌ Fehler beim Bestätigen der Firmware.")
|
||||||
53
tool/core/cmd/fw_status.py
Normal file
53
tool/core/cmd/fw_status.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# tool/core/cmd/fw_status.py
|
||||||
|
import struct
|
||||||
|
from core.utils import console, console_err
|
||||||
|
from core.protocol import COMMANDS, ERRORS
|
||||||
|
|
||||||
|
class fw_status:
|
||||||
|
def __init__(self, bus):
|
||||||
|
self.bus = bus
|
||||||
|
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
import struct
|
||||||
|
self.bus.send_request(COMMANDS['get_firmware_status'])
|
||||||
|
|
||||||
|
data = self.bus.receive_response(length=10)
|
||||||
|
if not data or data.get('type') == 'error':
|
||||||
|
return None
|
||||||
|
|
||||||
|
header = data['data']
|
||||||
|
status = header[0]
|
||||||
|
app_version_raw = struct.unpack('<I', header[1:5])[0]
|
||||||
|
ker_version_raw = struct.unpack('<I', header[5:9])[0]
|
||||||
|
str_len = header[9]
|
||||||
|
|
||||||
|
fw_string_bytes = self.bus.connection.read(str_len)
|
||||||
|
fw_string = fw_string_bytes.decode('utf-8')
|
||||||
|
|
||||||
|
result = {
|
||||||
|
'status': status,
|
||||||
|
'fw_version_raw': hex(app_version_raw),
|
||||||
|
'kernel_version_raw': hex(ker_version_raw),
|
||||||
|
'fw_major': (app_version_raw >> 24) & 0xFF,
|
||||||
|
'fw_minor': (app_version_raw >> 16) & 0xFF,
|
||||||
|
'fw_patch': (app_version_raw >> 8)& 0xFF,
|
||||||
|
'kernel_major': (ker_version_raw >> 16) & 0xFF,
|
||||||
|
'kernel_minor': (ker_version_raw >> 8) & 0xFF,
|
||||||
|
'kernel_patch': ker_version_raw & 0xFF,
|
||||||
|
'fw_string': fw_string,
|
||||||
|
'kernel_string': f"{(ker_version_raw >> 16) & 0xFF}.{(ker_version_raw >> 8) & 0xFF}.{ker_version_raw & 0xFF}"
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
def print(self, result):
|
||||||
|
if not result:
|
||||||
|
return
|
||||||
|
|
||||||
|
status = "UNKNOWN"
|
||||||
|
if result['status'] == 0x00: status = "CONFIRMED"
|
||||||
|
elif result['status'] == 0x01: status = "PENDING"
|
||||||
|
elif result['status'] == 0x02: status = "TESTING"
|
||||||
|
console.print(f"[info]Firmware Status[/info] des Controllers ist [info]{status}[/info]:")
|
||||||
|
console.print(f" • Firmware: [info]{result['fw_string']}[/info] ({result['fw_major']}.{result['fw_minor']}.{result['fw_patch']})")
|
||||||
|
console.print(f" • Kernel: [info]{result['kernel_string']}[/info] ({result['kernel_major']}.{result['kernel_minor']}.{result['kernel_patch']})")
|
||||||
89
tool/core/cmd/get_file.py
Normal file
89
tool/core/cmd/get_file.py
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
# tool/core/cmd/get_file.py
|
||||||
|
import struct
|
||||||
|
import zlib
|
||||||
|
from pathlib import Path
|
||||||
|
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, DownloadColumn, TransferSpeedColumn, TimeRemainingColumn
|
||||||
|
from core.utils import console, console_err
|
||||||
|
from core.protocol import COMMANDS
|
||||||
|
|
||||||
|
class get_file:
|
||||||
|
def __init__(self, bus):
|
||||||
|
self.bus = bus
|
||||||
|
|
||||||
|
def get(self, source_path: str, dest_path: str):
|
||||||
|
try:
|
||||||
|
p = Path(dest_path)
|
||||||
|
p.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(p, 'wb') as f:
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
console_err.print(f"Fehler: Kann Zieldatei nicht anlegen: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
source_path_bytes = source_path.encode('utf-8')
|
||||||
|
payload = struct.pack('B', len(source_path_bytes)) + source_path_bytes
|
||||||
|
self.bus.send_request(COMMANDS['get_file'], payload)
|
||||||
|
|
||||||
|
# Fortschrittsbalken Setup
|
||||||
|
with Progress(
|
||||||
|
SpinnerColumn(),
|
||||||
|
TextColumn("[progress.description]{task.description}"),
|
||||||
|
BarColumn(),
|
||||||
|
DownloadColumn(),
|
||||||
|
TransferSpeedColumn(),
|
||||||
|
"•",
|
||||||
|
TimeRemainingColumn(),
|
||||||
|
console=console,
|
||||||
|
transient=False
|
||||||
|
) as progress:
|
||||||
|
|
||||||
|
task = progress.add_task(f"Lade {source_path}...", total=None)
|
||||||
|
|
||||||
|
def update_bar(received, total):
|
||||||
|
progress.update(task, total=total, completed=received)
|
||||||
|
|
||||||
|
stream_res = self.bus.receive_stream(progress_callback=update_bar)
|
||||||
|
|
||||||
|
if not stream_res or stream_res.get('type') == 'error':
|
||||||
|
return None
|
||||||
|
file_data = stream_res['data']
|
||||||
|
remote_crc = stream_res.get('crc32')
|
||||||
|
local_crc = zlib.crc32(file_data) & 0xFFFFFFFF
|
||||||
|
duratuion = stream_res.get('duration')
|
||||||
|
|
||||||
|
if local_crc == remote_crc:
|
||||||
|
with open(p, 'wb') as f:
|
||||||
|
f.write(file_data)
|
||||||
|
success = True
|
||||||
|
else:
|
||||||
|
with open(p, 'wb') as f:
|
||||||
|
f.write(file_data)
|
||||||
|
success = False
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': success,
|
||||||
|
'source_path': source_path,
|
||||||
|
'dest_path': dest_path,
|
||||||
|
'crc32_remote': remote_crc,
|
||||||
|
'crc32_local': local_crc,
|
||||||
|
'size': len(file_data),
|
||||||
|
'duration': duratuion
|
||||||
|
}
|
||||||
|
|
||||||
|
def print(self, result):
|
||||||
|
if not result:
|
||||||
|
return
|
||||||
|
|
||||||
|
if result['success']:
|
||||||
|
console.print(f"✓ Datei [info]{result['source_path']}[/info] erfolgreich heruntergeladen.")
|
||||||
|
console.print(f" • Größe: [info]{result['size'] / 1024:.2f} KB[/info]")
|
||||||
|
else:
|
||||||
|
console_err.print(f"❌ CRC-FEHLER: Datei [error]{result['source_path']}[/error] wurde nicht korrekt empfangen!")
|
||||||
|
|
||||||
|
console.print(f" • Remote CRC: [info]{result['crc32_remote']:08X}[/info]")
|
||||||
|
console.print(f" • Local CRC: [info]{result['crc32_local']:08X}[/info]")
|
||||||
|
if result.get('crc32_device_file') is not None:
|
||||||
|
console.print(f" • Device CRC: [info]{result['crc32_device_file']:08X}[/info]")
|
||||||
|
console.print(f" • Zielpfad: [info]{result['dest_path']}[/info]")
|
||||||
|
if result.get('duration') is not None and result.get('duration') > 0:
|
||||||
|
console.print(f" • Dauer: [info]{result['duration']:.2f} s[/info]")
|
||||||
37
tool/core/cmd/get_setting.py
Normal file
37
tool/core/cmd/get_setting.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import struct
|
||||||
|
from core.utils import console
|
||||||
|
from core.protocol import COMMANDS
|
||||||
|
|
||||||
|
class get_setting:
|
||||||
|
def __init__(self, bus):
|
||||||
|
self.bus = bus
|
||||||
|
|
||||||
|
def get(self, key: str):
|
||||||
|
key_bytes = key.encode('utf-8')
|
||||||
|
payload = struct.pack('B', len(key_bytes)) + key_bytes
|
||||||
|
|
||||||
|
self.bus.send_request(COMMANDS['get_setting'], payload)
|
||||||
|
|
||||||
|
# varlen_params=1 liest exakt 1 Byte Länge + entsprechend viele Datenbytes
|
||||||
|
data = self.bus.receive_response(length=0, varlen_params=1)
|
||||||
|
|
||||||
|
if not data or data.get('type') == 'error':
|
||||||
|
return None
|
||||||
|
|
||||||
|
raw = data['data']
|
||||||
|
val_len = raw[0]
|
||||||
|
val_buf = raw[1:1+val_len]
|
||||||
|
|
||||||
|
# Binärdaten zurück in Python-Typen parsen
|
||||||
|
if key == "audio/vol" and val_len == 1:
|
||||||
|
return struct.unpack('<B', val_buf)[0]
|
||||||
|
elif key == "play/norepeat" and val_len == 1:
|
||||||
|
return bool(struct.unpack('<B', val_buf)[0])
|
||||||
|
elif key == "settings/storage_interval" and val_len == 2:
|
||||||
|
return struct.unpack('<H', val_buf)[0]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def print(self, result, key: str):
|
||||||
|
if result is not None:
|
||||||
|
console.print(f"⚙️ [info]{key}[/info] = [info]{result}[/info]")
|
||||||
43
tool/core/cmd/get_tags.py
Normal file
43
tool/core/cmd/get_tags.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# tool/core/cmd/get_tags.py
|
||||||
|
import struct
|
||||||
|
import json
|
||||||
|
from core.utils import console
|
||||||
|
from core.protocol import COMMANDS
|
||||||
|
from core.tag import TagManager
|
||||||
|
|
||||||
|
class get_tags:
|
||||||
|
def __init__(self, bus):
|
||||||
|
self.bus = bus
|
||||||
|
|
||||||
|
def get_raw_tlvs(self, path: str):
|
||||||
|
"""Holt die rohen TLVs vom Gerät."""
|
||||||
|
path_bytes = path.encode('utf-8')
|
||||||
|
payload = struct.pack('B', len(path_bytes)) + path_bytes
|
||||||
|
self.bus.send_request(COMMANDS['get_tags'], payload)
|
||||||
|
|
||||||
|
stream_res = self.bus.receive_stream()
|
||||||
|
if not stream_res or stream_res.get('type') == 'error': return []
|
||||||
|
|
||||||
|
return TagManager.parse_tlvs(stream_res['data'])
|
||||||
|
|
||||||
|
def get(self, path: str):
|
||||||
|
tlvs = self.get_raw_tlvs(path)
|
||||||
|
result = {"system": {}, "json": {}}
|
||||||
|
|
||||||
|
for tlv in tlvs:
|
||||||
|
if tlv['type'] == 0x00:
|
||||||
|
if tlv['index'] == 0x00 and len(tlv['value']) == 8:
|
||||||
|
codec, bit_depth, _, samplerate = struct.unpack('<BBHI', tlv['value'])
|
||||||
|
result["system"]["format"] = {"codec": codec, "bit_depth": bit_depth, "samplerate": samplerate}
|
||||||
|
elif tlv['index'] == 0x01 and len(tlv['value']) == 4:
|
||||||
|
result["system"]["crc32"] = f"0x{struct.unpack('<I', tlv['value'])[0]:08X}"
|
||||||
|
elif tlv['type'] == 0x10:
|
||||||
|
try:
|
||||||
|
result["json"].update(json.loads(tlv['value'].decode('utf-8')))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return result
|
||||||
|
|
||||||
|
def print(self, result, path: str):
|
||||||
|
console.print(f"[info]Metadaten[/info] für [info]{path}[/info]:")
|
||||||
|
console.print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||||
71
tool/core/cmd/list_dir.py
Normal file
71
tool/core/cmd/list_dir.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# tool/core/cmd/list_dir.py
|
||||||
|
import struct
|
||||||
|
from core.utils import console
|
||||||
|
from core.protocol import COMMANDS
|
||||||
|
|
||||||
|
class list_dir:
|
||||||
|
def __init__(self, bus):
|
||||||
|
self.bus = bus
|
||||||
|
|
||||||
|
def get(self, path: str, recursive: bool = False):
|
||||||
|
# Wir stellen sicher, dass der Pfad nicht leer ist und normalisieren ihn leicht
|
||||||
|
clean_path = path if path == "/" else path.rstrip('/')
|
||||||
|
path_bytes = clean_path.encode('utf-8')
|
||||||
|
|
||||||
|
payload = struct.pack('B', len(path_bytes)) + path_bytes
|
||||||
|
self.bus.send_request(COMMANDS['list_dir'], payload)
|
||||||
|
|
||||||
|
chunks = self.bus.receive_list()
|
||||||
|
if chunks is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
entries = []
|
||||||
|
for chunk in chunks:
|
||||||
|
if len(chunk) < 6: # Typ(1) + Size(4) + min 1 char Name
|
||||||
|
continue
|
||||||
|
|
||||||
|
is_dir = chunk[0] == 1
|
||||||
|
size = struct.unpack('<I', chunk[1:5])[0] if not is_dir else None
|
||||||
|
name = chunk[5:].decode('utf-8').rstrip('\x00')
|
||||||
|
|
||||||
|
entry = {
|
||||||
|
'name': name,
|
||||||
|
'is_dir': is_dir,
|
||||||
|
'size': size
|
||||||
|
}
|
||||||
|
|
||||||
|
if recursive and is_dir:
|
||||||
|
# Rekursiver Aufruf: Pfad sauber zusammenfügen
|
||||||
|
sub_path = f"{clean_path}/{name}"
|
||||||
|
entry['children'] = self.get(sub_path, recursive=True)
|
||||||
|
|
||||||
|
entries.append(entry)
|
||||||
|
|
||||||
|
return entries
|
||||||
|
|
||||||
|
def print(self, entries, path: str, prefix: str = ""):
|
||||||
|
if prefix == "":
|
||||||
|
console.print(f"Inhalt von [info]{path}[/info]:")
|
||||||
|
|
||||||
|
if not entries:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Sortierung: Verzeichnisse zuerst
|
||||||
|
entries.sort(key=lambda x: (not x['is_dir'], x['name'].lower()))
|
||||||
|
|
||||||
|
for i, entry in enumerate(entries):
|
||||||
|
# Prüfen, ob es das letzte Element auf dieser Ebene ist
|
||||||
|
is_last = (i == len(entries) - 1)
|
||||||
|
connector = "└" if is_last else "├"
|
||||||
|
|
||||||
|
icon = "📁" if entry['is_dir'] else "📄"
|
||||||
|
size_str = f" ({entry['size']/1024:.2f} KB)" if entry['size'] is not None else ""
|
||||||
|
|
||||||
|
# Ausgabe der aktuellen Zeile
|
||||||
|
console.print(f"{prefix}{connector}{icon} [info]{entry['name']}[/info]{size_str}")
|
||||||
|
|
||||||
|
# Wenn Kinder vorhanden sind, rekursiv weiter
|
||||||
|
if 'children' in entry and entry['children']:
|
||||||
|
# Für die Kinder-Ebene das Prefix anpassen
|
||||||
|
extension = " " if is_last else "│ "
|
||||||
|
self.print(entry['children'], "", prefix=prefix + extension)
|
||||||
23
tool/core/cmd/play.py
Normal file
23
tool/core/cmd/play.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import struct
|
||||||
|
from core.utils import console, console_err
|
||||||
|
from core.protocol import COMMANDS
|
||||||
|
|
||||||
|
class play:
|
||||||
|
def __init__(self, bus):
|
||||||
|
self.bus = bus
|
||||||
|
|
||||||
|
def get(self, path: str, interrupt: bool):
|
||||||
|
flags = 0x01 if interrupt else 0x00
|
||||||
|
path_bytes = path.encode('utf-8')
|
||||||
|
|
||||||
|
# Payload: [1 Byte Flags] + [1 Byte Path Length] + [Path String]
|
||||||
|
payload = struct.pack('B', flags) + struct.pack('B', len(path_bytes)) + path_bytes
|
||||||
|
|
||||||
|
self.bus.send_request(COMMANDS['play'], payload)
|
||||||
|
data = self.bus.receive_ack()
|
||||||
|
return data is not None and data.get('type') == 'ack'
|
||||||
|
|
||||||
|
def print(self, result, path: str, interrupt: bool):
|
||||||
|
if result:
|
||||||
|
mode = "sofort (Interrupt)" if interrupt else "in die Warteschlange (Queue)"
|
||||||
|
console.print(f"▶ Wiedergabe von [info]{path}[/info] {mode} eingereiht.")
|
||||||
28
tool/core/cmd/proto.py
Normal file
28
tool/core/cmd/proto.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# tool/core/cmd/proto.py
|
||||||
|
import struct
|
||||||
|
from core.utils import console, console_err
|
||||||
|
from core.protocol import COMMANDS, ERRORS
|
||||||
|
|
||||||
|
class proto:
|
||||||
|
def __init__(self, bus):
|
||||||
|
self.bus = bus
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
self.bus.send_request(COMMANDS['get_protocol_version'], None)
|
||||||
|
|
||||||
|
data = self.bus.receive_response(length=1)
|
||||||
|
if not data or data.get('type') == 'error':
|
||||||
|
return None
|
||||||
|
|
||||||
|
payload = data['data']
|
||||||
|
result = {
|
||||||
|
'protocol_version': payload[0]
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
def print(self, result):
|
||||||
|
if not result:
|
||||||
|
return
|
||||||
|
|
||||||
|
protocol_version = result['protocol_version']
|
||||||
|
console.print(f"[title]Protokoll Version[/info] des Controllers ist [info]{protocol_version}[/info]:")
|
||||||
100
tool/core/cmd/put_file.py
Normal file
100
tool/core/cmd/put_file.py
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
# tool/core/cmd/put_file.py
|
||||||
|
import struct
|
||||||
|
import zlib
|
||||||
|
from pathlib import Path
|
||||||
|
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, DownloadColumn, TransferSpeedColumn, TimeRemainingColumn
|
||||||
|
from core.utils import console, console_err
|
||||||
|
from core.protocol import COMMANDS
|
||||||
|
from core.tag import TagManager
|
||||||
|
from core.cmd.put_tags import put_tags
|
||||||
|
|
||||||
|
class put_file:
|
||||||
|
def __init__(self, bus):
|
||||||
|
self.bus = bus
|
||||||
|
|
||||||
|
def get(self, source_path: str, dest_path: str, cli_tags_json: str = None):
|
||||||
|
try:
|
||||||
|
p = Path(source_path)
|
||||||
|
if not p.exists() or not p.is_file():
|
||||||
|
console_err.print(f"Fehler: Quelldatei existiert nicht: {source_path}")
|
||||||
|
return None
|
||||||
|
with open(p, 'rb') as f:
|
||||||
|
file_data = f.read()
|
||||||
|
except Exception as e:
|
||||||
|
console_err.print(f"Fehler beim Lesen: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 1. Lokale Tags abtrennen
|
||||||
|
audio_data, local_tlvs = TagManager.split_file(file_data)
|
||||||
|
audio_size = len(audio_data)
|
||||||
|
|
||||||
|
# 2. Upload der REINEN Audiodaten
|
||||||
|
dest_path_bytes = dest_path.encode('utf-8')
|
||||||
|
payload = struct.pack('B', len(dest_path_bytes)) + dest_path_bytes + struct.pack('<I', audio_size)
|
||||||
|
|
||||||
|
self.bus.send_request(COMMANDS['put_file'], payload)
|
||||||
|
self.bus.receive_ack(timeout=5.0)
|
||||||
|
|
||||||
|
with Progress(SpinnerColumn(), TextColumn("[progress.description]{task.description}"), BarColumn(), DownloadColumn(), TransferSpeedColumn(), "•", TimeRemainingColumn(), console=console, transient=False) as progress:
|
||||||
|
task = progress.add_task(f"Sende {source_path}...", total=audio_size)
|
||||||
|
stream_res = self.bus.send_stream(audio_data, progress_callback=lambda sent, total: progress.update(task, total=total, completed=sent))
|
||||||
|
|
||||||
|
if not stream_res: return None
|
||||||
|
|
||||||
|
remote_crc = stream_res.get('crc32')
|
||||||
|
local_crc = zlib.crc32(audio_data) & 0xFFFFFFFF
|
||||||
|
|
||||||
|
if local_crc != remote_crc:
|
||||||
|
return {'success': False, 'source_path': source_path, 'crc32_remote': remote_crc, 'crc32_local': local_crc}
|
||||||
|
|
||||||
|
# 3. Tags aktualisieren (CRC32 + evtl. CLI-Tags)
|
||||||
|
# Alten CRC-Tag entfernen, neuen einsetzen
|
||||||
|
final_tlvs = [t for t in local_tlvs if not (t['type'] == 0x00 and t['index'] == 0x01)]
|
||||||
|
final_tlvs.append({'type': 0x00, 'index': 0x01, 'value': struct.pack('<I', local_crc)})
|
||||||
|
|
||||||
|
# Falls CLI-Tags übergeben wurden (-t), diese priorisiert anwenden
|
||||||
|
if cli_tags_json:
|
||||||
|
try:
|
||||||
|
cli_tlvs = TagManager.parse_cli_json(cli_tags_json)
|
||||||
|
# Bestehendes JSON löschen, wenn neues im CLI-Input definiert ist
|
||||||
|
if any(t['type'] == 0x10 for t in cli_tlvs):
|
||||||
|
final_tlvs = [t for t in final_tlvs if t['type'] != 0x10]
|
||||||
|
final_tlvs.extend(cli_tlvs)
|
||||||
|
except ValueError as e:
|
||||||
|
console_err.print(f"[warning]Warnung: Tags konnten nicht geparst werden ({e}). Datei wurde ohne extra Tags hochgeladen.[/warning]")
|
||||||
|
|
||||||
|
# 4. Tags via separatem Befehl anhängen
|
||||||
|
tag_cmd = put_tags(self.bus)
|
||||||
|
tag_blob = TagManager.build_blob(final_tlvs)
|
||||||
|
tag_cmd.send_blob(dest_path, tag_blob)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'source_path': source_path,
|
||||||
|
'dest_path': dest_path,
|
||||||
|
'crc32_remote': remote_crc,
|
||||||
|
'crc32_local': local_crc,
|
||||||
|
'size': audio_size,
|
||||||
|
'duration': stream_res.get('duration')
|
||||||
|
}
|
||||||
|
|
||||||
|
def print(self, result):
|
||||||
|
if not result:
|
||||||
|
return
|
||||||
|
|
||||||
|
if result.get('success'):
|
||||||
|
console.print(f"✓ Datei [info]{result['source_path']}[/info] erfolgreich hochgeladen und Tags generiert.")
|
||||||
|
console.print(f" • Größe: [info]{result['size'] / 1024:.2f} KB[/info]")
|
||||||
|
else:
|
||||||
|
console_err.print(f"❌ CRC-FEHLER: Datei [error]{result['source_path']}[/error] wurde auf dem Gerät korrumpiert!")
|
||||||
|
|
||||||
|
if 'crc32_remote' in result:
|
||||||
|
console.print(f" • Remote CRC: [info]{result['crc32_remote']:08X}[/info]")
|
||||||
|
if 'crc32_local' in result:
|
||||||
|
console.print(f" • Local CRC: [info]{result['crc32_local']:08X}[/info]")
|
||||||
|
|
||||||
|
if 'dest_path' in result:
|
||||||
|
console.print(f" • Zielpfad: [info]{result['dest_path']}[/info]")
|
||||||
|
|
||||||
|
if result.get('duration') is not None and result.get('duration') > 0:
|
||||||
|
console.print(f" • Dauer: [info]{result['duration']:.2f} s[/info]")
|
||||||
89
tool/core/cmd/put_fw.py
Normal file
89
tool/core/cmd/put_fw.py
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import struct
|
||||||
|
import zlib
|
||||||
|
from pathlib import Path
|
||||||
|
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, DownloadColumn, TransferSpeedColumn, TimeRemainingColumn
|
||||||
|
from core.utils import console, console_err
|
||||||
|
from core.protocol import COMMANDS
|
||||||
|
|
||||||
|
class put_fw:
|
||||||
|
def __init__(self, bus):
|
||||||
|
self.bus = bus
|
||||||
|
|
||||||
|
def get(self, file_path: str):
|
||||||
|
try:
|
||||||
|
p = Path(file_path)
|
||||||
|
if not p.exists() or not p.is_file():
|
||||||
|
console_err.print(f"Fehler: Firmware-Datei existiert nicht: {file_path}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
file_size = p.stat().st_size
|
||||||
|
with open(p, 'rb') as f:
|
||||||
|
file_data = f.read()
|
||||||
|
except Exception as e:
|
||||||
|
console_err.print(f"Lese-Fehler: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 1. Schritt: Löschvorgang mit minimalem Feedback
|
||||||
|
with Progress(
|
||||||
|
SpinnerColumn(),
|
||||||
|
TextColumn("[progress.description]{task.description}"),
|
||||||
|
BarColumn(),
|
||||||
|
console=console,
|
||||||
|
transient=True
|
||||||
|
) as progress:
|
||||||
|
erase_task = progress.add_task("Lösche Firmware Slot...", total=None)
|
||||||
|
|
||||||
|
payload = struct.pack('<I', file_size)
|
||||||
|
self.bus.send_request(COMMANDS['put_fw'], payload)
|
||||||
|
|
||||||
|
# Warten auf ACK (Balken pulsiert ohne Byte-Anzeige)
|
||||||
|
self.bus.receive_ack(timeout=10.0)
|
||||||
|
progress.update(erase_task, description="✓ Slot gelöscht", completed=100, total=100)
|
||||||
|
|
||||||
|
# 2. Schritt: Eigentlicher Transfer mit allen Metriken (Bytes, Speed, Time)
|
||||||
|
with Progress(
|
||||||
|
SpinnerColumn(),
|
||||||
|
TextColumn("[progress.description]{task.description}"),
|
||||||
|
BarColumn(),
|
||||||
|
DownloadColumn(),
|
||||||
|
TransferSpeedColumn(),
|
||||||
|
"•",
|
||||||
|
TimeRemainingColumn(),
|
||||||
|
console=console,
|
||||||
|
transient=False
|
||||||
|
) as progress:
|
||||||
|
transfer_task = progress.add_task("Sende Firmware...", total=file_size)
|
||||||
|
|
||||||
|
stream_res = self.bus.send_stream(
|
||||||
|
file_data,
|
||||||
|
progress_callback=lambda sent, total: progress.update(transfer_task, total=total, completed=sent)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not stream_res:
|
||||||
|
return None
|
||||||
|
|
||||||
|
remote_crc = stream_res.get('crc32')
|
||||||
|
local_crc = zlib.crc32(file_data) & 0xFFFFFFFF
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': local_crc == remote_crc,
|
||||||
|
'source_path': file_path,
|
||||||
|
'crc32_remote': remote_crc,
|
||||||
|
'crc32_local': local_crc,
|
||||||
|
'size': file_size,
|
||||||
|
'duration': stream_res.get('duration')
|
||||||
|
}
|
||||||
|
|
||||||
|
def print(self, result):
|
||||||
|
if not result:
|
||||||
|
return
|
||||||
|
|
||||||
|
if result['success']:
|
||||||
|
console.print(f"✓ Firmware [info]{result['source_path']}[/info] erfolgreich in Slot 1 geschrieben.")
|
||||||
|
console.print(" [warning]Achtung:[/warning] Das Image ist als 'Pending' markiert. Führe 'reboot' aus, um das Update zu installieren.")
|
||||||
|
else:
|
||||||
|
console_err.print(f"❌ CRC-FEHLER: Die Firmware wurde korrumpiert übertragen!")
|
||||||
|
|
||||||
|
console.print(f" • Größe: [info]{result['size'] / 1024:.2f} KB[/info]")
|
||||||
|
console.print(f" • Remote CRC: [info]{result['crc32_remote']:08X}[/info]")
|
||||||
|
console.print(f" • Local CRC: [info]{result['crc32_local']:08X}[/info]")
|
||||||
68
tool/core/cmd/put_tags.py
Normal file
68
tool/core/cmd/put_tags.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# tool/core/cmd/put_tags.py
|
||||||
|
import struct
|
||||||
|
import json
|
||||||
|
from core.utils import console, console_err
|
||||||
|
from core.protocol import COMMANDS
|
||||||
|
from core.tag import TagManager
|
||||||
|
from core.cmd.get_tags import get_tags
|
||||||
|
|
||||||
|
class put_tags:
|
||||||
|
def __init__(self, bus):
|
||||||
|
self.bus = bus
|
||||||
|
|
||||||
|
def get(self, path: str, json_str: str, overwrite: bool):
|
||||||
|
try:
|
||||||
|
new_tlvs = TagManager.parse_cli_json(json_str)
|
||||||
|
except ValueError as e:
|
||||||
|
console_err.print(f"[error]{e}[/error]")
|
||||||
|
return False
|
||||||
|
|
||||||
|
getter = get_tags(self.bus)
|
||||||
|
existing_tlvs = getter.get_raw_tlvs(path)
|
||||||
|
|
||||||
|
if overwrite:
|
||||||
|
# Bei Overwrite: Alle alten JSON-Tags löschen
|
||||||
|
existing_tlvs = [t for t in existing_tlvs if t['type'] != 0x10]
|
||||||
|
else:
|
||||||
|
# Ohne Overwrite: Bestehende JSON-Werte mit neuen mischen
|
||||||
|
existing_json = {}
|
||||||
|
for t in existing_tlvs:
|
||||||
|
if t['type'] == 0x10:
|
||||||
|
try: existing_json.update(json.loads(t['value'].decode('utf-8')))
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
# Neues JSON einmischen
|
||||||
|
for nt in new_tlvs:
|
||||||
|
if nt['type'] == 0x10:
|
||||||
|
try: existing_json.update(json.loads(nt['value'].decode('utf-8')))
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
existing_tlvs = [t for t in existing_tlvs if t['type'] != 0x10]
|
||||||
|
if existing_json:
|
||||||
|
existing_tlvs.append({'type': 0x10, 'index': 0x00, 'value': json.dumps(existing_json, ensure_ascii=False).encode('utf-8')})
|
||||||
|
|
||||||
|
# System-Tags (0x00) überschreiben alte direkt
|
||||||
|
new_sys_tlvs = [t for t in new_tlvs if t['type'] == 0x00]
|
||||||
|
for nt in new_sys_tlvs:
|
||||||
|
existing_tlvs = [t for t in existing_tlvs if not (t['type'] == nt['type'] and t['index'] == nt['index'])]
|
||||||
|
existing_tlvs.append(nt)
|
||||||
|
|
||||||
|
new_tlvs = existing_tlvs
|
||||||
|
|
||||||
|
blob = TagManager.build_blob(new_tlvs)
|
||||||
|
return self.send_blob(path, blob)
|
||||||
|
|
||||||
|
def send_blob(self, path: str, blob: bytes):
|
||||||
|
path_bytes = path.encode('utf-8')
|
||||||
|
req_payload = struct.pack('B', len(path_bytes)) + path_bytes + struct.pack('<I', len(blob))
|
||||||
|
|
||||||
|
self.bus.send_request(COMMANDS['put_tags'], req_payload)
|
||||||
|
self.bus.receive_ack(timeout=2.0)
|
||||||
|
|
||||||
|
if self.bus.send_stream(blob):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def print(self, result, path: str):
|
||||||
|
if result:
|
||||||
|
console.print(f"✓ Metadaten erfolgreich auf [info]{path}[/info] geschrieben.")
|
||||||
23
tool/core/cmd/reboot.py
Normal file
23
tool/core/cmd/reboot.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import serial
|
||||||
|
from core.utils import console, console_err
|
||||||
|
from core.protocol import COMMANDS
|
||||||
|
|
||||||
|
class reboot:
|
||||||
|
def __init__(self, bus):
|
||||||
|
self.bus = bus
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
self.bus.send_request(COMMANDS['reboot'])
|
||||||
|
try:
|
||||||
|
data = self.bus.receive_ack()
|
||||||
|
return data is not None and data.get('type') == 'ack'
|
||||||
|
except serial.SerialException:
|
||||||
|
# SerialException MUSS hier ignoriert werden, da der Controller
|
||||||
|
# den USB-Port beim Reboot hart schließt
|
||||||
|
return True
|
||||||
|
|
||||||
|
def print(self, result):
|
||||||
|
if result:
|
||||||
|
console.print("🔄 Neustart-Befehl erfolgreich gesendet. Controller [info]bootet neu...[/info]")
|
||||||
|
else:
|
||||||
|
console_err.print("❌ Fehler beim Senden des Neustart-Befehls.")
|
||||||
37
tool/core/cmd/rename.py
Normal file
37
tool/core/cmd/rename.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# tool/core/cmd/rename.py
|
||||||
|
import struct
|
||||||
|
from core.utils import console, console_err
|
||||||
|
from core.protocol import COMMANDS, ERRORS
|
||||||
|
|
||||||
|
class rename:
|
||||||
|
def __init__(self, bus):
|
||||||
|
self.bus = bus
|
||||||
|
|
||||||
|
def get(self, source_path: str, dest_path: str):
|
||||||
|
source_path_bytes = source_path.encode('utf-8')
|
||||||
|
dest_path_bytes = dest_path.encode('utf-8')
|
||||||
|
|
||||||
|
payload = struct.pack('B', len(source_path_bytes)) + source_path_bytes
|
||||||
|
payload += struct.pack('B', len(dest_path_bytes)) + dest_path_bytes
|
||||||
|
|
||||||
|
self.bus.send_request(COMMANDS['rename'], payload)
|
||||||
|
|
||||||
|
data = self.bus.receive_ack()
|
||||||
|
|
||||||
|
if not data or data.get('type') == 'error':
|
||||||
|
return None
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': data.get('type') == 'ack',
|
||||||
|
'source_path': source_path,
|
||||||
|
'dest_path': dest_path
|
||||||
|
}
|
||||||
|
|
||||||
|
def print(self, result):
|
||||||
|
if not result or not result.get('success'):
|
||||||
|
return
|
||||||
|
|
||||||
|
console.print(
|
||||||
|
f"Pfad [info]{result['source_path']}[/info] wurde erfolgreich in "
|
||||||
|
f"[info]{result['dest_path']}[/info] umbenannt."
|
||||||
|
)
|
||||||
32
tool/core/cmd/rm.py
Normal file
32
tool/core/cmd/rm.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# tool/core/cmd/rm.py
|
||||||
|
import struct
|
||||||
|
from core.utils import console, console_err
|
||||||
|
from core.protocol import COMMANDS, ERRORS
|
||||||
|
|
||||||
|
class rm:
|
||||||
|
def __init__(self, bus):
|
||||||
|
self.bus = bus
|
||||||
|
|
||||||
|
def get(self, path: str):
|
||||||
|
path_bytes = path.encode('utf-8')
|
||||||
|
payload = struct.pack('B', len(path_bytes)) + path_bytes
|
||||||
|
self.bus.send_request(COMMANDS['rm'], payload)
|
||||||
|
|
||||||
|
# 1 Byte Type + 4 Byte Size = 5
|
||||||
|
data = self.bus.receive_ack()
|
||||||
|
|
||||||
|
if not data or data.get('type') == 'error':
|
||||||
|
return None
|
||||||
|
|
||||||
|
if data.get('type') == 'ack':
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def print(self, result, path: str):
|
||||||
|
if result is None:
|
||||||
|
console_err.print(f"Fehler: Pfad [error]{path}[/error] konnte nicht entfernt werden.")
|
||||||
|
elif result is False:
|
||||||
|
console_err.print(f"Fehler: Pfad [error]{path}[/error] existiert nicht oder konnte nicht entfernt werden.")
|
||||||
|
else:
|
||||||
|
console.print(f"Pfad [info]{path}[/info] wurde erfolgreich entfernt.")
|
||||||
38
tool/core/cmd/set_setting.py
Normal file
38
tool/core/cmd/set_setting.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import struct
|
||||||
|
from core.utils import console, console_err
|
||||||
|
from core.protocol import COMMANDS
|
||||||
|
|
||||||
|
class set_setting:
|
||||||
|
def __init__(self, bus):
|
||||||
|
self.bus = bus
|
||||||
|
|
||||||
|
def get(self, key: str, value: str):
|
||||||
|
key_bytes = key.encode('utf-8')
|
||||||
|
val_bytes = b''
|
||||||
|
|
||||||
|
# Typen-Konvertierung basierend auf dem Key
|
||||||
|
try:
|
||||||
|
if key == "audio/vol":
|
||||||
|
val_bytes = struct.pack('<B', int(value))
|
||||||
|
elif key == "play/norepeat":
|
||||||
|
val_int = 1 if str(value).lower() in ['1', 'true', 'on', 'yes'] else 0
|
||||||
|
val_bytes = struct.pack('<B', val_int)
|
||||||
|
elif key == "settings/storage_interval":
|
||||||
|
val_bytes = struct.pack('<H', int(value))
|
||||||
|
else:
|
||||||
|
console_err.print(f"[error]Unbekannter Key: {key}[/error]")
|
||||||
|
return False
|
||||||
|
except ValueError:
|
||||||
|
console_err.print(f"[error]Ungültiger Wert für {key}: {value}[/error]")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Payload: [Key Len] [Key] [Val Len] [Val Bytes]
|
||||||
|
payload = struct.pack('B', len(key_bytes)) + key_bytes + struct.pack('B', len(val_bytes)) + val_bytes
|
||||||
|
self.bus.send_request(COMMANDS['set_setting'], payload)
|
||||||
|
|
||||||
|
data = self.bus.receive_ack()
|
||||||
|
return data is not None and data.get('type') == 'ack'
|
||||||
|
|
||||||
|
def print(self, result, key: str, value: str):
|
||||||
|
if result:
|
||||||
|
console.print(f"✓ Setting [info]{key}[/info] wurde auf [info]{value}[/info] gesetzt.")
|
||||||
36
tool/core/cmd/stat.py
Normal file
36
tool/core/cmd/stat.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# tool/core/cmd/stat.py
|
||||||
|
import struct
|
||||||
|
from core.utils import console, console_err
|
||||||
|
from core.protocol import COMMANDS, ERRORS
|
||||||
|
|
||||||
|
class stat:
|
||||||
|
def __init__(self, bus):
|
||||||
|
self.bus = bus
|
||||||
|
|
||||||
|
def get(self, path: str):
|
||||||
|
path_bytes = path.encode('utf-8')
|
||||||
|
payload = struct.pack('B', len(path_bytes)) + path_bytes
|
||||||
|
self.bus.send_request(COMMANDS['stat'], payload)
|
||||||
|
|
||||||
|
# 1 Byte Type + 4 Byte Size = 5
|
||||||
|
data = self.bus.receive_response(length=5)
|
||||||
|
|
||||||
|
if not data or data.get('type') == 'error':
|
||||||
|
return None
|
||||||
|
|
||||||
|
payload = data['data']
|
||||||
|
result = {
|
||||||
|
'is_directory': payload[0] == 1,
|
||||||
|
'size': struct.unpack('<I', payload[1:5])[0]
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
def print(self, result, path: str):
|
||||||
|
if not result:
|
||||||
|
return
|
||||||
|
|
||||||
|
t_name = "📁 Verzeichnis" if result['is_directory'] else "📄 Datei"
|
||||||
|
console.print(f"[info_title]Stat[/info_title] für [info]{path}[/info]:")
|
||||||
|
console.print(f" • Typ: [info]{t_name}[/info]")
|
||||||
|
if not result['is_directory']:
|
||||||
|
console.print(f" • Grösse: [info]{result['size']/1024:.2f} KB[/info] ({result['size']} Bytes)")
|
||||||
15
tool/core/cmd/stop.py
Normal file
15
tool/core/cmd/stop.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
from core.utils import console, console_err
|
||||||
|
from core.protocol import COMMANDS
|
||||||
|
|
||||||
|
class stop:
|
||||||
|
def __init__(self, bus):
|
||||||
|
self.bus = bus
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
self.bus.send_request(COMMANDS['stop'])
|
||||||
|
data = self.bus.receive_ack()
|
||||||
|
return data is not None and data.get('type') == 'ack'
|
||||||
|
|
||||||
|
def print(self, result):
|
||||||
|
if result:
|
||||||
|
console.print("⏹ Wiedergabe gestoppt und Warteschlange geleert.")
|
||||||
48
tool/core/config.py
Normal file
48
tool/core/config.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# tool/core/config.py
|
||||||
|
import os
|
||||||
|
import yaml
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
def __init__(self):
|
||||||
|
self._config = None
|
||||||
|
self.config_name = "config.yaml"
|
||||||
|
self.custom_path = None # Speicher für den Parameter-Pfad
|
||||||
|
self.debug = False
|
||||||
|
|
||||||
|
def _load(self):
|
||||||
|
if self._config is not None:
|
||||||
|
return
|
||||||
|
|
||||||
|
from core.utils import console
|
||||||
|
|
||||||
|
search_paths = []
|
||||||
|
if self.custom_path:
|
||||||
|
search_paths.append(Path(self.custom_path))
|
||||||
|
|
||||||
|
search_paths.append(Path(os.getcwd()) / self.config_name)
|
||||||
|
search_paths.append(Path(__file__).parent.parent / self.config_name)
|
||||||
|
|
||||||
|
config_path = None
|
||||||
|
for p in search_paths:
|
||||||
|
if p.exists():
|
||||||
|
config_path = p
|
||||||
|
break
|
||||||
|
|
||||||
|
if not config_path:
|
||||||
|
raise FileNotFoundError(f"Konfiguration konnte an keinem Ort gefunden werden.")
|
||||||
|
|
||||||
|
if not config_path.exists():
|
||||||
|
raise FileNotFoundError(f"Konfiguration nicht gefunden: {self.config_name}")
|
||||||
|
else:
|
||||||
|
if self.debug: console.print(f"[bold green]✓[/bold green] Konfiguration geladen: [info]{config_path}[/info]")
|
||||||
|
|
||||||
|
with open(config_path, 'r') as f:
|
||||||
|
self._config = yaml.safe_load(f)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def serial_settings(self):
|
||||||
|
self._load()
|
||||||
|
return self._config.get('serial', {})
|
||||||
|
|
||||||
|
cfg = Config()
|
||||||
77
tool/core/protocol.py
Normal file
77
tool/core/protocol.py
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# tool/core/protocol.py
|
||||||
|
VERSION = {
|
||||||
|
"min_protocol_version": 1,
|
||||||
|
"max_protocol_version": 1,
|
||||||
|
"current_protocol_version": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
SYNC_SEQ = b'BUZZ'
|
||||||
|
|
||||||
|
ERRORS = {
|
||||||
|
0x00: "NONE",
|
||||||
|
0x01: "INVALID_COMMAND",
|
||||||
|
0x02: "INVALID_PARAMETERS",
|
||||||
|
0x03: "MISSING_PARAMETERS",
|
||||||
|
|
||||||
|
0x10: "FILE_NOT_FOUND",
|
||||||
|
0x11: "ALREADY_EXISTS",
|
||||||
|
0x12: "NOT_A_DIRECTORY",
|
||||||
|
0x13: "IS_A_DIRECTORY",
|
||||||
|
0x14: "ACCESS_DENIED",
|
||||||
|
0x15: "NO_SPACE",
|
||||||
|
0x16: "FILE_TOO_LARGE",
|
||||||
|
|
||||||
|
0x20: "IO_ERROR",
|
||||||
|
0x21: "TIMEOUT",
|
||||||
|
0x22: "CRC_MISMATCH",
|
||||||
|
0x23: "TRANSFER_ABORTED",
|
||||||
|
|
||||||
|
0x30: "NOT_SUPPORTED",
|
||||||
|
0x31: "BUSY",
|
||||||
|
0x32: "INTERNAL_ERROR",
|
||||||
|
|
||||||
|
0x40: "NOT_IMPLEMENTED",
|
||||||
|
}
|
||||||
|
|
||||||
|
FRAME_TYPES = {
|
||||||
|
'request': 0x01,
|
||||||
|
|
||||||
|
'ack': 0x10,
|
||||||
|
|
||||||
|
'response': 0x11,
|
||||||
|
'stream_start': 0x12,
|
||||||
|
'stream_chunk': 0x13,
|
||||||
|
'stream_end': 0x14,
|
||||||
|
'list_start': 0x15,
|
||||||
|
'list_chunk': 0x16,
|
||||||
|
'list_end': 0x17,
|
||||||
|
|
||||||
|
'error': 0xFF,
|
||||||
|
}
|
||||||
|
|
||||||
|
COMMANDS = {
|
||||||
|
'get_protocol_version': 0x00,
|
||||||
|
'get_firmware_status': 0x01,
|
||||||
|
'get_flash_info': 0x02,
|
||||||
|
'confirm_fw': 0x03,
|
||||||
|
'reboot': 0x04,
|
||||||
|
|
||||||
|
'list_dir': 0x10,
|
||||||
|
'crc_32': 0x11,
|
||||||
|
'mkdir': 0x12,
|
||||||
|
'rm': 0x13,
|
||||||
|
'stat': 0x18,
|
||||||
|
'rename': 0x19,
|
||||||
|
|
||||||
|
'put_file': 0x20,
|
||||||
|
'put_fw': 0x21,
|
||||||
|
'get_file': 0x22,
|
||||||
|
'put_tags': 0x24,
|
||||||
|
'get_tags': 0x25,
|
||||||
|
|
||||||
|
'play': 0x30,
|
||||||
|
'stop': 0x31,
|
||||||
|
|
||||||
|
'set_setting': 0x40,
|
||||||
|
'get_setting': 0x41,
|
||||||
|
}
|
||||||
317
tool/core/serial_conn.py
Normal file
317
tool/core/serial_conn.py
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
# tool/core/serial_conn.py
|
||||||
|
import struct
|
||||||
|
import serial
|
||||||
|
import time
|
||||||
|
from core.utils import console, console_err
|
||||||
|
from core.protocol import SYNC_SEQ, ERRORS, FRAME_TYPES, VERSION
|
||||||
|
|
||||||
|
class SerialBus:
|
||||||
|
def __init__(self, settings: dict):
|
||||||
|
"""
|
||||||
|
Initialisiert den Bus mit den (ggf. übersteuerten) Settings.
|
||||||
|
"""
|
||||||
|
self.port = settings.get('port')
|
||||||
|
self.baudrate = settings.get('baudrate', 115200)
|
||||||
|
self.timeout = settings.get('timeout', 1.0)
|
||||||
|
self.debug = settings.get('debug', False)
|
||||||
|
self.connection = None
|
||||||
|
|
||||||
|
def open(self):
|
||||||
|
"""Öffnet die serielle Schnittstelle."""
|
||||||
|
try:
|
||||||
|
self.connection = serial.Serial(
|
||||||
|
port=self.port,
|
||||||
|
baudrate=self.baudrate,
|
||||||
|
timeout=self.timeout
|
||||||
|
)
|
||||||
|
self.flush_input()
|
||||||
|
if self.debug: console.print(f"[bold green]✓[/bold green] Port [info]{self.port}[/info] erfolgreich geöffnet.")
|
||||||
|
from core.cmd.proto import proto
|
||||||
|
cmd = proto(self)
|
||||||
|
data = cmd.get()
|
||||||
|
VERSION["current_protocol_version"] = data['protocol_version'] if data else None
|
||||||
|
if data:
|
||||||
|
if self.debug: console.print(f" • Protokoll Version: [info]{data['protocol_version']}[/info]")
|
||||||
|
if data['protocol_version'] < VERSION["min_protocol_version"] or data['protocol_version'] > VERSION["max_protocol_version"]:
|
||||||
|
if VERSION["min_protocol_version"] == VERSION["max_protocol_version"]:
|
||||||
|
expected = f"Version {VERSION['min_protocol_version']}"
|
||||||
|
else:
|
||||||
|
expected = f"Version {VERSION['min_protocol_version']} bis {VERSION['max_protocol_version']}"
|
||||||
|
raise ValueError(f"Inkompatibles Protokoll. Controller spricht {data['protocol_version']}, erwartet wird {expected}.")
|
||||||
|
else:
|
||||||
|
raise ValueError("Keine gültige Antwort auf Protokollversion erhalten.")
|
||||||
|
except serial.SerialException as e:
|
||||||
|
console_err.print(f"[bold red]Serieller Fehler:[/bold red] [error_msg]{e}[/error_msg]")
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
console_err.print(f"[bold red]Unerwarteter Fehler beim Öffnen:[/bold red] [error_msg]{e}[/error_msg]")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def flush_input(self):
|
||||||
|
"""Leert den Empfangspuffer der seriellen Schnittstelle."""
|
||||||
|
if self.connection and self.connection.is_open:
|
||||||
|
self.connection.reset_input_buffer()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""Schließt die Verbindung sauber."""
|
||||||
|
if self.connection and self.connection.is_open:
|
||||||
|
self.connection.close()
|
||||||
|
if self.debug: console.print(f"Verbindung zu [info]{self.port}[/info] geschlossen.")
|
||||||
|
|
||||||
|
def send_binary(self, data: bytes):
|
||||||
|
"""Sendet Rohdaten und loggt sie im Hex-Format."""
|
||||||
|
if not self.connection or not self.connection.is_open:
|
||||||
|
raise ConnectionError("Port ist nicht geöffnet.")
|
||||||
|
|
||||||
|
self.connection.write(data)
|
||||||
|
|
||||||
|
if self.debug:
|
||||||
|
hex_data = data.hex(' ').upper()
|
||||||
|
console.print(f"TX -> [grey62]{hex_data}[/grey62]")
|
||||||
|
|
||||||
|
def _read_exact(self, length: int, context: str = "Daten") -> bytes:
|
||||||
|
data = bytearray()
|
||||||
|
while len(data) < length:
|
||||||
|
try:
|
||||||
|
chunk = self.connection.read(length - len(data))
|
||||||
|
except serial.SerialException as e:
|
||||||
|
raise IOError(f"Serielle Verbindung verloren beim Lesen von {context}: {e}") from e
|
||||||
|
if not chunk:
|
||||||
|
raise TimeoutError(f"Timeout beim Lesen von {context}: {len(data)}/{length} Bytes.")
|
||||||
|
data.extend(chunk)
|
||||||
|
return bytes(data)
|
||||||
|
|
||||||
|
def wait_for_sync(self, sync_seq: bytes, max_time: float = 2.0):
|
||||||
|
"""Wartet maximal max_time Sekunden auf die Sync-Sequenz."""
|
||||||
|
buffer = b""
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
if self.debug:
|
||||||
|
console.print(f"[bold cyan]Warte auf SYNC-Sequenz:[/bold cyan] [grey62]{sync_seq.hex(' ').upper()}[/grey62]")
|
||||||
|
|
||||||
|
# Kurzer interner Timeout für reaktive Schleife
|
||||||
|
original_timeout = self.connection.timeout
|
||||||
|
self.connection.timeout = 0.1
|
||||||
|
|
||||||
|
try:
|
||||||
|
while (time.time() - start_time) < max_time:
|
||||||
|
char = self.connection.read(1)
|
||||||
|
if not char:
|
||||||
|
continue
|
||||||
|
|
||||||
|
buffer += char
|
||||||
|
if len(buffer) > len(sync_seq):
|
||||||
|
buffer = buffer[1:]
|
||||||
|
|
||||||
|
if buffer == sync_seq:
|
||||||
|
if self.debug: console.print("[bold cyan]RX <- SYNC OK[/bold cyan]")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
finally:
|
||||||
|
self.connection.timeout = original_timeout
|
||||||
|
|
||||||
|
def send_request(self, cmd_id: int, payload: bytes = b''):
|
||||||
|
self.flush_input()
|
||||||
|
frame_type = struct.pack('B', FRAME_TYPES['request'])
|
||||||
|
cmd_byte = struct.pack('B', cmd_id)
|
||||||
|
|
||||||
|
full_frame = SYNC_SEQ + frame_type + cmd_byte
|
||||||
|
if payload:
|
||||||
|
full_frame += payload
|
||||||
|
self.send_binary(full_frame)
|
||||||
|
|
||||||
|
def send_stream(self, data: bytes, chunk_size: int = 4096, progress_callback=None):
|
||||||
|
"""Sendet einen Datenstrom in Chunks und wartet auf die Bestätigung (CRC)."""
|
||||||
|
start_time = time.time()
|
||||||
|
size = len(data)
|
||||||
|
sent_size = 0
|
||||||
|
|
||||||
|
while sent_size < size:
|
||||||
|
chunk = data[sent_size:sent_size+chunk_size]
|
||||||
|
self.connection.write(chunk)
|
||||||
|
sent_size += len(chunk)
|
||||||
|
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(sent_size, size)
|
||||||
|
|
||||||
|
if not self.wait_for_sync(SYNC_SEQ, max_time=self.timeout + 5.0):
|
||||||
|
raise TimeoutError("Timeout beim Warten auf Stream-Ende (Flash ist evtl. noch beschäftigt).")
|
||||||
|
|
||||||
|
ftype = self._read_exact(1, "Frame-Typ")[0]
|
||||||
|
|
||||||
|
if ftype == FRAME_TYPES['stream_end']:
|
||||||
|
end_time = time.time()
|
||||||
|
crc32 = struct.unpack('<I', self._read_exact(4, "CRC32"))[0]
|
||||||
|
return {
|
||||||
|
'crc32': crc32,
|
||||||
|
'duration': end_time - start_time
|
||||||
|
}
|
||||||
|
elif ftype == FRAME_TYPES['error']:
|
||||||
|
err_code_raw = self.connection.read(1)
|
||||||
|
err_code = err_code_raw[0] if err_code_raw else 0xFF
|
||||||
|
err_name = ERRORS.get(err_code, "UNKNOWN")
|
||||||
|
raise ControllerError(err_code, err_name)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unerwarteter Frame-Typ nach Stream-Upload: 0x{ftype:02X}")
|
||||||
|
|
||||||
|
def receive_ack(self, timeout: float = None):
|
||||||
|
wait_time = timeout if timeout is not None else self.timeout
|
||||||
|
|
||||||
|
if not self.wait_for_sync(SYNC_SEQ, max_time=wait_time):
|
||||||
|
raise TimeoutError(f"SYNC-Sequenz nicht innerhalb von {wait_time}s gefunden.")
|
||||||
|
|
||||||
|
ftype_raw = self.connection.read(1)
|
||||||
|
if not ftype_raw:
|
||||||
|
raise TimeoutError("Timeout beim Lesen des Frame-Typs.")
|
||||||
|
ftype = ftype_raw[0]
|
||||||
|
|
||||||
|
if ftype == FRAME_TYPES['error']:
|
||||||
|
err_code_raw = self.connection.read(1)
|
||||||
|
err_code = err_code_raw[0] if err_code_raw else 0xFF
|
||||||
|
err_name = ERRORS.get(err_code, "UNKNOWN")
|
||||||
|
raise ControllerError(err_code, err_name)
|
||||||
|
|
||||||
|
elif ftype == FRAME_TYPES['ack']:
|
||||||
|
if self.debug:
|
||||||
|
console.print(f"[green]ACK empfangen[/green]")
|
||||||
|
return {"type": "ack"}
|
||||||
|
raise ValueError(f"Unerwarteter Frame-Typ (0x{FRAME_TYPES['ack']:02X} (ACK) erwartet): 0x{ftype:02X}")
|
||||||
|
|
||||||
|
def receive_response(self, length: int, timeout: float = None, varlen_params: int = 0):
|
||||||
|
wait_time = timeout if timeout is not None else self.timeout
|
||||||
|
|
||||||
|
if not self.wait_for_sync(SYNC_SEQ, max_time=wait_time):
|
||||||
|
raise TimeoutError(f"SYNC-Sequenz nicht innerhalb von {wait_time}s gefunden.")
|
||||||
|
|
||||||
|
ftype_raw = self.connection.read(1)
|
||||||
|
if not ftype_raw:
|
||||||
|
raise TimeoutError("Timeout beim Lesen des Frame-Typs.")
|
||||||
|
ftype = ftype_raw[0]
|
||||||
|
|
||||||
|
if ftype == FRAME_TYPES['error']:
|
||||||
|
err_code_raw = self.connection.read(1)
|
||||||
|
err_code = err_code_raw[0] if err_code_raw else 0xFF
|
||||||
|
err_name = ERRORS.get(err_code, "UNKNOWN")
|
||||||
|
raise ControllerError(err_code, err_name)
|
||||||
|
|
||||||
|
elif ftype == FRAME_TYPES['response']:
|
||||||
|
data = self.connection.read(length)
|
||||||
|
for varlen_param in range(varlen_params):
|
||||||
|
length_byte = self.connection.read(1)
|
||||||
|
if not length_byte:
|
||||||
|
raise TimeoutError("Timeout beim Lesen der Länge eines variablen Parameters.")
|
||||||
|
param_length = length_byte[0]
|
||||||
|
param_data = self.connection.read(param_length)
|
||||||
|
if not param_data:
|
||||||
|
raise TimeoutError("Timeout beim Lesen eines variablen Parameters.")
|
||||||
|
data += length_byte + param_data
|
||||||
|
if self.debug:
|
||||||
|
console.print(f"RX <- [grey62]{data.hex(' ').upper()}[/grey62]")
|
||||||
|
if len(data) < length:
|
||||||
|
raise IOError(f"Unvollständiges Paket: {len(data)}/{length} Bytes.")
|
||||||
|
return {"type": "response", "data": data}
|
||||||
|
|
||||||
|
raise ValueError(f"Unerwarteter Frame-Typ: 0x{ftype:02X}")
|
||||||
|
|
||||||
|
def receive_list(self):
|
||||||
|
"""Liest eine Liste von Einträgen, bis list_end kommt."""
|
||||||
|
is_list = False
|
||||||
|
list_items = []
|
||||||
|
|
||||||
|
while True:
|
||||||
|
if not self.wait_for_sync(SYNC_SEQ):
|
||||||
|
raise TimeoutError("Timeout beim Warten auf Sync im List-Modus.")
|
||||||
|
|
||||||
|
ftype = self.connection.read(1)[0]
|
||||||
|
|
||||||
|
if ftype == FRAME_TYPES['list_start']:
|
||||||
|
is_list = True
|
||||||
|
list_items = []
|
||||||
|
elif ftype == FRAME_TYPES['list_chunk']:
|
||||||
|
if not is_list: raise ValueError("Chunk ohne Start.")
|
||||||
|
length = struct.unpack('<H', self.connection.read(2))[0]
|
||||||
|
if self.debug: console.print(f"Erwarte List-Chunk mit Länge: {length} Bytes")
|
||||||
|
data = self.connection.read(length)
|
||||||
|
if self.debug: console.print(f"Rohdaten List-Chunk: [grey62]{data.hex(' ').upper()}[/grey62]") # Debug-Ausgabe des rohen Chunks
|
||||||
|
list_items.append(data)
|
||||||
|
elif ftype == FRAME_TYPES['list_end']:
|
||||||
|
if not is_list: raise ValueError("Ende ohne Start.")
|
||||||
|
num_entries = struct.unpack('<H', self.connection.read(2))[0]
|
||||||
|
if len(list_items) != num_entries:
|
||||||
|
console_err.print(f"[warning]Warnung: Erwartete {num_entries} Items, bekam {len(list_items)}[/warning]")
|
||||||
|
return list_items
|
||||||
|
elif ftype == FRAME_TYPES['error']:
|
||||||
|
err_code_raw = self.connection.read(1)
|
||||||
|
err_code = err_code_raw[0] if err_code_raw else 0xFF
|
||||||
|
err_name = ERRORS.get(err_code, "UNKNOWN")
|
||||||
|
raise ControllerError(err_code, err_name)
|
||||||
|
|
||||||
|
def receive_stream(self, chunk_size: int = 1024, progress_callback=None):
|
||||||
|
"""Liest einen Datenstrom in Chunks, bis ein Fehler oder Ende-Signal kommt."""
|
||||||
|
is_stream = False
|
||||||
|
data_chunks = []
|
||||||
|
start_time = None
|
||||||
|
|
||||||
|
while True:
|
||||||
|
if not self.wait_for_sync(SYNC_SEQ):
|
||||||
|
raise TimeoutError("Timeout beim Warten auf Sync im Stream-Modus.")
|
||||||
|
|
||||||
|
ftype = self._read_exact(1, "Frame-Typ")[0]
|
||||||
|
|
||||||
|
if ftype == FRAME_TYPES['stream_start']:
|
||||||
|
is_stream = True
|
||||||
|
data_chunks = []
|
||||||
|
size = struct.unpack('<I', self._read_exact(4, "Stream-Größe"))[0]
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
received_size = 0
|
||||||
|
while received_size < size:
|
||||||
|
chunk_length = min(chunk_size, size - received_size)
|
||||||
|
chunk_data = self._read_exact(chunk_length, f"Daten-Chunk @ {received_size}/{size}")
|
||||||
|
data_chunks.append(chunk_data)
|
||||||
|
received_size += len(chunk_data)
|
||||||
|
|
||||||
|
# Callback für UI-Update (z.B. Progress Bar)
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(received_size, size)
|
||||||
|
|
||||||
|
if self.debug: console.print("Stream vollständig empfangen.")
|
||||||
|
|
||||||
|
elif ftype == FRAME_TYPES['stream_end']:
|
||||||
|
end_time = time.time()
|
||||||
|
if not is_stream: raise ValueError("Ende ohne Start.")
|
||||||
|
crc32 = struct.unpack('<I', self._read_exact(4, "CRC32"))[0]
|
||||||
|
return {
|
||||||
|
'data': b''.join(data_chunks),
|
||||||
|
'crc32': crc32,
|
||||||
|
'duration': end_time - start_time if start_time and end_time else None
|
||||||
|
}
|
||||||
|
|
||||||
|
# elif ftype == FRAME_TYPES['list_chunk']:
|
||||||
|
# if not is_list: raise ValueError("Chunk ohne Start.")
|
||||||
|
# length = struct.unpack('<H', self.connection.read(2))[0]
|
||||||
|
# if self.debug: console.print(f"Erwarte List-Chunk mit Länge: {length} Bytes")
|
||||||
|
# data = self.connection.read(length)
|
||||||
|
# if self.debug: console.print(f"Rohdaten List-Chunk: [grey62]{data.hex(' ').upper()}[/grey62]") # Debug-Ausgabe des rohen Chunks
|
||||||
|
# list_items.append(data)
|
||||||
|
# elif ftype == FRAME_TYPES['list_end']:
|
||||||
|
# if not is_list: raise ValueError("Ende ohne Start.")
|
||||||
|
# num_entries = struct.unpack('<H', self.connection.read(2))[0]
|
||||||
|
# if len(list_items) != num_entries:
|
||||||
|
# console_err.print(f"[warning]Warnung: Erwartete {num_entries} Items, bekam {len(list_items)}[/warning]")
|
||||||
|
# return list_items
|
||||||
|
elif ftype == FRAME_TYPES['error']:
|
||||||
|
err_code_raw = self.connection.read(1)
|
||||||
|
err_code = err_code_raw[0] if err_code_raw else 0xFF
|
||||||
|
err_name = ERRORS.get(err_code, "UNKNOWN")
|
||||||
|
raise ControllerError(err_code, err_name)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unerwarteter Frame-Typ: 0x{ftype:02X}")
|
||||||
|
|
||||||
|
class ControllerError(Exception):
|
||||||
|
"""Wird ausgelöst, wenn der Controller einen Error-Frame (0xFF) sendet."""
|
||||||
|
def __init__(self, code, name):
|
||||||
|
self.code = code
|
||||||
|
self.name = name
|
||||||
|
super().__init__(f"Controller Error 0x{code:02X} ({name})")
|
||||||
95
tool/core/tag.py
Normal file
95
tool/core/tag.py
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
# tool/core/tag.py
|
||||||
|
import struct
|
||||||
|
import json
|
||||||
|
|
||||||
|
class TagManager:
|
||||||
|
FOOTER_MAGIC = b'TAG!'
|
||||||
|
FOOTER_SIZE = 8
|
||||||
|
FORMAT_VERSION = 1
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def split_file(cls, file_data: bytes):
|
||||||
|
"""Trennt eine Datei in reine Audiodaten und eine Liste von TLVs auf."""
|
||||||
|
if len(file_data) < cls.FOOTER_SIZE:
|
||||||
|
return file_data, []
|
||||||
|
|
||||||
|
footer = file_data[-cls.FOOTER_SIZE:]
|
||||||
|
total_size, version, magic = struct.unpack('<HH4s', footer)
|
||||||
|
|
||||||
|
if magic != cls.FOOTER_MAGIC or version != cls.FORMAT_VERSION:
|
||||||
|
return file_data, []
|
||||||
|
|
||||||
|
if total_size > len(file_data) or total_size < cls.FOOTER_SIZE:
|
||||||
|
return file_data, []
|
||||||
|
|
||||||
|
audio_limit = len(file_data) - total_size
|
||||||
|
audio_data = file_data[:audio_limit]
|
||||||
|
tag_data = file_data[audio_limit:-cls.FOOTER_SIZE]
|
||||||
|
|
||||||
|
tlvs = cls.parse_tlvs(tag_data)
|
||||||
|
return audio_data, tlvs
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse_tlvs(cls, tag_data: bytes):
|
||||||
|
"""Parst einen rohen TLV-Byteblock in eine Liste von Dictionaries."""
|
||||||
|
tlvs = []
|
||||||
|
pos = 0
|
||||||
|
while pos + 4 <= len(tag_data):
|
||||||
|
t, i, length = struct.unpack('<BBH', tag_data[pos:pos+4])
|
||||||
|
pos += 4
|
||||||
|
if pos + length > len(tag_data):
|
||||||
|
break # Korrupt
|
||||||
|
val = tag_data[pos:pos+length]
|
||||||
|
tlvs.append({'type': t, 'index': i, 'value': val})
|
||||||
|
pos += length
|
||||||
|
return tlvs
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def build_blob(cls, tlvs: list):
|
||||||
|
"""Baut aus einer TLV-Liste den fertigen Byte-Blob inklusive Footer."""
|
||||||
|
# Sortierung: Type 0x00 (System) zwingend nach vorne
|
||||||
|
tlvs = sorted(tlvs, key=lambda x: x['type'])
|
||||||
|
|
||||||
|
payload = b""
|
||||||
|
for tlv in tlvs:
|
||||||
|
payload += struct.pack('<BBH', tlv['type'], tlv['index'], len(tlv['value']))
|
||||||
|
payload += tlv['value']
|
||||||
|
|
||||||
|
total_size = len(payload) + cls.FOOTER_SIZE
|
||||||
|
footer = struct.pack('<HH4s', total_size, cls.FORMAT_VERSION, cls.FOOTER_MAGIC)
|
||||||
|
return payload + footer
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse_cli_json(cls, json_str: str):
|
||||||
|
"""Konvertiert den Kommandozeilen-JSON-String in neue TLVs."""
|
||||||
|
try:
|
||||||
|
data = json.loads(json_str)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
raise ValueError(f"Ungültiges JSON-Format: {e}")
|
||||||
|
|
||||||
|
new_tlvs = []
|
||||||
|
|
||||||
|
# 1. System Tags (0x00)
|
||||||
|
if "system" in data:
|
||||||
|
sys_data = data["system"]
|
||||||
|
if "format" in sys_data:
|
||||||
|
fmt = sys_data["format"]
|
||||||
|
if isinstance(fmt, str) and fmt.startswith("0x"):
|
||||||
|
val = bytes.fromhex(fmt[2:])
|
||||||
|
else:
|
||||||
|
val = struct.pack('<BBHI', fmt.get("codec", 0), fmt.get("bit_depth", 16), 0, fmt.get("samplerate", 16000))
|
||||||
|
new_tlvs.append({'type': 0x00, 'index': 0x00, 'value': val})
|
||||||
|
|
||||||
|
if "crc32" in sys_data:
|
||||||
|
crc_str = sys_data["crc32"]
|
||||||
|
crc_val = int(crc_str, 16) if isinstance(crc_str, str) else crc_str
|
||||||
|
new_tlvs.append({'type': 0x00, 'index': 0x01, 'value': struct.pack('<I', crc_val)})
|
||||||
|
|
||||||
|
# 2. JSON Tags (0x10)
|
||||||
|
if "json" in data:
|
||||||
|
# Bei leerem JSON-Objekt ("{}") wird kein 0x10 TLV erstellt
|
||||||
|
if data["json"]:
|
||||||
|
json_bytes = json.dumps(data["json"], ensure_ascii=False).encode('utf-8')
|
||||||
|
new_tlvs.append({'type': 0x10, 'index': 0x00, 'value': json_bytes})
|
||||||
|
|
||||||
|
return new_tlvs
|
||||||
14
tool/core/utils.py
Normal file
14
tool/core/utils.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
from rich.console import Console
|
||||||
|
from rich.theme import Theme
|
||||||
|
|
||||||
|
custom_theme = Theme({
|
||||||
|
"info": "bold blue",
|
||||||
|
"warning": "yellow",
|
||||||
|
"error": "bold red",
|
||||||
|
"error_msg": "red",
|
||||||
|
"sync": "bold magenta",
|
||||||
|
"wait": "italic grey50"
|
||||||
|
})
|
||||||
|
|
||||||
|
console = Console(theme=custom_theme, highlight=False)
|
||||||
|
console_err = Console(theme=custom_theme, stderr=True, highlight=False)
|
||||||
3
tool/requirements.txt
Normal file
3
tool/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
PyYAML
|
||||||
|
pyserial
|
||||||
|
rich
|
||||||
@@ -1,50 +1,143 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
import { bus } from '../lib/bus/SerialBus';
|
||||||
import { buzzer } from '../lib/buzzerStore';
|
import { buzzer } from '../lib/buzzerStore';
|
||||||
import { connectToPort, disconnectBuzzer,initSerialListeners } from '../lib/buzzerActions';
|
import { GetProtocolCommand } from '../lib/protocol/commands/GetProtocol';
|
||||||
import { PlugsIcon, PlugsConnectedIcon } from 'phosphor-svelte';
|
import { addToast } from '../lib/toastStore';
|
||||||
|
import {
|
||||||
|
PlugsIcon,
|
||||||
|
PlugsConnectedIcon,
|
||||||
|
CaretDownIcon,
|
||||||
|
BluetoothIcon,
|
||||||
|
TrashIcon,
|
||||||
|
PlusCircleIcon
|
||||||
|
} from 'phosphor-svelte';
|
||||||
|
import { slide } from 'svelte/transition';
|
||||||
|
import { initializeBuzzer } from '../lib/buzzerActions';
|
||||||
|
|
||||||
const BUZZER_FILTER = [
|
const BUZZER_FILTER = [{ usbVendorId: 0x1209, usbProductId: 0xEDED }];
|
||||||
{ usbVendorId: 0x2fe3, usbProductId: 0x0001 },
|
|
||||||
];
|
|
||||||
onMount(() => {
|
|
||||||
initSerialListeners();
|
|
||||||
});
|
|
||||||
|
|
||||||
async function handleConnectClick() {
|
let showMenu = false;
|
||||||
try {
|
let menuElement: HTMLElement;
|
||||||
if ($buzzer.connected) {
|
|
||||||
console.log("Trenne verbindung zum aktuellen Buzzer...");
|
|
||||||
await disconnectBuzzer();
|
|
||||||
console.log("Verbindung getrennt");
|
|
||||||
}
|
|
||||||
|
|
||||||
const port = await navigator.serial.requestPort({ filters: BUZZER_FILTER });
|
// Schließt das Menü bei Klick außerhalb
|
||||||
console.log("Port ausgewählt, versuche Verbindung.", port.getInfo());
|
function handleOutsideClick(event: MouseEvent) {
|
||||||
await connectToPort(port);
|
if (showMenu && menuElement && !menuElement.contains(event.target as Node)) {
|
||||||
} catch (e) {
|
showMenu = false;
|
||||||
// Verhindert das Error-Logging, wenn der User einfach nur "Abbrechen" klickt
|
|
||||||
if (e instanceof Error && e.name === 'NotFoundError') {
|
|
||||||
console.log("Keine Verbindung ausgewählt, Abbruch durch Nutzer.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.error("Verbindung abgebrochen", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function connectTo(port: SerialPort) {
|
||||||
|
try {
|
||||||
|
await bus.connect(port);
|
||||||
|
// Kurze Pause für die Hardware-Bereitschaft
|
||||||
|
await new Promise(r => setTimeout(r, 100));
|
||||||
|
|
||||||
|
// Logische Initialisierung starten
|
||||||
|
await initializeBuzzer();
|
||||||
|
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error("Port-Fehler:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleMainAction() {
|
||||||
|
if ($buzzer.connected) {
|
||||||
|
await bus.disconnect();
|
||||||
|
buzzer.update(s => ({ ...s, connected: false }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ports = await navigator.serial.getPorts();
|
||||||
|
if (ports.length > 0) {
|
||||||
|
await connectTo(ports[0]);
|
||||||
|
} else {
|
||||||
|
await pairNewDevice();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pairNewDevice() {
|
||||||
|
showMenu = false;
|
||||||
|
try {
|
||||||
|
const port = await navigator.serial.requestPort({ filters: BUZZER_FILTER });
|
||||||
|
await connectTo(port);
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Pairing abgebrochen");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function forgetDevice() {
|
||||||
|
showMenu = false;
|
||||||
|
const ports = await navigator.serial.getPorts();
|
||||||
|
for (const port of ports) {
|
||||||
|
if ('forget' in port) {
|
||||||
|
await (port as any).forget();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($buzzer.connected) {
|
||||||
|
await bus.disconnect();
|
||||||
|
buzzer.update(s => ({ ...s, connected: false }));
|
||||||
|
}
|
||||||
|
addToast("Geräte entkoppelt", "info");
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
window.addEventListener('click', handleOutsideClick);
|
||||||
|
return () => window.removeEventListener('click', handleOutsideClick);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button
|
<div class="relative inline-flex shadow-lg" bind:this={menuElement}>
|
||||||
on:click={handleConnectClick}
|
<button
|
||||||
class="flex items-center gap-2 px-4 py-2 rounded-xl transition-all
|
on:click={handleMainAction}
|
||||||
{$buzzer.connected
|
class="flex items-center gap-3 px-5 py-2.5 rounded-l-xl transition-all border-r border-white/10
|
||||||
? 'bg-emerald-500/20 text-emerald-400 border border-emerald-500/50'
|
{$buzzer.connected
|
||||||
: 'bg-slate-700 hover:bg-slate-600 text-slate-200 border border-slate-600'}"
|
? 'bg-emerald-600 hover:bg-emerald-500 text-white'
|
||||||
>
|
: 'bg-slate-700 hover:bg-slate-600 text-slate-200'}"
|
||||||
{#if $buzzer.connected}
|
>
|
||||||
<PlugsConnectedIcon size={18} weight="fill" />
|
{#if $buzzer.connected}
|
||||||
<span class="text-xs font-bold uppercase tracking-wider text-emerald-300">Verbunden</span>
|
<PlugsConnectedIcon size={20} weight="fill" />
|
||||||
{:else}
|
<span class="text-sm font-bold uppercase tracking-wide">Trennen</span>
|
||||||
<PlugsIcon size={18} weight="bold" />
|
{:else}
|
||||||
<span class="text-xs font-bold uppercase tracking-wider">Verbinden</span>
|
<PlugsIcon size={20} weight="bold" />
|
||||||
|
<span class="text-sm font-bold uppercase tracking-wide">Verbinden</span>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
on:click={() => showMenu = !showMenu}
|
||||||
|
class="px-3 py-2.5 rounded-r-xl transition-all
|
||||||
|
{$buzzer.connected
|
||||||
|
? 'bg-emerald-600 hover:bg-emerald-500 text-white'
|
||||||
|
: 'bg-slate-700 hover:bg-slate-600 text-slate-200'}"
|
||||||
|
>
|
||||||
|
<CaretDownIcon size={16} weight="bold" class="transition-transform {showMenu ? 'rotate-180' : ''}" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{#if showMenu}
|
||||||
|
<div
|
||||||
|
transition:slide={{ duration: 150 }}
|
||||||
|
class="absolute top-full right-0 mt-2 w-56 bg-slate-800 border border-slate-700 rounded-xl overflow-hidden z-50 shadow-2xl"
|
||||||
|
>
|
||||||
|
<div class="p-2 flex flex-col gap-1">
|
||||||
|
<button
|
||||||
|
on:click={pairNewDevice}
|
||||||
|
class="flex items-center gap-3 w-full px-3 py-2 text-sm text-slate-300 hover:bg-slate-700 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<PlusCircleIcon size={18} class="text-emerald-400" />
|
||||||
|
Neuen Buzzer koppeln
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="h-px bg-slate-700 my-1"></div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
on:click={forgetDevice}
|
||||||
|
class="flex items-center gap-3 w-full px-3 py-2 text-sm text-rose-400 hover:bg-rose-500/10 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<TrashIcon size={18} />
|
||||||
|
Buzzer entkoppeln
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</div>
|
||||||
@@ -1,17 +1,25 @@
|
|||||||
<script>
|
<script>
|
||||||
import { buzzer } from '../lib/buzzerStore';
|
import { buzzer } from '../lib/buzzerStore';
|
||||||
|
import { CpuIcon } from 'phosphor-svelte';
|
||||||
</script>
|
</script>
|
||||||
|
<div class="h-48 bg-indigo-950/20 border border-indigo-500/20 rounded-[2rem] p-6 relative overflow-hidden shrink-0">
|
||||||
<div class="text-[11px] font-mono text-slate-400 space-y-1 relative z-10">
|
<div class="absolute top-6 right-6 text-indigo-500 opacity-20">
|
||||||
<p>
|
<CpuIcon class="w-12 h-12" weight="fill" />
|
||||||
Firmware: <span class="text-indigo-300">{$buzzer.version}</span>
|
</div>
|
||||||
</p>
|
<h3 class="text-indigo-400 text-[10px] font-black uppercase tracking-[0.2em] mb-4">
|
||||||
<p>
|
Device Info
|
||||||
Protocol: <span class="text-indigo-300">{$buzzer.protocol}</span>
|
</h3>
|
||||||
</p>
|
<div class="text-[11px] font-mono text-slate-400 space-y-1 relative z-10 transition-all duration-300 {!$buzzer.connected ? 'blur-[1px] opacity-30 grayscale pointer-events-none' : ''}">
|
||||||
<p>
|
<p>
|
||||||
Status: <span class="{$buzzer.connected ? 'text-emerald-400' : 'text-red-400'}">
|
Firmware: <span class="text-indigo-300">{$buzzer.version}</span>
|
||||||
{$buzzer.connected ? 'Confirmed' : 'Disconnected'}
|
</p>
|
||||||
</span>
|
<p>
|
||||||
</p>
|
Protocol: <span class="text-indigo-300">{$buzzer.protocol}</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Status: <span class="{$buzzer.connected ? 'text-emerald-400' : 'text-red-400'}">
|
||||||
|
{$buzzer.connected ? 'Confirmed' : 'Disconnected'}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
$: isDisconnected = !$buzzer.connected;
|
$: isDisconnected = !$buzzer.connected;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col gap-1.5 w-96 transition-all duration-700 {isDisconnected ? 'blur-sm opacity-30 grayscale pointer-events-none' : ''}">
|
<div class="flex flex-col gap-1.5 w-96 transition-all duration-300 {isDisconnected ? 'blur-[1px] opacity-30 grayscale pointer-events-none' : ''}">
|
||||||
|
|
||||||
<div class="h-3.5 w-full bg-slate-800 rounded-full overflow-hidden flex border border-slate-700 shadow-inner">
|
<div class="h-3.5 w-full bg-slate-800 rounded-full overflow-hidden flex border border-slate-700 shadow-inner">
|
||||||
<div class="h-full bg-slate-300 transition-all duration-500" style="width: {pMeta}%"></div>
|
<div class="h-full bg-slate-300 transition-all duration-500" style="width: {pMeta}%"></div>
|
||||||
|
|||||||
@@ -1,10 +1,31 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { buzzer } from '../lib/buzzerStore';
|
import { buzzer } from '../lib/buzzerStore';
|
||||||
import { playFile, deleteFile } from '../lib/buzzerActions';
|
import { InfoIcon, MusicNotesIcon, WrenchIcon, PlayIcon, TrashIcon, ArrowsLeftRightIcon, QuestionMarkIcon } from "phosphor-svelte";
|
||||||
import { MusicNotesIcon, WrenchIcon, PlayIcon, TrashIcon, ArrowsLeftRightIcon, QuestionMarkIcon } from "phosphor-svelte";
|
import { PlayFileCommand } from '../lib/protocol/commands/PlayFile';
|
||||||
|
|
||||||
export let file: { name: string, size: string, isSystem: boolean, crc32?: number};
|
export let file: { name: string, size: string, isSystem: boolean, crc32?: number};
|
||||||
export let selected = false;
|
export let selected = false;
|
||||||
|
|
||||||
|
async function handlePlay() {
|
||||||
|
try {
|
||||||
|
const cmd = new PlayFileCommand();
|
||||||
|
|
||||||
|
// 1. WICHTIG: Der Buzzer braucht den absoluten Pfad!
|
||||||
|
const fullPath = `/lfs/a/${file.name}`;
|
||||||
|
|
||||||
|
console.log("Sende Play-Befehl für:", fullPath);
|
||||||
|
const success = await cmd.execute(fullPath);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
// Optional: Erfolg kurz im Log zeigen
|
||||||
|
console.log("Wiedergabe läuft...");
|
||||||
|
} else {
|
||||||
|
addToast(`Buzzer konnte ${file.name} nicht abspielen.`, "error");
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
addToast(`Fehler: ${e.message}`, "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -47,18 +68,25 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity shrink-0 ml-4">
|
<div class="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity shrink-0 ml-4">
|
||||||
|
<button
|
||||||
|
class="p-2 hover:bg-gray-500/20 rounded-lg text-white-400 transition-colors"
|
||||||
|
title="Datei-Infos"
|
||||||
|
>
|
||||||
|
<InfoIcon size={16} />
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
on:click|stopPropagation={() => playFile(file.name)}
|
on:click|stopPropagation={handlePlay}
|
||||||
class="p-2 hover:bg-blue-500/20 rounded-lg text-blue-400 transition-colors"
|
class="p-2 hover:bg-blue-500/20 rounded-lg text-blue-400 transition-colors"
|
||||||
title="Play Sound"
|
title="Auf dem Buzzer abspielen"
|
||||||
>
|
>
|
||||||
<PlayIcon size={16} weight="fill" />
|
<PlayIcon size={16} weight="fill" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
on:click|stopPropagation={() => deleteFile(file.name)}
|
// on:click|stopPropagation={() => deleteFile(file.name)}
|
||||||
class="p-2 hover:bg-red-500/20 rounded-lg text-red-400 transition-colors"
|
class="p-2 hover:bg-red-500/20 rounded-lg text-red-400 transition-colors"
|
||||||
title="Delete File"
|
title="Datei vom Buzzer löschen"
|
||||||
>
|
>
|
||||||
<TrashIcon size={16} weight="fill" />
|
<TrashIcon size={16} weight="fill" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { buzzer } from '../lib/buzzerStore';
|
import { buzzer } from '../lib/buzzerStore';
|
||||||
import { refreshFileList } from '../lib/buzzerActions';
|
|
||||||
import FileRow from './FileRow.svelte';
|
import FileRow from './FileRow.svelte';
|
||||||
import { ArrowsCounterClockwiseIcon } from 'phosphor-svelte';
|
import { ArrowsCounterClockwiseIcon } from 'phosphor-svelte';
|
||||||
|
import { refreshFileList } from '../lib/buzzerActions';
|
||||||
|
|
||||||
async function handleRefresh() {
|
async function handleRefresh() {
|
||||||
console.log("Aktualisiere Dateiliste...");
|
await refreshFileList();
|
||||||
await refreshFileList();
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
134
webpage/src/lib/bus/SerialBus.ts
Normal file
134
webpage/src/lib/bus/SerialBus.ts
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import { SYNC_SEQ, FrameType } from '../protocol/constants';
|
||||||
|
import { buzzer } from '../buzzerStore';
|
||||||
|
|
||||||
|
class SerialBus {
|
||||||
|
public port: SerialPort | null = null;
|
||||||
|
private reader: ReadableStreamDefaultReader<Uint8Array> | null = null;
|
||||||
|
private internalBuffer: Uint8Array = new Uint8Array(0);
|
||||||
|
|
||||||
|
async connect(port: SerialPort) {
|
||||||
|
if (this.port) await this.disconnect();
|
||||||
|
await port.open({ baudRate: 115200 });
|
||||||
|
this.port = port;
|
||||||
|
this.internalBuffer = new Uint8Array(0);
|
||||||
|
this.reader = null;
|
||||||
|
port.addEventListener('disconnect', () => {
|
||||||
|
console.warn("Hardware-Verbindung verloren!");
|
||||||
|
this.disconnect();
|
||||||
|
buzzer.update(s => ({ ...s, connected: false }));
|
||||||
|
});
|
||||||
|
(window as any).buzzerBus = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hilfsmethode: Stellt sicher, dass wir einen aktiven Reader haben ohne zu crashen
|
||||||
|
private async ensureReader() {
|
||||||
|
if (!this.port?.readable) throw new Error("Port nicht lesbar");
|
||||||
|
if (!this.reader) {
|
||||||
|
this.reader = this.port.readable.getReader();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendRequest(cmd: number, payload: Uint8Array = new Uint8Array(0)) {
|
||||||
|
if (!this.port?.writable) throw new Error("Port nicht bereit");
|
||||||
|
const writer = this.port.writable.getWriter();
|
||||||
|
const header = new Uint8Array([FrameType.REQUEST, cmd]);
|
||||||
|
const frame = new Uint8Array(SYNC_SEQ.length + header.length + payload.length);
|
||||||
|
frame.set(SYNC_SEQ, 0);
|
||||||
|
frame.set(header, SYNC_SEQ.length);
|
||||||
|
frame.set(payload, SYNC_SEQ.length + header.length);
|
||||||
|
await writer.write(frame);
|
||||||
|
writer.releaseLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
async waitForSync(timeoutMs = 2000): Promise<boolean> {
|
||||||
|
await this.ensureReader();
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
while (Date.now() - startTime < timeoutMs) {
|
||||||
|
// 1. Zuerst im Puffer schauen (verhindert Datenverlust zwischen Frames!)
|
||||||
|
for (let i = 0; i <= this.internalBuffer.length - SYNC_SEQ.length; i++) {
|
||||||
|
if (this.internalBuffer[i] === SYNC_SEQ[0] && this.internalBuffer[i+1] === SYNC_SEQ[1] &&
|
||||||
|
this.internalBuffer[i+2] === SYNC_SEQ[2] && this.internalBuffer[i+3] === SYNC_SEQ[3]) {
|
||||||
|
this.internalBuffer = this.internalBuffer.subarray(i + SYNC_SEQ.length);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Neue Daten lesen
|
||||||
|
const { value, done } = await this.reader!.read();
|
||||||
|
if (done) break;
|
||||||
|
if (value) {
|
||||||
|
const next = new Uint8Array(this.internalBuffer.length + value.length);
|
||||||
|
next.set(this.internalBuffer);
|
||||||
|
next.set(value, this.internalBuffer.length);
|
||||||
|
this.internalBuffer = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async readExact(len: number): Promise<Uint8Array> {
|
||||||
|
await this.ensureReader();
|
||||||
|
while (this.internalBuffer.length < len) {
|
||||||
|
const { value, done } = await this.reader!.read();
|
||||||
|
if (done || !value) throw new Error("Stream closed");
|
||||||
|
const next = new Uint8Array(this.internalBuffer.length + value.length);
|
||||||
|
next.set(this.internalBuffer);
|
||||||
|
next.set(value, this.internalBuffer.length);
|
||||||
|
this.internalBuffer = next;
|
||||||
|
}
|
||||||
|
const res = this.internalBuffer.subarray(0, len);
|
||||||
|
this.internalBuffer = this.internalBuffer.subarray(len);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
public releaseReadLock() {
|
||||||
|
if (this.reader) {
|
||||||
|
this.reader.releaseLock();
|
||||||
|
this.reader = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async disconnect() {
|
||||||
|
this.releaseReadLock();
|
||||||
|
if (this.port) {
|
||||||
|
try { await this.port.close(); } catch (e) {}
|
||||||
|
this.port = null;
|
||||||
|
}
|
||||||
|
this.internalBuffer = new Uint8Array(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingBus = typeof window !== 'undefined' ? (window as any).buzzerBus : null;
|
||||||
|
|
||||||
|
export const bus: SerialBus = existingBus || new SerialBus();
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
(window as any).buzzerBus = bus;
|
||||||
|
|
||||||
|
(window as any).initDebug = async () => {
|
||||||
|
console.log("🚀 Lade Debug-Kommandos...");
|
||||||
|
|
||||||
|
// Nutze hier am besten absolute Pfade ab /src/
|
||||||
|
const [proto, settings, list, play, flash] = await Promise.all([
|
||||||
|
import('../protocol/commands/GetProtocol.ts'),
|
||||||
|
import('../protocol/commands/GetSettings.ts'),
|
||||||
|
import('../protocol/commands/ListDir.ts'),
|
||||||
|
import('../protocol/commands/PlayFile.ts'), // Pfad prüfen!
|
||||||
|
import('../protocol/commands/GetFlashInfo.ts') // Pfad prüfen!
|
||||||
|
]);
|
||||||
|
|
||||||
|
(window as any).GetProtocolCommand = proto.GetProtocolCommand;
|
||||||
|
(window as any).GetSettingCommand = settings.GetSettingCommand;
|
||||||
|
(window as any).ListDirCommand = list.ListDirCommand;
|
||||||
|
(window as any).PlayFileCommand = play.PlayFileCommand;
|
||||||
|
(window as any).GetFlashInfoCommand = flash.GetFlashInfoCommand;
|
||||||
|
|
||||||
|
console.log("✅ Alle Commands geladen und an window gebunden.");
|
||||||
|
};
|
||||||
|
|
||||||
|
(window as any).run = async (CommandClass: any, ...args: any[]) => {
|
||||||
|
const cmd = new CommandClass();
|
||||||
|
return await cmd.execute(...args);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,362 +1,46 @@
|
|||||||
import { buzzer } from './buzzerStore';
|
|
||||||
import { get } from 'svelte/store';
|
|
||||||
import { addToast } from './toastStore';
|
|
||||||
|
|
||||||
let isConnecting = false;
|
|
||||||
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
|
||||||
|
|
||||||
type Task = {
|
|
||||||
command: string;
|
|
||||||
priority: number; // 0 = Hintergrund, 1 = User-Aktion
|
|
||||||
resolve: (lines: string[]) => void;
|
|
||||||
key?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
class SerialQueue {
|
|
||||||
private queue: Task[] = [];
|
|
||||||
private isProcessing = false;
|
|
||||||
private port: SerialPort | null = null;
|
|
||||||
|
|
||||||
private reader: ReadableStreamDefaultReader<Uint8Array> | null = null;
|
|
||||||
private writer: WritableStreamDefaultWriter<Uint8Array> | null = null;
|
|
||||||
|
|
||||||
setPort(port: SerialPort | null) { this.port = port; }
|
|
||||||
|
|
||||||
async add(command: string, priority = 1, key?: string): Promise<string[]> {
|
|
||||||
if (key) {
|
|
||||||
this.queue = this.queue.filter(t => t.key !== key);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const task = { command, priority, resolve, key };
|
|
||||||
if (priority === 1) {
|
|
||||||
const lastUserTaskIndex = this.queue.findLastIndex(t => t.priority === 1);
|
|
||||||
this.queue.splice(lastUserTaskIndex + 1, 0, task);
|
|
||||||
} else {
|
|
||||||
this.queue.push(task);
|
|
||||||
}
|
|
||||||
this.process();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async process() {
|
|
||||||
if (this.isProcessing || !this.port || this.queue.length === 0) return;
|
|
||||||
this.isProcessing = true;
|
|
||||||
|
|
||||||
const task = this.queue.shift()!;
|
|
||||||
try {
|
|
||||||
const encoder = new TextEncoder();
|
|
||||||
const decoder = new TextDecoder();
|
|
||||||
|
|
||||||
// Reader und Writer in der Instanz speichern
|
|
||||||
this.writer = this.port.writable.getWriter();
|
|
||||||
this.reader = this.port.readable.getReader();
|
|
||||||
|
|
||||||
await this.writer.write(encoder.encode(task.command + "\n"));
|
|
||||||
|
|
||||||
// Writer sofort wieder freigeben nach dem Senden
|
|
||||||
this.writer.releaseLock();
|
|
||||||
this.writer = null;
|
|
||||||
|
|
||||||
let raw = "";
|
|
||||||
while (true) {
|
|
||||||
// Hier könnte die Queue hängen bleiben, wenn das Gerät nicht antwortet
|
|
||||||
const { value, done } = await this.reader.read();
|
|
||||||
if (done) break;
|
|
||||||
raw += decoder.decode(value);
|
|
||||||
if (raw.includes("OK") || raw.includes("ERR")) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.reader.releaseLock();
|
|
||||||
this.reader = null;
|
|
||||||
|
|
||||||
const lines = raw.split('\n').map(l => l.trim()).filter(l => l);
|
|
||||||
const errorLine = lines.find(l => l.startsWith("ERR"));
|
|
||||||
if (errorLine) addToast(`Gerätefehler: ${errorLine}`, 'error', 5000);
|
|
||||||
|
|
||||||
task.resolve(lines.filter(l => l !== "OK" && !l.startsWith("ERR") && !l.startsWith(task.command)));
|
|
||||||
} catch (e) {
|
|
||||||
// Im Fehlerfall Locks sicher aufheben
|
|
||||||
this.cleanupLocks();
|
|
||||||
if (e instanceof Error && e.name !== 'AbortError') {
|
|
||||||
console.error("Queue Error:", e);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
this.isProcessing = false;
|
|
||||||
this.process();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hilfsmethode zum Aufräumen der Sperren
|
|
||||||
private cleanupLocks() {
|
|
||||||
if (this.reader) {
|
|
||||||
try { this.reader.releaseLock(); } catch { }
|
|
||||||
this.reader = null;
|
|
||||||
}
|
|
||||||
if (this.writer) {
|
|
||||||
try { this.writer.releaseLock(); } catch { }
|
|
||||||
this.writer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async close() {
|
|
||||||
this.queue = [];
|
|
||||||
if (this.port) {
|
|
||||||
try {
|
|
||||||
// Erst die Streams abbrechen, um laufende Reads zu beenden
|
|
||||||
if (this.reader) {
|
|
||||||
await this.reader.cancel();
|
|
||||||
}
|
|
||||||
if (this.writer) {
|
|
||||||
await this.writer.abort();
|
|
||||||
}
|
|
||||||
// Dann die Locks freigeben
|
|
||||||
this.cleanupLocks();
|
|
||||||
await this.port.close();
|
|
||||||
// console.log("Port erfolgreich geschlossen");
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Port-Fehler beim Schließen:", e);
|
|
||||||
this.cleanupLocks();
|
|
||||||
}
|
|
||||||
this.port = null;
|
|
||||||
}
|
|
||||||
this.isProcessing = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const queue = new SerialQueue();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialisiert die globalen Serial-Listener (aufgerufen beim Start der App)
|
* Initialisiert den Buzzer nach dem physikalischen Verbindungsaufbau.
|
||||||
*/
|
*/
|
||||||
export function initSerialListeners() {
|
export async function initializeBuzzer() {
|
||||||
if (typeof navigator === 'undefined' || !navigator.serial) return;
|
try {
|
||||||
|
const version = await new GetProtocolCommand().execute();
|
||||||
|
|
||||||
// 1. Wenn ein bereits gekoppeltes Gerät eingesteckt wird
|
if (version !== null) {
|
||||||
navigator.serial.addEventListener('connect', (event) => {
|
buzzer.update(s => ({ ...s, connected: true, protocol: version }));
|
||||||
// console.log('Neues Gerät erkannt, starte Auto-Connect...');
|
|
||||||
autoConnect();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Beim Laden der Seite prüfen, ob wir bereits Zugriff auf Geräte haben
|
// FIX 1: Flash-Info muss auch beim Start geladen werden!
|
||||||
autoConnect();
|
await refreshFileList();
|
||||||
}
|
await refreshFlashInfo();
|
||||||
|
|
||||||
/**
|
addToast(`Buzzer bereit (v${version})`, 'success');
|
||||||
* Versucht eine Verbindung zu bereits gekoppelten Geräten herzustellen
|
return true;
|
||||||
*/
|
|
||||||
export async function autoConnect() {
|
|
||||||
if (typeof navigator === 'undefined' || !navigator.serial) return;
|
|
||||||
|
|
||||||
const ports = await navigator.serial.getPorts();
|
|
||||||
if (ports.length > 0) {
|
|
||||||
const port = ports[0];
|
|
||||||
const retryDelays = [100, 500];
|
|
||||||
|
|
||||||
// Erster Versuch + 2 Retries = max 3 Versuche
|
|
||||||
for (let i = 0; i <= retryDelays.length; i++) {
|
|
||||||
try {
|
|
||||||
// console.log("Auto-Connect Versuch mit Port:", port.getInfo());
|
|
||||||
await connectToPort(port);
|
|
||||||
return; // Erfolg!
|
|
||||||
} catch (e) {
|
|
||||||
if (i < retryDelays.length) {
|
|
||||||
// console.log(`Reconnect Versuch ${i + 1} fehlgeschlagen, warte ${retryDelays[i]}ms...`);
|
|
||||||
await delay(retryDelays[i]);
|
|
||||||
} else {
|
|
||||||
console.error('Auto-Connect nach Retries endgültig fehlgeschlagen.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error("Initialisierung fehlgeschlagen:", e);
|
||||||
|
addToast(`Fehler: ${e.message}`, "error");
|
||||||
|
await bus.disconnect();
|
||||||
|
buzzer.update(s => ({ ...s, connected: false }));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export async function refreshFlashInfo() {
|
||||||
* Kernfunktion für den Verbindungsaufbau
|
try {
|
||||||
*/
|
const flashInfo = await new GetFlashInfoCommand().execute();
|
||||||
export async function connectToPort(port: SerialPort) {
|
if (flashInfo) {
|
||||||
if (isConnecting || get(buzzer).connected) return;
|
const totalSize = (flashInfo.total_size / (1024 * 1024));
|
||||||
isConnecting = true;
|
const freeSize = (flashInfo.free_size / (1024 * 1024));
|
||||||
|
const fwSlotSize = (flashInfo.fw_slot_size / 1024);
|
||||||
|
const maxPathLength = flashInfo.max_path_length;
|
||||||
|
|
||||||
try {
|
buzzer.update(s => ({
|
||||||
// console.log("Versuche Verbindung mit Port:", port.getInfo());
|
|
||||||
await port.open({ baudRate: 115200 });
|
|
||||||
await delay(100);
|
|
||||||
setActivePort(port);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Validierung: Antwortet das Teil auf "info"?
|
|
||||||
const success = await Promise.race([
|
|
||||||
updateDeviceInfo(port),
|
|
||||||
new Promise<boolean>((_, reject) => setTimeout(() => reject(new Error("Timeout")), 1500))
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!success) throw new Error("Kein Buzzer");
|
|
||||||
|
|
||||||
port.addEventListener('disconnect', () => {
|
|
||||||
addToast("Buzzer-Verbindung verloren!", "warning");
|
|
||||||
handleDisconnect();
|
|
||||||
});
|
|
||||||
|
|
||||||
buzzer.update(s => ({ ...s, connected: true }));
|
|
||||||
addToast("Buzzer erfolgreich verbunden", "success");
|
|
||||||
|
|
||||||
await refreshFileList();
|
|
||||||
} catch (validationError) {
|
|
||||||
addToast("Buzzer-Validierung fehlgeschlagen!", "error");
|
|
||||||
await disconnectBuzzer();
|
|
||||||
|
|
||||||
try {
|
|
||||||
if ('forget' in port) { // Check für Browser-Support
|
|
||||||
await (port as any).forget();
|
|
||||||
console.log("Gerät wurde erfolgreich entkoppelt.");
|
|
||||||
}
|
|
||||||
} catch (forgetError) {
|
|
||||||
console.error("Entkoppeln fehlgeschlagen:", forgetError);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error("Device ist kein gültiger Buzzer");
|
|
||||||
throw validationError; // Fehler an den äußeren Block weitergeben
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
setActivePort(null);
|
|
||||||
// Hier landen wir, wenn der User den Port-Dialog abbricht oder die Validierung fehlschlägt
|
|
||||||
throw e;
|
|
||||||
} finally {
|
|
||||||
isConnecting = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDisconnect() {
|
|
||||||
setActivePort(null);
|
|
||||||
buzzer.update(s => ({
|
|
||||||
...s,
|
...s,
|
||||||
connected: false,
|
storage: { total: totalSize, available: freeSize }, // FIX 2: "storage" korrekt geschrieben
|
||||||
files: [] // Liste leeren, da Gerät weg
|
fw_slot_size: fwSlotSize,
|
||||||
}));
|
max_path_length: maxPathLength
|
||||||
}
|
}));
|
||||||
|
|
||||||
// --- EXPORTE ---
|
|
||||||
|
|
||||||
export function setActivePort(port: SerialPort | null) {
|
|
||||||
queue.setPort(port);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function disconnectBuzzer() {
|
|
||||||
await queue.close();
|
|
||||||
handleDisconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateDeviceInfo(port: SerialPort): Promise<boolean> {
|
|
||||||
const lines = await queue.add("info", 1);
|
|
||||||
if (lines.length > 0) {
|
|
||||||
const parts = lines[0].split(';');
|
|
||||||
if (parts.length >= 6) {
|
|
||||||
const pageSize = parseInt(parts[2]);
|
|
||||||
const totalPages = parseInt(parts[3]);
|
|
||||||
const availablePages = parseInt(parts[4]);
|
|
||||||
|
|
||||||
// MB Berechnung mit dem korrekten Divisor (1024 * 1024)
|
|
||||||
const totalMB = (totalPages * pageSize) / 1048576;
|
|
||||||
const availableMB = (availablePages * pageSize) / 1048576;
|
|
||||||
|
|
||||||
buzzer.update(s => ({
|
|
||||||
...s,
|
|
||||||
version: parts[1],
|
|
||||||
protocol: parseInt(parts[0]),
|
|
||||||
storage: { ...s.storage, total: totalMB, available: availableMB }
|
|
||||||
}));
|
|
||||||
return true; // Validierung erfolgreich
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return false; // Keine gültigen Daten erhalten
|
} catch (e) { // FIX 3: try/catch Block sauber schließen
|
||||||
}
|
console.error("Fehler beim Abrufen der Flash-Info:", e);
|
||||||
|
}
|
||||||
export async function refreshFileList() {
|
} // Funktion schließen
|
||||||
let totalSystemBytes = 0;
|
|
||||||
let totalAudioBytes = 0;
|
|
||||||
|
|
||||||
// 1. System-Größe abfragen (nur Summieren, keine Liste speichern)
|
|
||||||
const syslines = await queue.add("ls /lfs/sys", 1, 'ls');
|
|
||||||
syslines.forEach(line => {
|
|
||||||
const parts = line.split(',');
|
|
||||||
if (parts.length >= 2) {
|
|
||||||
totalSystemBytes += parseInt(parts[1]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 2. Audio-Files abfragen und Liste für das UI erstellen
|
|
||||||
const lines = await queue.add("ls /lfs/a", 1, 'ls');
|
|
||||||
const audioFiles = lines.map(line => {
|
|
||||||
const parts = line.split(',');
|
|
||||||
if (parts.length < 3) return null;
|
|
||||||
const size = parseInt(parts[1]);
|
|
||||||
totalAudioBytes += size;
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: parts[2],
|
|
||||||
size: (size / 1024).toFixed(1) + " KB",
|
|
||||||
crc32: 0,
|
|
||||||
isSystem: false
|
|
||||||
};
|
|
||||||
}).filter(f => f !== null) as any[];
|
|
||||||
|
|
||||||
// 3. Den Store mit MB-Werten aktualisieren
|
|
||||||
buzzer.update(s => {
|
|
||||||
// Konvertierung in MB (1024 * 1024 = 1048576)
|
|
||||||
const audioMB = totalAudioBytes / 1048576;
|
|
||||||
const sysMB = totalSystemBytes / 1048576;
|
|
||||||
const usedTotalMB = s.storage.total - s.storage.available;
|
|
||||||
const unknownMB = Math.max(0, usedTotalMB - audioMB - sysMB);
|
|
||||||
console.log(`Storage: Total ${s.storage.total} MB, Used ${usedTotalMB.toFixed(2)} MB, Audio ${audioMB.toFixed(2)} MB, System ${sysMB.toFixed(2)} MB, Unknown ${unknownMB.toFixed(2)} MB`);
|
|
||||||
return {
|
|
||||||
...s,
|
|
||||||
files: audioFiles,
|
|
||||||
storage: {
|
|
||||||
...s.storage,
|
|
||||||
usedSys: sysMB,
|
|
||||||
usedAudio: audioMB,
|
|
||||||
unknown: unknownMB
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
startBackgroundCrcCheck();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function startBackgroundCrcCheck() {
|
|
||||||
const currentFiles = get(buzzer).files;
|
|
||||||
for (const file of currentFiles) {
|
|
||||||
if (true) {//(!file.crc32) {
|
|
||||||
const tagresponse = await queue.add(`gett /lfs/a/${file.name}`, 0);
|
|
||||||
if (tagresponse.length > 0) {
|
|
||||||
console.log(`Tag für ${file.name}:`, tagresponse[0]);
|
|
||||||
}
|
|
||||||
const response = await queue.add(`check /lfs/a/${file.name}`, 0);
|
|
||||||
if (response.length > 0) {
|
|
||||||
const match = response[0].match(/0x([0-9a-fA-F]+)/);
|
|
||||||
if (match) {
|
|
||||||
const crc = parseInt(match[1], 16);
|
|
||||||
updateFileCrc(file.name, crc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateFileCrc(name: string, crc: number) {
|
|
||||||
buzzer.update(s => ({
|
|
||||||
...s,
|
|
||||||
files: s.files.map(f => f.name === name ? { ...f, crc32: crc } : f)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function playFile(filename: string) {
|
|
||||||
return queue.add(`play /lfs/a/${filename}`, 1, 'play');
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteFile(filename: string) {
|
|
||||||
if (!confirm(`Datei ${filename} wirklich löschen?`)) return;
|
|
||||||
await queue.add(`rm /lfs/a/${filename}`, 1);
|
|
||||||
await refreshFileList();
|
|
||||||
}
|
|
||||||
@@ -2,15 +2,18 @@ import { writable } from 'svelte/store';
|
|||||||
|
|
||||||
export const buzzer = writable({
|
export const buzzer = writable({
|
||||||
connected: false,
|
connected: false,
|
||||||
version: 'v0.0.0',
|
|
||||||
protocol: 0,
|
protocol: 0,
|
||||||
build: 'unknown',
|
version: 'v0.0.0',
|
||||||
|
kernel_version: 'v0.0.0',
|
||||||
storage: {
|
storage: {
|
||||||
total: 8.0, // 8 MB Flash laut Spezifikation
|
total: 8.0,
|
||||||
available: 0.0,
|
available: 0.0,
|
||||||
unknown: 8.0,
|
|
||||||
usedSys: 0.0,
|
usedSys: 0.0,
|
||||||
usedAudio: 0.0
|
usedAudio: 0.0,
|
||||||
|
unknown: 0.0
|
||||||
},
|
},
|
||||||
files: [] as {name: string, size: string, crc32: number, isSystem: boolean, isSynced: boolean}[]
|
max_path_length: 15,
|
||||||
|
fw_slot_size: 0,
|
||||||
|
|
||||||
|
files: [] as {name: string, size: string, crc32: number | null, isSystem: boolean}[]
|
||||||
});
|
});
|
||||||
49
webpage/src/lib/protocol/commands/GetFlashInfo.ts
Normal file
49
webpage/src/lib/protocol/commands/GetFlashInfo.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { bus } from '../../bus/SerialBus';
|
||||||
|
import { Command, FrameType } from '../constants';
|
||||||
|
import { BinaryUtils } from '../../utils/BinaryUtils';
|
||||||
|
|
||||||
|
export interface FlashInfo {
|
||||||
|
total_size: number; // in Bytes
|
||||||
|
free_size: number; // in Bytes
|
||||||
|
fw_slot_size: number; // in Bytes
|
||||||
|
ext_flash_erase_size: number; // in Bytes
|
||||||
|
int_flash_erase_size: number; // in Bytes
|
||||||
|
max_path_length: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GetFlashInfoCommand {
|
||||||
|
async execute(): Promise<FlashInfo | null> {
|
||||||
|
try {
|
||||||
|
await bus.sendRequest(Command.GET_FLASH_INFO);
|
||||||
|
|
||||||
|
if (await bus.waitForSync()) {
|
||||||
|
const typeArr = await bus.readExact(1);
|
||||||
|
if (typeArr[0] === FrameType.RESPONSE) {
|
||||||
|
// Wir lesen exakt 21 Bytes für die Flash-Info
|
||||||
|
const data = await bus.readExact(21);
|
||||||
|
const pageSize = BinaryUtils.readUint32LE(data.subarray(0, 4));
|
||||||
|
const totalSize = BinaryUtils.readUint32LE(data.subarray(4, 8)) * pageSize;
|
||||||
|
const freeSize = BinaryUtils.readUint32LE(data.subarray(8, 12)) * pageSize; // Aktuell haben wir keine Info über belegten Speicher, also annehmen, dass alles frei ist
|
||||||
|
const fwSlotSize = BinaryUtils.readUint32LE(data.subarray(12, 16));
|
||||||
|
const extEraseSize = BinaryUtils.readUint16LE(data.subarray(16, 18));
|
||||||
|
const intEraseSize = BinaryUtils.readUint16LE(data.subarray(18, 20));
|
||||||
|
const maxPathLength = data[20];
|
||||||
|
console.log("Flash Info:", { pageSize, totalSize, freeSize, fwSlotSize, extEraseSize, intEraseSize, maxPathLength });
|
||||||
|
return {
|
||||||
|
total_size: totalSize,
|
||||||
|
free_size: totalSize, // Aktuell haben wir keine Info über belegten Speicher, also annehmen, dass alles frei ist
|
||||||
|
fw_slot_size: fwSlotSize,
|
||||||
|
ext_flash_erase_size: extEraseSize,
|
||||||
|
int_flash_erase_size: intEraseSize,
|
||||||
|
max_path_length: maxPathLength
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("GetFlashInfoCommand failed:", e);
|
||||||
|
} finally {
|
||||||
|
bus.releaseReadLock();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
26
webpage/src/lib/protocol/commands/GetProtocol.ts
Normal file
26
webpage/src/lib/protocol/commands/GetProtocol.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { bus } from '../../bus/SerialBus';
|
||||||
|
import { Command, FrameType } from '../constants';
|
||||||
|
import { BinaryUtils } from '../../utils/BinaryUtils';
|
||||||
|
|
||||||
|
export class GetProtocolCommand {
|
||||||
|
async execute(): Promise<number | null> {
|
||||||
|
try {
|
||||||
|
await bus.sendRequest(Command.GET_PROTOCOL_VERSION);
|
||||||
|
|
||||||
|
if (await bus.waitForSync()) {
|
||||||
|
const typeArr = await bus.readExact(1);
|
||||||
|
if (typeArr[0] === FrameType.RESPONSE) {
|
||||||
|
// Wir lesen exakt 2 Bytes für die Version
|
||||||
|
const data = await bus.readExact(2);
|
||||||
|
// Nutze die neue Hilfsfunktion
|
||||||
|
return BinaryUtils.readUint16LE(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("GetProtocolCommand failed:", e);
|
||||||
|
} finally {
|
||||||
|
bus.releaseReadLock();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
37
webpage/src/lib/protocol/commands/GetSettings.ts
Normal file
37
webpage/src/lib/protocol/commands/GetSettings.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { bus } from '../../bus/SerialBus';
|
||||||
|
import { Command, FrameType } from '../constants';
|
||||||
|
import { BinaryUtils } from '../../utils/BinaryUtils';
|
||||||
|
|
||||||
|
export class GetSettingCommand {
|
||||||
|
async execute(key: string): Promise<number | boolean | null> {
|
||||||
|
try {
|
||||||
|
const keyBuf = new TextEncoder().encode(key);
|
||||||
|
const payload = new Uint8Array(1 + keyBuf.length);
|
||||||
|
payload[0] = keyBuf.length;
|
||||||
|
payload.set(keyBuf, 1);
|
||||||
|
|
||||||
|
await bus.sendRequest(Command.GET_SETTING, payload);
|
||||||
|
|
||||||
|
if (await bus.waitForSync()) {
|
||||||
|
const typeArr = await bus.readExact(1);
|
||||||
|
if (typeArr[0] === FrameType.RESPONSE) {
|
||||||
|
const lenArr = await bus.readExact(1);
|
||||||
|
const valLen = lenArr[0];
|
||||||
|
const data = await bus.readExact(valLen);
|
||||||
|
|
||||||
|
// Typ-Konvertierung analog zu C/Python
|
||||||
|
if (key === "audio/vol" || key === "play/norepeat") {
|
||||||
|
return data[0] === 1 ? true : (key === "audio/vol" ? data[0] : false);
|
||||||
|
} else if (key === "settings/storage_interval") {
|
||||||
|
return BinaryUtils.readUint16LE(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("GetSetting failed:", e);
|
||||||
|
} finally {
|
||||||
|
bus.releaseReadLock();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
43
webpage/src/lib/protocol/commands/ListDir.ts
Normal file
43
webpage/src/lib/protocol/commands/ListDir.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// src/lib/protocol/commands/ListDir.ts
|
||||||
|
import { bus } from '../../bus/SerialBus';
|
||||||
|
import { Command, FrameType } from '../constants';
|
||||||
|
import { BinaryUtils } from '../../utils/BinaryUtils';
|
||||||
|
|
||||||
|
export class ListDirCommand {
|
||||||
|
async execute(path: string) {
|
||||||
|
try {
|
||||||
|
const p = new TextEncoder().encode(path);
|
||||||
|
const req = new Uint8Array(p.length + 1);
|
||||||
|
req[0] = p.length; req.set(p, 1);
|
||||||
|
|
||||||
|
await bus.sendRequest(Command.LIST_DIR, req);
|
||||||
|
const entries = [];
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
// Wichtig: Wir rufen waitForSync ohne releaseReadLock zwischendurch auf!
|
||||||
|
if (!(await bus.waitForSync())) break;
|
||||||
|
|
||||||
|
const type = (await bus.readExact(1))[0];
|
||||||
|
if (type === FrameType.LIST_START) continue;
|
||||||
|
if (type === FrameType.LIST_END) {
|
||||||
|
const expected = BinaryUtils.readUint16LE(await bus.readExact(2));
|
||||||
|
console.log(`Erwartet: ${expected}, Erhalten: ${entries.length}`);
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
if (type === FrameType.LIST_CHUNK) {
|
||||||
|
const len = BinaryUtils.readUint16LE(await bus.readExact(2));
|
||||||
|
const data = await bus.readExact(len);
|
||||||
|
entries.push({
|
||||||
|
isDir: data[0] === 1,
|
||||||
|
size: data[0] === 1 ? null : BinaryUtils.readUint32LE(data.subarray(1, 5)),
|
||||||
|
name: new TextDecoder().decode(data.subarray(5)).replace(/\0/g, '')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (type === FrameType.ERROR) break;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
bus.releaseReadLock(); // ERST HIER LOCK LÖSEN!
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
31
webpage/src/lib/protocol/commands/PlayFile.ts
Normal file
31
webpage/src/lib/protocol/commands/PlayFile.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { bus } from '../../bus/SerialBus';
|
||||||
|
import { Command, FrameType } from '../constants';
|
||||||
|
|
||||||
|
export class PlayFileCommand {
|
||||||
|
async execute(path: string): Promise<boolean | null> {
|
||||||
|
try {
|
||||||
|
const p = new TextEncoder().encode(path);
|
||||||
|
// Wir brauchen: 1 Byte Flags + 1 Byte Länge + Pfad
|
||||||
|
const req = new Uint8Array(p.length + 2);
|
||||||
|
|
||||||
|
req[0] = 0x01; // 1. Byte: Flags (LSB: 1 = sofort abspielen)
|
||||||
|
req[1] = p.length; // 2. Byte: Länge für get_path() im C-Code
|
||||||
|
req.set(p, 2); // Ab 3. Byte: Der Pfad-String
|
||||||
|
|
||||||
|
await bus.sendRequest(Command.PLAY, req);
|
||||||
|
|
||||||
|
// Warten auf das ACK vom Board
|
||||||
|
if (await bus.waitForSync()) {
|
||||||
|
const typeArr = await bus.readExact(1);
|
||||||
|
if (typeArr[0] === FrameType.ACK) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("PlayFileCommand failed:", e);
|
||||||
|
} finally {
|
||||||
|
bus.releaseReadLock();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
91
webpage/src/lib/protocol/constants.ts
Normal file
91
webpage/src/lib/protocol/constants.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
export const SYNC_SEQ = new TextEncoder().encode('BUZZ');
|
||||||
|
|
||||||
|
export enum FrameType {
|
||||||
|
REQUEST = 0x01,
|
||||||
|
|
||||||
|
ACK = 0x10,
|
||||||
|
RESPONSE = 0x11,
|
||||||
|
STREAM_START = 0x12,
|
||||||
|
STREAM_CHUNK = 0x13,
|
||||||
|
STREAM_END = 0x14,
|
||||||
|
LIST_START = 0x15,
|
||||||
|
LIST_CHUNK = 0x16,
|
||||||
|
LIST_END = 0x17,
|
||||||
|
|
||||||
|
ERROR = 0xFF,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Command {
|
||||||
|
GET_PROTOCOL_VERSION = 0x00,
|
||||||
|
GET_FIRMWARE_STATUS = 0x01,
|
||||||
|
GET_FLASH_INFO = 0x02,
|
||||||
|
CONFIRM_FIRMWARE = 0x03,
|
||||||
|
REBOOT = 0x04,
|
||||||
|
|
||||||
|
LIST_DIR = 0x10,
|
||||||
|
CRC32 = 0x11,
|
||||||
|
MKDIR = 0x12,
|
||||||
|
RM = 0x13,
|
||||||
|
STAT = 0x18,
|
||||||
|
RENAME = 0x19,
|
||||||
|
|
||||||
|
PUT_FILE = 0x20,
|
||||||
|
PUT_FW = 0x21,
|
||||||
|
GET_FILE = 0x22,
|
||||||
|
PUT_TAGS = 0x24,
|
||||||
|
GET_TAGS = 0x25,
|
||||||
|
|
||||||
|
PLAY = 0x30,
|
||||||
|
STOP = 0x31,
|
||||||
|
|
||||||
|
SET_SETTING = 0x40,
|
||||||
|
GET_SETTING = 0x41,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ERRORS: Record<number, string> = {
|
||||||
|
0x00: "NONE",
|
||||||
|
0x01: "INVALID_COMMAND",
|
||||||
|
0x02: "INVALID_PARAMETERS",
|
||||||
|
0x03: "COMMAND_TOO_LONG",
|
||||||
|
|
||||||
|
0x10: "FILE_NOT_FOUND",
|
||||||
|
0x11: "ALREADY_EXISTS",
|
||||||
|
0x12: "NOT_A_DIRECTORY",
|
||||||
|
0x13: "IS_A_DIRECTORY",
|
||||||
|
0x14: "ACCESS_DENIED",
|
||||||
|
0x15: "NO_SPACE",
|
||||||
|
0x16: "FILE_TOO_LARGE",
|
||||||
|
|
||||||
|
0x20: "IO_ERROR",
|
||||||
|
0x21: "TIMEOUT",
|
||||||
|
0x22: "CRC_MISMATCH",
|
||||||
|
0x23: "TRANSFER_ABORTED",
|
||||||
|
|
||||||
|
0x30: "NOT_SUPPORTED",
|
||||||
|
0x31: "BUSY",
|
||||||
|
0x32: "INTERNAL_ERROR",
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum ErrorCode {
|
||||||
|
P_ERR_NONE = 0x00,
|
||||||
|
P_ERR_INVALID_COMMAND = 0x01,
|
||||||
|
P_ERR_INVALID_PARAMETERS = 0x02,
|
||||||
|
P_ERR_COMMAND_TOO_LONG = 0x03,
|
||||||
|
|
||||||
|
P_ERR_FILE_NOT_FOUND = 0x10,
|
||||||
|
P_ERR_ALREADY_EXISTS = 0x11,
|
||||||
|
P_ERR_NOT_A_DIRECTORY = 0x12,
|
||||||
|
P_ERR_IS_A_DIRECTORY = 0x13,
|
||||||
|
P_ERR_ACCESS_DENIED = 0x14,
|
||||||
|
P_ERR_NO_SPACE = 0x15,
|
||||||
|
P_ERR_FILE_TOO_LARGE = 0x16,
|
||||||
|
|
||||||
|
P_ERR_IO = 0x20,
|
||||||
|
P_ERR_TIMEOUT = 0x21,
|
||||||
|
P_ERR_CRC_MISMATCH = 0x22,
|
||||||
|
P_ERR_TRANSFER_ABORTED = 0x23,
|
||||||
|
|
||||||
|
P_ERR_NOT_SUPPORTED = 0x30,
|
||||||
|
P_ERR_BUSY = 0x31,
|
||||||
|
P_ERR_INTERNAL = 0x32,
|
||||||
|
};
|
||||||
34
webpage/src/lib/utils/BinaryUtils.ts
Normal file
34
webpage/src/lib/utils/BinaryUtils.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
export class BinaryUtils {
|
||||||
|
/**
|
||||||
|
* Konvertiert 2 Bytes (Little Endian) in eine Zahl (uint16)
|
||||||
|
*/
|
||||||
|
static readUint16LE(data: Uint8Array, offset = 0): number {
|
||||||
|
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
||||||
|
return view.getUint16(offset, true); // true = Little Endian
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Konvertiert 4 Bytes (Little Endian) in eine Zahl (uint32)
|
||||||
|
*/
|
||||||
|
static readUint32LE(data: Uint8Array, offset = 0): number {
|
||||||
|
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
||||||
|
return view.getUint32(offset, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erstellt ein Uint8Array aus einer Zahl (uint16 LE)
|
||||||
|
*/
|
||||||
|
static writeUint16LE(value: number): Uint8Array {
|
||||||
|
const buf = new Uint8Array(2);
|
||||||
|
const view = new DataView(buf.buffer);
|
||||||
|
view.setUint16(0, value, true);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
static writeUint32LE(value: number): Uint8Array {
|
||||||
|
const buf = new Uint8Array(4);
|
||||||
|
const view = new DataView(buf.buffer);
|
||||||
|
view.setUint32(0, value, true);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -67,17 +67,7 @@ import type { loadRenderers } from "astro:container";
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="flex-1 flex flex-col gap-6 min-h-0">
|
<section class="flex-1 flex flex-col gap-6 min-h-0">
|
||||||
<div
|
<DeviceInfo client:load />
|
||||||
class="h-48 bg-indigo-950/20 border border-indigo-500/20 rounded-[2rem] p-6 relative overflow-hidden shrink-0">
|
|
||||||
<div class="absolute top-6 right-6 text-indigo-500 opacity-20">
|
|
||||||
<Icon name="ph:cpu-fill" class="w-12 h-12" />
|
|
||||||
</div>
|
|
||||||
<h3 class="text-indigo-400 text-[10px] font-black uppercase tracking-[0.2em] mb-4">
|
|
||||||
Device Info
|
|
||||||
</h3>
|
|
||||||
<DeviceInfo client:load />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex-1 flex flex-col overflow-hidden">
|
<div class="flex-1 flex flex-col overflow-hidden">
|
||||||
<FileStorage client:load />
|
<FileStorage client:load />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user