# 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('