pre uart exchange

This commit is contained in:
2026-03-04 16:32:51 +01:00
parent b665cb5def
commit 4f3fbff258
46 changed files with 2820 additions and 3186 deletions

170
tool/buzz.py Normal file
View File

@@ -0,0 +1,170 @@
import sys
import os
import argparse
# # Falls buzz.py tief in Unterordnern liegt, stellen wir sicher,
# # dass das Hauptverzeichnis im Pfad ist:
# sys.path.append(os.path.dirname(os.path.abspath(__file__)))
def main():
try:
parser = argparse.ArgumentParser(description="Buzzer Serial Comm Tool")
# Allgemeine Parameter
parser.add_argument("-c", "--config", help="Pfad zur config.yaml (optional)", type=str)
parser.add_argument("-d", "--debug", help="Aktiviert detaillierte Hex-Logs", action="store_true")
# Verbindungsparameter (können auch in config.yaml definiert werden)
parser.add_argument("-p", "--port", help="Serieller Port", type=str)
parser.add_argument("-b", "--baud", help="Baudrate", type=int)
parser.add_argument("-t", "--timeout", help="Timeout in Sekunden", type=float)
# Subparser für Befehle
subparsers = parser.add_subparsers(dest="command", help="Verfügbare Befehle")
# Befehl: flash_info
flash_info_parser = subparsers.add_parser("flash_info", help="Informationen über den Flash-Speicher des Controllers abfragen")
# Befehl: fw_status
fw_status_parser = subparsers.add_parser("fw_status", help="Firmware- und Kernel-Status des Controllers abfragen")
# Befehl: get_file
get_file_parser = subparsers.add_parser("get_file", help="Datei vom Zielsystem herunterladen")
get_file_parser.add_argument("source_path", help="Pfad der Datei auf dem Zielsystem")
get_file_parser.add_argument("dest_path", help="Zielpfad auf dem lokalen System")
# Befehl: ls
ls_parser = subparsers.add_parser("ls", help="Listet Dateien/Ordner in einem Verzeichnis auf")
ls_parser.add_argument("path", help="Pfad auf dem Zielsystem")
ls_parser.add_argument("-r", "--recursive", help="Rekursiv durch die Verzeichnisse durchsuchen", action="store_true")
# Befehl: proto
proto_parser = subparsers.add_parser("proto", help="Protokollversion des Controllers abfragen")
# Befehl: rename
rename_parser = subparsers.add_parser("rename", help="Benennen Sie eine Datei oder einen Ordner auf dem Zielsystem um")
rename_parser.add_argument("source_path", help="Aktueller Pfad der Datei/des Ordners auf dem Zielsystem")
rename_parser.add_argument("dest_path", help="Neuer Pfad der Datei/des Ordners auf dem Zielsystem")
# Befehl: rm
rm_parser = subparsers.add_parser("rm", help="Entfernt eine Datei oder einen Ordner auf dem Zielsystem")
rm_parser.add_argument("path", help="Pfad auf dem Zielsystem")
# Befehl: stat
stat_parser = subparsers.add_parser("stat", help="Informationen zu einer Datei/Ordner")
stat_parser.add_argument("path", help="Pfad auf dem Zielsystem")
args = parser.parse_args()
if not args.command:
parser.print_help()
sys.exit(0)
from core.config import cfg
from core.utils import console, console_err
if args.config:
cfg.custom_path = args.config
if args.debug:
cfg.debug = True
console.print("[bold blue]Buzzer Tool v1.0[/bold blue]", justify="left")
settings = cfg.serial_settings
settings['debug'] = args.debug
# Überschreibe Einstellungen mit Kommandozeilenparametern, falls vorhanden
if args.port:
settings['port'] = args.port
if args.baud:
settings['baudrate'] = args.baud
if args.timeout:
settings['timeout'] = args.timeout
# Ausgabe der aktuellen Einstellungen
port = settings.get('port')
baud = settings.get('baudrate', 'N/A')
timeout = settings.get('timeout', 'N/A')
if not port:
console_err.print("[error]Fehler: Kein serieller Port angegeben.[/error]")
sys.exit(1)
if args.debug:
console.print(f" • Port: [info]{port}[/info]")
console.print(f" • Baud: [info]{baud}[/info]")
console.print(f" • Timeout: [info]{timeout:1.2f}s[/info]")
console.print("-" * 78)
from core.serial_conn import SerialBus
bus = SerialBus(settings)
try:
bus.open()
if args.command == "get_file":
from core.cmd.get_file import get_file
cmd = get_file(bus)
result = cmd.get(args.source_path, args.dest_path)
cmd.print(result)
elif args.command == "flash_info":
from core.cmd.flash_info import flash_info
cmd = flash_info(bus)
result = cmd.get()
cmd.print(result)
elif args.command == "fw_status":
from core.cmd.fw_status import fw_status
cmd = fw_status(bus)
result = cmd.get()
cmd.print(result)
elif args.command == "ls":
from core.cmd.list_dir import list_dir
cmd = list_dir(bus)
result = cmd.get(args.path, recursive=args.recursive)
cmd.print(result, args.path)
elif args.command == "proto":
from core.cmd.proto import proto
cmd = proto(bus)
result = cmd.get()
cmd.print(result)
elif args.command == "rename":
from core.cmd.rename import rename
cmd = rename(bus)
result = cmd.get(args.source_path, args.dest_path)
cmd.print(result)
elif args.command == "rm":
from core.cmd.rm import rm
cmd = rm(bus)
result = cmd.get(args.path)
cmd.print(result, args.path)
elif args.command == "stat":
from core.cmd.stat import stat
cmd = stat(bus)
result = cmd.get(args.path)
cmd.print(result, args.path)
finally:
bus.close()
except FileNotFoundError as e:
console_err.print(f"[error]Fehler: {e}[/error]")
sys.exit(1)
except (TimeoutError, IOError, ValueError) as e:
console_err.print(f"[bold red]KOMMUNIKATIONSFEHLER:[/bold red] [error_msg]{e}[/error_msg]")
sys.exit(1) # Beendet das Script mit Fehlercode 1 für Tests
except Exception as e:
# Hier fangen wir auch deinen neuen ControllerError ab
from core.serial_conn import ControllerError
if isinstance(e, ControllerError):
console_err.print(f"[bold red]CONTROLLER FEHLER:[/bold red] [error_msg]{e}[/error_msg]")
else:
console_err.print(f"[bold red]UNERWARTETER FEHLER:[/bold red] [error_msg]{e}[/error_msg]")
if args.debug:
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()

