Files
buzzer/tool/core/serial_conn.py
2026-03-04 16:32:51 +01:00

279 lines
13 KiB
Python

# 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})")