This commit is contained in:
2026-03-07 08:51:50 +01:00
parent 4f3fbff258
commit f85143d7e5
60 changed files with 3245 additions and 1205 deletions

36
tool/core/cmd/crc32.py Normal file
View 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]")

View 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.")

View File

@@ -2,6 +2,7 @@
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
@@ -21,24 +22,34 @@ class get_file:
source_path_bytes = source_path.encode('utf-8')
payload = struct.pack('B', len(source_path_bytes)) + source_path_bytes
device_file_crc = None
try:
self.bus.send_request(COMMANDS['crc_32'], payload)
crc_resp = self.bus.receive_response(length=4)
if crc_resp and crc_resp.get('type') == 'response':
device_file_crc = struct.unpack('<I', crc_resp['data'])[0]
except Exception:
device_file_crc = None
self.bus.send_request(COMMANDS['get_file'], payload)
stream_res = self.bus.receive_stream()
# 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['crc32']
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:
@@ -55,8 +66,8 @@ class get_file:
'dest_path': dest_path,
'crc32_remote': remote_crc,
'crc32_local': local_crc,
'crc32_device_file': device_file_crc,
'size': len(file_data)
'size': len(file_data),
'duration': duratuion
}
def print(self, result):
@@ -73,4 +84,6 @@ class get_file:
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]")
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]")

View 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
View 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))

23
tool/core/cmd/play.py Normal file
View 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.")

100
tool/core/cmd/put_file.py Normal file
View 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
View 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
View 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
View 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.")

View 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.")

15
tool/core/cmd/stop.py Normal file
View 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.")