4
tool/config.yaml Normal file
View File

@@ -0,0 +1,4 @@
serial:
port: "/dev/cu.usbmodem83401"
baudrate: 115200
timeout: 1.0

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

View File

@@ -0,0 +1,53 @@
# tool/core/cmd/fw_status.py
import struct
from core.utils import console, console_err
from core.protocol import COMMANDS, ERRORS
class fw_status:
def __init__(self, bus):
self.bus = bus
def get(self):
import struct
self.bus.send_request(COMMANDS['get_firmware_status'])
data = self.bus.receive_response(length=10)
if not data or data.get('type') == 'error':
return None
header = data['data']
status = header[0]
app_version_raw = struct.unpack('<I', header[1:5])[0]
ker_version_raw = struct.unpack('<I', header[5:9])[0]
str_len = header[9]
fw_string_bytes = self.bus.connection.read(str_len)
fw_string = fw_string_bytes.decode('utf-8')
result = {
'status': status,
'fw_version_raw': hex(app_version_raw),
'kernel_version_raw': hex(ker_version_raw),
'fw_major': (app_version_raw >> 24) & 0xFF,
'fw_minor': (app_version_raw >> 16) & 0xFF,
'fw_patch': (app_version_raw >> 8)& 0xFF,
'kernel_major': (ker_version_raw >> 16) & 0xFF,
'kernel_minor': (ker_version_raw >> 8) & 0xFF,
'kernel_patch': ker_version_raw & 0xFF,
'fw_string': fw_string,
'kernel_string': f"{(ker_version_raw >> 16) & 0xFF}.{(ker_version_raw >> 8) & 0xFF}.{ker_version_raw & 0xFF}"
}
return result
def print(self, result):
if not result:
return
status = "UNKNOWN"
if result['status'] == 0x00: status = "CONFIRMED"
elif result['status'] == 0x01: status = "PENDING"
elif result['status'] == 0x02: status = "TESTING"
console.print(f"[info]Firmware Status[/info] des Controllers ist [info]{status}[/info]:")
console.print(f" • Firmware: [info]{result['fw_string']}[/info] ({result['fw_major']}.{result['fw_minor']}.{result['fw_patch']})")
console.print(f" • Kernel: [info]{result['kernel_string']}[/info] ({result['kernel_major']}.{result['kernel_minor']}.{result['kernel_patch']})")

