sync
This commit is contained in:
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]")
|
||||
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.")
|
||||
@@ -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]")
|
||||
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))
|
||||
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.")
|
||||
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.")
|
||||
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.")
|
||||
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.")
|
||||
@@ -2,7 +2,7 @@
|
||||
VERSION = {
|
||||
"min_protocol_version": 1,
|
||||
"max_protocol_version": 1,
|
||||
"current_protocol_version": None
|
||||
"current_protocol_version": None,
|
||||
}
|
||||
|
||||
SYNC_SEQ = b'BUZZ'
|
||||
@@ -33,56 +33,45 @@ ERRORS = {
|
||||
0x40: "NOT_IMPLEMENTED",
|
||||
}
|
||||
|
||||
FRAME_TYPE_REQUEST = 0x01
|
||||
FRAME_TYPE_ACK = 0x10
|
||||
FRAME_TYPE_RESPONSE = 0x11
|
||||
FRAME_TYPE_STREAM_START = 0x12
|
||||
FRAME_TYPE_STREAM_CHUNK = 0x13
|
||||
FRAME_TYPE_STREAM_END = 0x14
|
||||
FRAME_TYPE_LIST_START = 0x15
|
||||
FRAME_TYPE_LIST_CHUNK = 0x16
|
||||
FRAME_TYPE_LIST_END = 0x17
|
||||
FRAME_TYPE_ERROR = 0xFF
|
||||
|
||||
FRAME_TYPES = {
|
||||
'request': FRAME_TYPE_REQUEST,
|
||||
'ack': FRAME_TYPE_ACK,
|
||||
'response': FRAME_TYPE_RESPONSE,
|
||||
'error': FRAME_TYPE_ERROR,
|
||||
'stream_start': FRAME_TYPE_STREAM_START,
|
||||
'stream_chunk': FRAME_TYPE_STREAM_CHUNK,
|
||||
'stream_end': FRAME_TYPE_STREAM_END,
|
||||
'list_start': FRAME_TYPE_LIST_START,
|
||||
'list_chunk': FRAME_TYPE_LIST_CHUNK,
|
||||
'list_end': FRAME_TYPE_LIST_END
|
||||
'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,
|
||||
}
|
||||
|
||||
CMD_GET_PROTOCOL_VERSION = 0x00
|
||||
CMD_GET_FIRMWARE_STATUS = 0x01
|
||||
CMD_GET_FLASH_INFO = 0x02
|
||||
|
||||
CMD_LIST_DIR = 0x10
|
||||
CMD_CRC_32 = 0x11
|
||||
CMD_MKDIR = 0x12
|
||||
CMD_RM = 0x13
|
||||
CMD_STAT = 0x18
|
||||
CMD_RENAME = 0x19
|
||||
|
||||
CMD_PUT_FILE = 0x20
|
||||
CMD_PUT_FW = 0x21
|
||||
CMD_GET_FILE = 0x22
|
||||
|
||||
COMMANDS = {
|
||||
'get_protocol_version': CMD_GET_PROTOCOL_VERSION,
|
||||
'get_firmware_status': CMD_GET_FIRMWARE_STATUS,
|
||||
'get_flash_info': CMD_GET_FLASH_INFO,
|
||||
'list_dir': CMD_LIST_DIR,
|
||||
'stat': CMD_STAT,
|
||||
'rm': CMD_RM,
|
||||
'rename': CMD_RENAME,
|
||||
'mkdir': CMD_MKDIR,
|
||||
'crc_32': CMD_CRC_32,
|
||||
'put_file': CMD_PUT_FILE,
|
||||
'get_file': CMD_GET_FILE,
|
||||
'put_fw': CMD_PUT_FW,
|
||||
'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,
|
||||
}
|
||||
@@ -120,6 +120,40 @@ class SerialBus:
|
||||
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
|
||||
|
||||
@@ -212,42 +246,46 @@ class SerialBus:
|
||||
err_name = ERRORS.get(err_code, "UNKNOWN")
|
||||
raise ControllerError(err_code, err_name)
|
||||
|
||||
def receive_stream(self, chunk_size: int = 1024):
|
||||
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 self.debug: console.print(f"Empfangener Frame-Typ: 0x{ftype:02X} (start: 0x{FRAME_TYPES['stream_start']:02X}, chunk: 0x{FRAME_TYPES['stream_chunk']:02X}, end: 0x{FRAME_TYPES['stream_end']:02X})")
|
||||
|
||||
if ftype == FRAME_TYPES['stream_start']:
|
||||
is_stream = True
|
||||
data_chunks = []
|
||||
size = struct.unpack('<I', self._read_exact(4, "Stream-Größe"))[0]
|
||||
if self.debug: console.print(f"Stream gestartet, erwartete Gesamtgröße: {size} Bytes")
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
received_size = 0
|
||||
while received_size < size:
|
||||
chunk_length = min(chunk_size, size - received_size)
|
||||
try:
|
||||
chunk_data = self._read_exact(chunk_length, f"Daten-Chunk @ {received_size}/{size}")
|
||||
except Exception as e:
|
||||
raise IOError(f"Stream-Abbruch bei {received_size}/{size} Bytes: {e}") from e
|
||||
chunk_data = self._read_exact(chunk_length, f"Daten-Chunk @ {received_size}/{size}")
|
||||
data_chunks.append(chunk_data)
|
||||
received_size += len(chunk_data)
|
||||
if self.debug: console.print(f"Empfangen: {received_size}/{size} Bytes ({(received_size/size)*100:.2f}%)")
|
||||
|
||||
# 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]
|
||||
if self.debug: console.print(f"Stream-Ende empfangen, CRC32: 0x{crc32:08X}")
|
||||
|
||||
return {'data': b''.join(data_chunks), 'crc32': crc32}
|
||||
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.")
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user