317 lines
14 KiB
Python
317 lines
14 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 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
|
|
|
|
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, 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 ftype == FRAME_TYPES['stream_start']:
|
|
is_stream = True
|
|
data_chunks = []
|
|
size = struct.unpack('<I', self._read_exact(4, "Stream-Größe"))[0]
|
|
start_time = time.time()
|
|
|
|
received_size = 0
|
|
while received_size < size:
|
|
chunk_length = min(chunk_size, size - received_size)
|
|
chunk_data = self._read_exact(chunk_length, f"Daten-Chunk @ {received_size}/{size}")
|
|
data_chunks.append(chunk_data)
|
|
received_size += len(chunk_data)
|
|
|
|
# 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]
|
|
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.")
|
|
# 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})") |