76
tool/core/cmd/get_file.py Normal file
View File

@@ -0,0 +1,76 @@
# tool/core/cmd/get_file.py
import struct
import zlib
from pathlib import Path
from core.utils import console, console_err
from core.protocol import COMMANDS
class get_file:
def __init__(self, bus):
self.bus = bus
def get(self, source_path: str, dest_path: str):
try:
p = Path(dest_path)
p.parent.mkdir(parents=True, exist_ok=True)
with open(p, 'wb') as f:
pass
except Exception as e:
console_err.print(f"Fehler: Kann Zieldatei nicht anlegen: {e}")
return None
source_path_bytes = source_path.encode('utf-8')
payload = struct.pack('B', len(source_path_bytes)) + source_path_bytes
device_file_crc = None
try:
self.bus.send_request(COMMANDS['crc_32'], payload)
crc_resp = self.bus.receive_response(length=4)
if crc_resp and crc_resp.get('type') == 'response':
device_file_crc = struct.unpack('<I', crc_resp['data'])[0]
except Exception:
device_file_crc = None
self.bus.send_request(COMMANDS['get_file'], payload)
stream_res = self.bus.receive_stream()
if not stream_res or stream_res.get('type') == 'error':
return None
file_data = stream_res['data']
remote_crc = stream_res['crc32']
if local_crc == remote_crc:
with open(p, 'wb') as f:
f.write(file_data)
success = True
else:
with open(p, 'wb') as f:
f.write(file_data)
success = False
return {
'success': success,
'source_path': source_path,
'dest_path': dest_path,
'crc32_remote': remote_crc,
'crc32_local': local_crc,
'crc32_device_file': device_file_crc,
'size': len(file_data)
}
def print(self, result):
if not result:
return
if result['success']:
console.print(f"✓ Datei [info]{result['source_path']}[/info] erfolgreich heruntergeladen.")
console.print(f" • Größe: [info]{result['size'] / 1024:.2f} KB[/info]")
else:
console_err.print(f"❌ CRC-FEHLER: Datei [error]{result['source_path']}[/error] wurde nicht korrekt empfangen!")
console.print(f" • Remote CRC: [info]{result['crc32_remote']:08X}[/info]")
console.print(f" • Local CRC: [info]{result['crc32_local']:08X}[/info]")
if result.get('crc32_device_file') is not None:
console.print(f" • Device CRC: [info]{result['crc32_device_file']:08X}[/info]")
console.print(f" • Zielpfad: [info]{result['dest_path']}[/info]")

71
tool/core/cmd/list_dir.py Normal file
View File

@@ -0,0 +1,71 @@
# tool/core/cmd/list_dir.py
import struct
from core.utils import console
from core.protocol import COMMANDS
class list_dir:
def __init__(self, bus):
self.bus = bus
def get(self, path: str, recursive: bool = False):
# Wir stellen sicher, dass der Pfad nicht leer ist und normalisieren ihn leicht
clean_path = path if path == "/" else path.rstrip('/')
path_bytes = clean_path.encode('utf-8')
payload = struct.pack('B', len(path_bytes)) + path_bytes
self.bus.send_request(COMMANDS['list_dir'], payload)
chunks = self.bus.receive_list()
if chunks is None:
return None
entries = []
for chunk in chunks:
if len(chunk) < 6: # Typ(1) + Size(4) + min 1 char Name
continue
is_dir = chunk[0] == 1
size = struct.unpack('<I', chunk[1:5])[0] if not is_dir else None
name = chunk[5:].decode('utf-8').rstrip('\x00')
entry = {
'name': name,
'is_dir': is_dir,
'size': size
}
if recursive and is_dir:
# Rekursiver Aufruf: Pfad sauber zusammenfügen
sub_path = f"{clean_path}/{name}"
entry['children'] = self.get(sub_path, recursive=True)
entries.append(entry)
return entries
def print(self, entries, path: str, prefix: str = ""):
if prefix == "":
console.print(f"Inhalt von [info]{path}[/info]:")
if not entries:
return
# Sortierung: Verzeichnisse zuerst
entries.sort(key=lambda x: (not x['is_dir'], x['name'].lower()))
for i, entry in enumerate(entries):
# Prüfen, ob es das letzte Element auf dieser Ebene ist
is_last = (i == len(entries) - 1)
connector = "" if is_last else ""
icon = "📁" if entry['is_dir'] else "📄"
size_str = f" ({entry['size']/1024:.2f} KB)" if entry['size'] is not None else ""
# Ausgabe der aktuellen Zeile
console.print(f"{prefix}{connector}{icon} [info]{entry['name']}[/info]{size_str}")
# Wenn Kinder vorhanden sind, rekursiv weiter
if 'children' in entry and entry['children']:
# Für die Kinder-Ebene das Prefix anpassen
extension = " " if is_last else ""
self.print(entry['children'], "", prefix=prefix + extension)

