pre uart exchange
This commit is contained in:
279
tool/core/serial_conn.py
Normal file
279
tool/core/serial_conn.py
Normal 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})")
|
||||
Reference in New Issue
Block a user