28
tool/core/cmd/proto.py Normal file
View File

@@ -0,0 +1,28 @@
# tool/core/cmd/proto.py
import struct
from core.utils import console, console_err
from core.protocol import COMMANDS, ERRORS
class proto:
def __init__(self, bus):
self.bus = bus
def get(self):
self.bus.send_request(COMMANDS['get_protocol_version'], None)
data = self.bus.receive_response(length=1)
if not data or data.get('type') == 'error':
return None
payload = data['data']
result = {
'protocol_version': payload[0]
}
return result
def print(self, result):
if not result:
return
protocol_version = result['protocol_version']
console.print(f"[title]Protokoll Version[/info] des Controllers ist [info]{protocol_version}[/info]:")

37
tool/core/cmd/rename.py Normal file
View 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
View File

@@ -0,0 +1,32 @@
# tool/core/cmd/rm.py
import struct
from core.utils import console, console_err
from core.protocol import COMMANDS, ERRORS
class rm:
def __init__(self, bus):
self.bus = bus
def get(self, path: str):
path_bytes = path.encode('utf-8')
payload = struct.pack('B', len(path_bytes)) + path_bytes
self.bus.send_request(COMMANDS['rm'], payload)
# 1 Byte Type + 4 Byte Size = 5
data = self.bus.receive_ack()
if not data or data.get('type') == 'error':
return None
if data.get('type') == 'ack':
return True
return False
def print(self, result, path: str):
if result is None:
console_err.print(f"Fehler: Pfad [error]{path}[/error] konnte nicht entfernt werden.")
elif result is False:
console_err.print(f"Fehler: Pfad [error]{path}[/error] existiert nicht oder konnte nicht entfernt werden.")
else:
console.print(f"Pfad [info]{path}[/info] wurde erfolgreich entfernt.")

36
tool/core/cmd/stat.py Normal file
View File

@@ -0,0 +1,36 @@
# tool/core/cmd/stat.py
import struct
from core.utils import console, console_err
from core.protocol import COMMANDS, ERRORS
class stat:
def __init__(self, bus):
self.bus = bus
def get(self, path: str):
path_bytes = path.encode('utf-8')
payload = struct.pack('B', len(path_bytes)) + path_bytes
self.bus.send_request(COMMANDS['stat'], payload)
# 1 Byte Type + 4 Byte Size = 5
data = self.bus.receive_response(length=5)
if not data or data.get('type') == 'error':
return None
payload = data['data']
result = {
'is_directory': payload[0] == 1,
'size': struct.unpack('<I', payload[1:5])[0]
}
return result
def print(self, result, path: str):
if not result:
return
t_name = "📁 Verzeichnis" if result['is_directory'] else "📄 Datei"
console.print(f"[info_title]Stat[/info_title] für [info]{path}[/info]:")
console.print(f" • Typ: [info]{t_name}[/info]")
if not result['is_directory']:
console.print(f" • Grösse: [info]{result['size']/1024:.2f} KB[/info] ({result['size']} Bytes)")

48
tool/core/config.py Normal file
View File

@@ -0,0 +1,48 @@
# tool/core/config.py
import os
import yaml
from pathlib import Path
class Config:
def __init__(self):
self._config = None
self.config_name = "config.yaml"
self.custom_path = None # Speicher für den Parameter-Pfad
self.debug = False
def _load(self):
if self._config is not None:
return
from core.utils import console
search_paths = []
if self.custom_path:
search_paths.append(Path(self.custom_path))
search_paths.append(Path(os.getcwd()) / self.config_name)
search_paths.append(Path(__file__).parent.parent / self.config_name)
config_path = None
for p in search_paths:
if p.exists():
config_path = p
break
if not config_path:
raise FileNotFoundError(f"Konfiguration konnte an keinem Ort gefunden werden.")
if not config_path.exists():
raise FileNotFoundError(f"Konfiguration nicht gefunden: {self.config_name}")
else:
if self.debug: console.print(f"[bold green]✓[/bold green] Konfiguration geladen: [info]{config_path}[/info]")
with open(config_path, 'r') as f:
self._config = yaml.safe_load(f)
@property
def serial_settings(self):
self._load()
return self._config.get('serial', {})
cfg = Config()

88
tool/core/protocol.py Normal file
View File

@@ -0,0 +1,88 @@
# tool/core/protocol.py
VERSION = {
"min_protocol_version": 1,
"max_protocol_version": 1,
"current_protocol_version": None
}
SYNC_SEQ = b'BUZZ'
ERRORS = {
0x00: "NONE",
0x01: "INVALID_COMMAND",
0x02: "INVALID_PARAMETERS",
0x03: "MISSING_PARAMETERS",
0x10: "FILE_NOT_FOUND",
0x11: "ALREADY_EXISTS",
0x12: "NOT_A_DIRECTORY",
0x13: "IS_A_DIRECTORY",
0x14: "ACCESS_DENIED",
0x15: "NO_SPACE",
0x16: "FILE_TOO_LARGE",
0x20: "IO_ERROR",
0x21: "TIMEOUT",
0x22: "CRC_MISMATCH",
0x23: "TRANSFER_ABORTED",
0x30: "NOT_SUPPORTED",
0x31: "BUSY",
0x32: "INTERNAL_ERROR",
0x40: "NOT_IMPLEMENTED",
}
FRAME_TYPE_REQUEST = 0x01
FRAME_TYPE_ACK = 0x10
FRAME_TYPE_RESPONSE = 0x11
FRAME_TYPE_STREAM_START = 0x12
FRAME_TYPE_STREAM_CHUNK = 0x13
FRAME_TYPE_STREAM_END = 0x14
FRAME_TYPE_LIST_START = 0x15
FRAME_TYPE_LIST_CHUNK = 0x16
FRAME_TYPE_LIST_END = 0x17
FRAME_TYPE_ERROR = 0xFF
FRAME_TYPES = {
'request': FRAME_TYPE_REQUEST,
'ack': FRAME_TYPE_ACK,
'response': FRAME_TYPE_RESPONSE,
'error': FRAME_TYPE_ERROR,
'stream_start': FRAME_TYPE_STREAM_START,
'stream_chunk': FRAME_TYPE_STREAM_CHUNK,
'stream_end': FRAME_TYPE_STREAM_END,
'list_start': FRAME_TYPE_LIST_START,
'list_chunk': FRAME_TYPE_LIST_CHUNK,
'list_end': FRAME_TYPE_LIST_END
}
CMD_GET_PROTOCOL_VERSION = 0x00
CMD_GET_FIRMWARE_STATUS = 0x01
CMD_GET_FLASH_INFO = 0x02
CMD_LIST_DIR = 0x10
CMD_CRC_32 = 0x11
CMD_MKDIR = 0x12
CMD_RM = 0x13
CMD_STAT = 0x18
CMD_RENAME = 0x19
CMD_PUT_FILE = 0x20
CMD_PUT_FW = 0x21
CMD_GET_FILE = 0x22
COMMANDS = {
'get_protocol_version': CMD_GET_PROTOCOL_VERSION,
'get_firmware_status': CMD_GET_FIRMWARE_STATUS,
'get_flash_info': CMD_GET_FLASH_INFO,
'list_dir': CMD_LIST_DIR,
'stat': CMD_STAT,
'rm': CMD_RM,
'rename': CMD_RENAME,
'mkdir': CMD_MKDIR,
'crc_32': CMD_CRC_32,
'put_file': CMD_PUT_FILE,
'get_file': CMD_GET_FILE,
'put_fw': CMD_PUT_FW,
}

279
tool/core/serial_conn.py Normal file
View File

@@ -0,0 +1,279 @@
# tool/core/serial_conn.py
import struct
import serial
import time
from core.utils import console, console_err
from core.protocol import SYNC_SEQ, ERRORS, FRAME_TYPES, VERSION
class SerialBus:
def __init__(self, settings: dict):
"""
Initialisiert den Bus mit den (ggf. übersteuerten) Settings.
"""
self.port = settings.get('port')
self.baudrate = settings.get('baudrate', 115200)
self.timeout = settings.get('timeout', 1.0)
self.debug = settings.get('debug', False)
self.connection = None
def open(self):
"""Öffnet die serielle Schnittstelle."""
try:
self.connection = serial.Serial(
port=self.port,
baudrate=self.baudrate,
timeout=self.timeout
)
self.flush_input()
if self.debug: console.print(f"[bold green]✓[/bold green] Port [info]{self.port}[/info] erfolgreich geöffnet.")
from core.cmd.proto import proto
cmd = proto(self)
data = cmd.get()
VERSION["current_protocol_version"] = data['protocol_version'] if data else None
if data:
if self.debug: console.print(f" • Protokoll Version: [info]{data['protocol_version']}[/info]")
if data['protocol_version'] < VERSION["min_protocol_version"] or data['protocol_version'] > VERSION["max_protocol_version"]:
if VERSION["min_protocol_version"] == VERSION["max_protocol_version"]:
expected = f"Version {VERSION['min_protocol_version']}"
else:
expected = f"Version {VERSION['min_protocol_version']} bis {VERSION['max_protocol_version']}"
raise ValueError(f"Inkompatibles Protokoll. Controller spricht {data['protocol_version']}, erwartet wird {expected}.")
else:
raise ValueError("Keine gültige Antwort auf Protokollversion erhalten.")
except serial.SerialException as e:
console_err.print(f"[bold red]Serieller Fehler:[/bold red] [error_msg]{e}[/error_msg]")
raise
except Exception as e:
console_err.print(f"[bold red]Unerwarteter Fehler beim Öffnen:[/bold red] [error_msg]{e}[/error_msg]")
raise
def flush_input(self):
"""Leert den Empfangspuffer der seriellen Schnittstelle."""
if self.connection and self.connection.is_open:
self.connection.reset_input_buffer()
def close(self):
"""Schließt die Verbindung sauber."""
if self.connection and self.connection.is_open:
self.connection.close()
if self.debug: console.print(f"Verbindung zu [info]{self.port}[/info] geschlossen.")
def send_binary(self, data: bytes):
"""Sendet Rohdaten und loggt sie im Hex-Format."""
if not self.connection or not self.connection.is_open:
raise ConnectionError("Port ist nicht geöffnet.")
self.connection.write(data)
if self.debug:
hex_data = data.hex(' ').upper()
console.print(f"TX -> [grey62]{hex_data}[/grey62]")
def _read_exact(self, length: int, context: str = "Daten") -> bytes:
data = bytearray()
while len(data) < length:
try:
chunk = self.connection.read(length - len(data))
except serial.SerialException as e:
raise IOError(f"Serielle Verbindung verloren beim Lesen von {context}: {e}") from e
if not chunk:
raise TimeoutError(f"Timeout beim Lesen von {context}: {len(data)}/{length} Bytes.")
data.extend(chunk)
return bytes(data)
def wait_for_sync(self, sync_seq: bytes, max_time: float = 2.0):
"""Wartet maximal max_time Sekunden auf die Sync-Sequenz."""
buffer = b""
start_time = time.time()
if self.debug:
console.print(f"[bold cyan]Warte auf SYNC-Sequenz:[/bold cyan] [grey62]{sync_seq.hex(' ').upper()}[/grey62]")
# Kurzer interner Timeout für reaktive Schleife
original_timeout = self.connection.timeout
self.connection.timeout = 0.1
try:
while (time.time() - start_time) < max_time:
char = self.connection.read(1)
if not char:
continue
buffer += char
if len(buffer) > len(sync_seq):
buffer = buffer[1:]
if buffer == sync_seq:
if self.debug: console.print("[bold cyan]RX <- SYNC OK[/bold cyan]")
return True
return False
finally:
self.connection.timeout = original_timeout
def send_request(self, cmd_id: int, payload: bytes = b''):
self.flush_input()
frame_type = struct.pack('B', FRAME_TYPES['request'])
cmd_byte = struct.pack('B', cmd_id)
full_frame = SYNC_SEQ + frame_type + cmd_byte
if payload:
full_frame += payload
self.send_binary(full_frame)
def receive_ack(self, timeout: float = None):
wait_time = timeout if timeout is not None else self.timeout
if not self.wait_for_sync(SYNC_SEQ, max_time=wait_time):
raise TimeoutError(f"SYNC-Sequenz nicht innerhalb von {wait_time}s gefunden.")
ftype_raw = self.connection.read(1)
if not ftype_raw:
raise TimeoutError("Timeout beim Lesen des Frame-Typs.")
ftype = ftype_raw[0]
if ftype == FRAME_TYPES['error']:
err_code_raw = self.connection.read(1)
err_code = err_code_raw[0] if err_code_raw else 0xFF
err_name = ERRORS.get(err_code, "UNKNOWN")
raise ControllerError(err_code, err_name)
elif ftype == FRAME_TYPES['ack']:
if self.debug:
console.print(f"[green]ACK empfangen[/green]")
return {"type": "ack"}
raise ValueError(f"Unerwarteter Frame-Typ (0x{FRAME_TYPES['ack']:02X} (ACK) erwartet): 0x{ftype:02X}")
def receive_response(self, length: int, timeout: float = None, varlen_params: int = 0):
wait_time = timeout if timeout is not None else self.timeout
if not self.wait_for_sync(SYNC_SEQ, max_time=wait_time):
raise TimeoutError(f"SYNC-Sequenz nicht innerhalb von {wait_time}s gefunden.")
ftype_raw = self.connection.read(1)
if not ftype_raw:
raise TimeoutError("Timeout beim Lesen des Frame-Typs.")
ftype = ftype_raw[0]
if ftype == FRAME_TYPES['error']:
err_code_raw = self.connection.read(1)
err_code = err_code_raw[0] if err_code_raw else 0xFF
err_name = ERRORS.get(err_code, "UNKNOWN")
raise ControllerError(err_code, err_name)
elif ftype == FRAME_TYPES['response']:
data = self.connection.read(length)
for varlen_param in range(varlen_params):
length_byte = self.connection.read(1)
if not length_byte:
raise TimeoutError("Timeout beim Lesen der Länge eines variablen Parameters.")
param_length = length_byte[0]
param_data = self.connection.read(param_length)
if not param_data:
raise TimeoutError("Timeout beim Lesen eines variablen Parameters.")
data += length_byte + param_data
if self.debug:
console.print(f"RX <- [grey62]{data.hex(' ').upper()}[/grey62]")
if len(data) < length:
raise IOError(f"Unvollständiges Paket: {len(data)}/{length} Bytes.")
return {"type": "response", "data": data}
raise ValueError(f"Unerwarteter Frame-Typ: 0x{ftype:02X}")
def receive_list(self):
"""Liest eine Liste von Einträgen, bis list_end kommt."""
is_list = False
list_items = []
while True:
if not self.wait_for_sync(SYNC_SEQ):
raise TimeoutError("Timeout beim Warten auf Sync im List-Modus.")
ftype = self.connection.read(1)[0]
if ftype == FRAME_TYPES['list_start']:
is_list = True
list_items = []
elif ftype == FRAME_TYPES['list_chunk']:
if not is_list: raise ValueError("Chunk ohne Start.")
length = struct.unpack('<H', self.connection.read(2))[0]
if self.debug: console.print(f"Erwarte List-Chunk mit Länge: {length} Bytes")
data = self.connection.read(length)
if self.debug: console.print(f"Rohdaten List-Chunk: [grey62]{data.hex(' ').upper()}[/grey62]") # Debug-Ausgabe des rohen Chunks
list_items.append(data)
elif ftype == FRAME_TYPES['list_end']:
if not is_list: raise ValueError("Ende ohne Start.")
num_entries = struct.unpack('<H', self.connection.read(2))[0]
if len(list_items) != num_entries:
console_err.print(f"[warning]Warnung: Erwartete {num_entries} Items, bekam {len(list_items)}[/warning]")
return list_items
elif ftype == FRAME_TYPES['error']:
err_code_raw = self.connection.read(1)
err_code = err_code_raw[0] if err_code_raw else 0xFF
err_name = ERRORS.get(err_code, "UNKNOWN")
raise ControllerError(err_code, err_name)
def receive_stream(self, chunk_size: int = 1024):
"""Liest einen Datenstrom in Chunks, bis ein Fehler oder Ende-Signal kommt."""
is_stream = False
data_chunks = []
while True:
if not self.wait_for_sync(SYNC_SEQ):
raise TimeoutError("Timeout beim Warten auf Sync im Stream-Modus.")
ftype = self._read_exact(1, "Frame-Typ")[0]
if self.debug: console.print(f"Empfangener Frame-Typ: 0x{ftype:02X} (start: 0x{FRAME_TYPES['stream_start']:02X}, chunk: 0x{FRAME_TYPES['stream_chunk']:02X}, end: 0x{FRAME_TYPES['stream_end']:02X})")
if ftype == FRAME_TYPES['stream_start']:
is_stream = True
data_chunks = []
size = struct.unpack('<I', self._read_exact(4, "Stream-Größe"))[0]
if self.debug: console.print(f"Stream gestartet, erwartete Gesamtgröße: {size} Bytes")
received_size = 0
while received_size < size:
chunk_length = min(chunk_size, size - received_size)
try:
chunk_data = self._read_exact(chunk_length, f"Daten-Chunk @ {received_size}/{size}")
except Exception as e:
raise IOError(f"Stream-Abbruch bei {received_size}/{size} Bytes: {e}") from e
data_chunks.append(chunk_data)
received_size += len(chunk_data)
if self.debug: console.print(f"Empfangen: {received_size}/{size} Bytes ({(received_size/size)*100:.2f}%)")
if self.debug: console.print("Stream vollständig empfangen.")
elif ftype == FRAME_TYPES['stream_end']:
if not is_stream: raise ValueError("Ende ohne Start.")
crc32 = struct.unpack('<I', self._read_exact(4, "CRC32"))[0]
if self.debug: console.print(f"Stream-Ende empfangen, CRC32: 0x{crc32:08X}")
return {'data': b''.join(data_chunks), 'crc32': crc32}
# elif ftype == FRAME_TYPES['list_chunk']:
# if not is_list: raise ValueError("Chunk ohne Start.")
# length = struct.unpack('<H', self.connection.read(2))[0]
# if self.debug: console.print(f"Erwarte List-Chunk mit Länge: {length} Bytes")
# data = self.connection.read(length)
# if self.debug: console.print(f"Rohdaten List-Chunk: [grey62]{data.hex(' ').upper()}[/grey62]") # Debug-Ausgabe des rohen Chunks
# list_items.append(data)
# elif ftype == FRAME_TYPES['list_end']:
# if not is_list: raise ValueError("Ende ohne Start.")
# num_entries = struct.unpack('<H', self.connection.read(2))[0]
# if len(list_items) != num_entries:
# console_err.print(f"[warning]Warnung: Erwartete {num_entries} Items, bekam {len(list_items)}[/warning]")
# return list_items
elif ftype == FRAME_TYPES['error']:
err_code_raw = self.connection.read(1)
err_code = err_code_raw[0] if err_code_raw else 0xFF
err_name = ERRORS.get(err_code, "UNKNOWN")
raise ControllerError(err_code, err_name)
else:
raise ValueError(f"Unerwarteter Frame-Typ: 0x{ftype:02X}")
class ControllerError(Exception):
"""Wird ausgelöst, wenn der Controller einen Error-Frame (0xFF) sendet."""
def __init__(self, code, name):
self.code = code
self.name = name
super().__init__(f"Controller Error 0x{code:02X} ({name})")

14
tool/core/utils.py Normal file
View 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
View File

@@ -0,0 +1,3 @@
PyYAML
pyserial
rich