Files
buzzer/buzzer_tool/core/connection.py
2026-03-02 00:25:40 +01:00

866 lines
35 KiB
Python

# core/connection.py
import time
import os
import struct
import binascii
PROTOCOL_ERROR_MESSAGES = {
0x01: "Ungültiger Befehl.",
0x02: "Ungültige Parameter.",
0x03: "Befehl oder Parameter sind zu lang.",
0x10: "Datei oder Verzeichnis wurde nicht gefunden.",
0x11: "Ziel existiert bereits.",
0x12: "Pfad ist kein Verzeichnis.",
0x13: "Pfad ist ein Verzeichnis.",
0x14: "Zugriff verweigert.",
0x15: "Kein freier Speicher mehr vorhanden.",
0x16: "Datei ist zu groß.",
0x20: "Allgemeiner Ein-/Ausgabefehler auf dem Gerät.",
0x21: "Zeitüberschreitung auf dem Gerät.",
0x22: "CRC-Prüfung fehlgeschlagen (Daten beschädigt).",
0x23: "Übertragung wurde vom Gerät abgebrochen.",
0x30: "Befehl wird vom Gerät nicht unterstützt.",
0x31: "Gerät ist beschäftigt.",
0x32: "Interner Gerätefehler.",
}
SYNC = b"BUZZ"
HEADER_SIZE = 14
DEFAULT_MAX_PATH_LEN = 32
FRAME_REQ = 0x01
FRAME_RESP_ACK = 0x10
FRAME_RESP_DATA = 0x11
FRAME_RESP_STREAM_START = 0x12
FRAME_RESP_STREAM_CHUNK = 0x13
FRAME_RESP_STREAM_END = 0x14
FRAME_RESP_ERROR = 0x7F
POLL_SLEEP_SECONDS = 0.002
CMD_GET_PROTOCOL_VERSION = 0x00
CMD_GET_FIRMWARE_STATUS = 0x01
CMD_GET_FLASH_STATUS = 0x02
CMD_CONFIRM_FIRMWARE = 0x03
CMD_REBOOT = 0x04
CMD_LIST_DIR = 0x10
CMD_CHECK_FILE_CRC = 0x11
CMD_MKDIR = 0x12
CMD_RM = 0x13
CMD_PUT_FILE_START = 0x14
CMD_PUT_FILE_CHUNK = 0x15
CMD_PUT_FILE_END = 0x16
CMD_PUT_FW_START = 0x17
CMD_STAT = 0x18
CMD_RENAME = 0x19
CMD_RM_R = 0x1A
CMD_GET_FILE = 0x1B
CMD_GET_TAG_BLOB = 0x20
CMD_SET_TAG_BLOB_START = 0x21
CMD_SET_TAG_BLOB_CHUNK = 0x22
CMD_SET_TAG_BLOB_END = 0x23
class BuzzerError(Exception):
pass
class BuzzerConnection:
def __init__(self, config):
self.port = config.get("port")
self.baudrate = config.get("baudrate", 115200)
self.timeout = config.get("timeout", 5.0)
self.crc_timeout_min_seconds = float(config.get("crc_timeout_min_seconds", 2.0))
self.crc_timeout_ms_per_100kb = float(config.get("crc_timeout_ms_per_100kb", 1.5))
self.serial = None
self._sequence = 0
self._max_path_len = DEFAULT_MAX_PATH_LEN
def __enter__(self):
if not self.port:
raise ValueError("Kein serieller Port konfiguriert.")
try:
import serial
except ImportError as e:
raise BuzzerError("PySerial ist nicht installiert. Bitte 'pip install -r requirements.txt' ausführen.") from e
# write_timeout verhindert endloses Blockieren auf inaktiven Ports
self.serial = serial.Serial(
port=self.port,
baudrate=self.baudrate,
timeout=self.timeout,
write_timeout=self.timeout
)
self.serial.reset_input_buffer()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if self.serial and self.serial.is_open:
self.serial.close()
def _parse_controller_error(self, line: str) -> str:
code_str = line.split(" ", 1)[1].strip() if " " in line else ""
try:
code = int(code_str, 10)
except ValueError:
return f"Controller meldet einen unbekannten Fehler: '{line}'"
message = PROTOCOL_ERROR_MESSAGES.get(code, "Unbekannter Fehlercode vom Gerät.")
return f"Controller-Fehler {code} (0x{code:02X}): {message}"
def _parse_controller_error_code(self, code: int) -> str:
message = PROTOCOL_ERROR_MESSAGES.get(code, "Unbekannter Fehlercode vom Gerät.")
return f"Controller-Fehler {code} (0x{code:02X}): {message}"
def _raise_error_from_payload(self, payload: bytes) -> None:
error_code = payload[0] if len(payload) >= 1 else 0x32
detail = ""
if len(payload) >= 2:
detail_len = payload[1]
if detail_len > 0 and len(payload) >= 2 + detail_len:
detail = payload[2:2 + detail_len].decode("utf-8", errors="replace")
msg = self._parse_controller_error_code(error_code)
if detail:
msg = f"{msg} Detail: {detail}"
raise BuzzerError(msg)
def _next_sequence(self) -> int:
seq = self._sequence
self._sequence = (self._sequence + 1) & 0xFFFF
return seq
def _crc16_ccitt_false(self, data: bytes) -> int:
crc = 0xFFFF
for b in data:
crc ^= b
for _ in range(8):
if crc & 0x0001:
crc = ((crc >> 1) ^ 0x8408) & 0xFFFF
else:
crc = (crc >> 1) & 0xFFFF
return crc
def _read_exact(self, size: int, timeout: float) -> bytes:
deadline = time.monotonic() + timeout
chunks = bytearray()
while len(chunks) < size:
remaining_time = deadline - time.monotonic()
if remaining_time <= 0:
raise TimeoutError(f"Lese-Timeout beim Warten auf {size} Bytes.")
old_timeout = self.serial.timeout
self.serial.timeout = min(remaining_time, 0.25)
try:
chunk = self.serial.read(size - len(chunks))
finally:
self.serial.timeout = old_timeout
if chunk:
chunks.extend(chunk)
return bytes(chunks)
def _build_frame(self, frame_type: int, command_id: int, sequence: int, payload: bytes) -> bytes:
payload = payload or b""
payload_len = len(payload)
header_no_sync_crc = struct.pack("<BBHI", frame_type, command_id, sequence, payload_len)
header_crc = self._crc16_ccitt_false(header_no_sync_crc)
header = SYNC + header_no_sync_crc + struct.pack("<H", header_crc)
payload_crc = binascii.crc32(payload) & 0xFFFFFFFF
return header + payload + struct.pack("<I", payload_crc)
def _write_frame(self, frame: bytes) -> None:
try:
self.serial.write(frame)
self.serial.flush()
except Exception as e:
if e.__class__.__name__ == "SerialTimeoutException":
raise TimeoutError(f"Schreib-Timeout am Port {self.port}. Ist das Gerät blockiert?") from e
raise
def _send_binary_frame_no_wait(self, command_id: int, payload: bytes = b"") -> int:
if self.serial is None:
raise BuzzerError("Serielle Verbindung ist nicht geöffnet.")
sequence = self._next_sequence()
frame = self._build_frame(FRAME_REQ, command_id, sequence, payload)
self._write_frame(frame)
return sequence
def _read_frame(self, timeout: float = None) -> dict:
eff_timeout = timeout if timeout is not None else self.timeout
deadline = time.monotonic() + eff_timeout
sync_idx = 0
while sync_idx < len(SYNC):
remaining = deadline - time.monotonic()
if remaining <= 0:
raise TimeoutError("Timeout beim Warten auf Sync 'BUZZ'.")
b = self._read_exact(1, remaining)
if b[0] == SYNC[sync_idx]:
sync_idx += 1
else:
sync_idx = 1 if b[0] == SYNC[0] else 0
remaining = deadline - time.monotonic()
if remaining <= 0:
raise TimeoutError("Timeout beim Lesen des Frame-Headers.")
rest_header = self._read_exact(HEADER_SIZE - len(SYNC), remaining)
frame_type, command_id, sequence, payload_len, rx_header_crc = struct.unpack("<BBHIH", rest_header)
calc_header_crc = self._crc16_ccitt_false(struct.pack("<BBHI", frame_type, command_id, sequence, payload_len))
if rx_header_crc != calc_header_crc:
raise BuzzerError(
f"Ungültige Header-CRC: empfangen 0x{rx_header_crc:04X}, erwartet 0x{calc_header_crc:04X}"
)
remaining = deadline - time.monotonic()
if remaining <= 0:
raise TimeoutError("Timeout beim Lesen des Payloads.")
payload = self._read_exact(payload_len, remaining) if payload_len else b""
remaining = deadline - time.monotonic()
if remaining <= 0:
raise TimeoutError("Timeout beim Lesen der Payload-CRC.")
rx_payload_crc = struct.unpack("<I", self._read_exact(4, remaining))[0]
calc_payload_crc = binascii.crc32(payload) & 0xFFFFFFFF
if rx_payload_crc != calc_payload_crc:
raise BuzzerError(
f"Ungültige Payload-CRC: empfangen 0x{rx_payload_crc:08X}, erwartet 0x{calc_payload_crc:08X}"
)
return {
"frame_type": frame_type,
"command_id": command_id,
"sequence": sequence,
"payload": payload,
}
def send_binary_command(self, command_id: int, payload: bytes = b"", timeout: float = None) -> bytes:
if self.serial is None:
raise BuzzerError("Serielle Verbindung ist nicht geöffnet.")
eff_timeout = timeout if timeout is not None else self.timeout
self.serial.reset_input_buffer()
sequence = self._next_sequence()
frame = self._build_frame(FRAME_REQ, command_id, sequence, payload)
self._write_frame(frame)
response = self._read_frame(timeout=eff_timeout)
if response["sequence"] != sequence:
raise BuzzerError(
f"Antwort-Sequenz passt nicht: erwartet {sequence}, erhalten {response['sequence']}"
)
if response["command_id"] != command_id:
raise BuzzerError(
f"Antwort-Kommando passt nicht: erwartet 0x{command_id:02X}, erhalten 0x{response['command_id']:02X}"
)
if response["frame_type"] == FRAME_RESP_ERROR:
self._raise_error_from_payload(response["payload"])
if response["frame_type"] not in (FRAME_RESP_ACK, FRAME_RESP_DATA):
raise BuzzerError(f"Unerwarteter Response-Typ: 0x{response['frame_type']:02X}")
return response["payload"]
def get_protocol_version(self, timeout: float = None) -> int:
payload = self.send_binary_command(CMD_GET_PROTOCOL_VERSION, b"", timeout=timeout)
if len(payload) != 2:
raise BuzzerError(f"Ungültige Antwortlänge für GET_PROTOCOL_VERSION: {len(payload)}")
return struct.unpack("<H", payload)[0]
def get_firmware_status(self, timeout: float = None) -> tuple[int, str]:
payload = self.send_binary_command(CMD_GET_FIRMWARE_STATUS, b"", timeout=timeout)
if len(payload) < 2:
raise BuzzerError("Ungültige Antwort für GET_FIRMWARE_STATUS: zu kurz")
status = payload[0]
version_len = payload[1]
if len(payload) != 2 + version_len:
raise BuzzerError(
f"Ungültige Antwort für GET_FIRMWARE_STATUS: erwartete Länge {2 + version_len}, erhalten {len(payload)}"
)
version = payload[2:2 + version_len].decode("utf-8", errors="replace")
return status, version
def get_flash_status(self, timeout: float = None) -> dict:
payload = self.send_binary_command(CMD_GET_FLASH_STATUS, b"", timeout=timeout)
if len(payload) != 16:
raise BuzzerError(f"Ungültige Antwortlänge für GET_FLASH_STATUS: {len(payload)}")
block_size, total_blocks, free_blocks, path_max_len = struct.unpack("<IIII", payload)
if path_max_len > 0:
self._max_path_len = int(path_max_len)
return {
"block_size": block_size,
"total_blocks": total_blocks,
"free_blocks": free_blocks,
"path_max_len": path_max_len,
}
def confirm_firmware(self, timeout: float = None) -> None:
payload = self.send_binary_command(CMD_CONFIRM_FIRMWARE, b"", timeout=timeout)
if len(payload) != 0:
raise BuzzerError(f"Unerwartete Payload für CONFIRM_FIRMWARE: {len(payload)} Bytes")
def reboot_device(self, timeout: float = None) -> None:
payload = self.send_binary_command(CMD_REBOOT, b"", timeout=timeout)
if len(payload) != 0:
raise BuzzerError(f"Unerwartete Payload für REBOOT: {len(payload)} Bytes")
def _encode_path_payload(self, path: str) -> bytes:
path_bytes = path.encode("utf-8")
if len(path_bytes) == 0:
raise BuzzerError("Pfad darf nicht leer sein.")
max_path_len = min(self._max_path_len, 255)
if len(path_bytes) > max_path_len:
raise BuzzerError(f"Pfad ist zu lang (max. {max_path_len} Bytes).")
return bytes([len(path_bytes)]) + path_bytes
def list_directory(self, path: str, timeout: float = None) -> list[str]:
if self.serial is None:
raise BuzzerError("Serielle Verbindung ist nicht geöffnet.")
eff_timeout = timeout if timeout is not None else self.timeout
self.serial.reset_input_buffer()
sequence = self._next_sequence()
frame = self._build_frame(FRAME_REQ, CMD_LIST_DIR, sequence, self._encode_path_payload(path))
try:
self.serial.write(frame)
self.serial.flush()
except Exception as e:
if e.__class__.__name__ == "SerialTimeoutException":
raise TimeoutError(f"Schreib-Timeout am Port {self.port}. Ist das Gerät blockiert?") from e
raise
lines = []
stream_started = False
while True:
response = self._read_frame(timeout=eff_timeout)
if response["sequence"] != sequence:
raise BuzzerError(
f"Antwort-Sequenz passt nicht: erwartet {sequence}, erhalten {response['sequence']}"
)
if response["command_id"] != CMD_LIST_DIR:
raise BuzzerError(
f"Antwort-Kommando passt nicht: erwartet 0x{CMD_LIST_DIR:02X}, erhalten 0x{response['command_id']:02X}"
)
frame_type = response["frame_type"]
payload = response["payload"]
if frame_type == FRAME_RESP_ERROR:
error_code = payload[0] if len(payload) >= 1 else 0x32
detail = ""
if len(payload) >= 2:
detail_len = payload[1]
if detail_len > 0 and len(payload) >= 2 + detail_len:
detail = payload[2:2 + detail_len].decode("utf-8", errors="replace")
msg = self._parse_controller_error_code(error_code)
if detail:
msg = f"{msg} Detail: {detail}"
raise BuzzerError(msg)
if frame_type == FRAME_RESP_STREAM_START:
stream_started = True
continue
if frame_type == FRAME_RESP_STREAM_CHUNK:
if len(payload) < 6:
raise BuzzerError("Ungültiger LIST_DIR Chunk: zu kurz")
entry_type = payload[0]
name_len = payload[1]
if len(payload) != 6 + name_len:
raise BuzzerError("Ungültiger LIST_DIR Chunk: inkonsistente Namenslänge")
size = struct.unpack("<I", payload[2:6])[0]
name = payload[6:6 + name_len].decode("utf-8", errors="replace")
if entry_type == 0:
type_char = "F"
elif entry_type == 1:
type_char = "D"
else:
raise BuzzerError(f"Ungültiger LIST_DIR entry_type: {entry_type}")
lines.append(f"{type_char},{size},{name}")
continue
if frame_type == FRAME_RESP_STREAM_END:
return lines
if not stream_started and frame_type == FRAME_RESP_DATA:
text = payload.decode("utf-8", errors="replace")
return [line for line in text.splitlines() if line]
if frame_type == FRAME_RESP_ACK:
return lines
raise BuzzerError(f"Unerwarteter LIST_DIR Response-Typ: 0x{frame_type:02X}")
def check_file_crc(self, path: str, timeout: float = None) -> int:
payload = self.send_binary_command(CMD_CHECK_FILE_CRC, self._encode_path_payload(path), timeout=timeout)
if len(payload) != 4:
raise BuzzerError(f"Ungültige Antwortlänge für CHECK_FILE_CRC: {len(payload)}")
return struct.unpack("<I", payload)[0]
def mkdir(self, path: str, timeout: float = None) -> None:
payload = self.send_binary_command(CMD_MKDIR, self._encode_path_payload(path), timeout=timeout)
if len(payload) != 0:
raise BuzzerError(f"Unerwartete Payload für MKDIR: {len(payload)} Bytes")
def rm(self, path: str, timeout: float = None) -> None:
payload = self.send_binary_command(CMD_RM, self._encode_path_payload(path), timeout=timeout)
if len(payload) != 0:
raise BuzzerError(f"Unerwartete Payload für RM: {len(payload)} Bytes")
def stat(self, path: str, timeout: float = None) -> dict:
payload = self.send_binary_command(CMD_STAT, self._encode_path_payload(path), timeout=timeout)
if len(payload) != 5:
raise BuzzerError(f"Ungültige Antwortlänge für STAT: {len(payload)}")
entry_type = payload[0]
if entry_type == 0:
type_char = "F"
elif entry_type == 1:
type_char = "D"
else:
raise BuzzerError(f"Ungültiger STAT entry_type: {entry_type}")
size = struct.unpack("<I", payload[1:5])[0]
return {
"type": type_char,
"size": int(size),
}
def rename(self, old_path: str, new_path: str, timeout: float = None) -> None:
old_payload = self._encode_path_payload(old_path)
new_payload = self._encode_path_payload(new_path)
payload = old_payload + new_payload
response = self.send_binary_command(CMD_RENAME, payload, timeout=timeout)
if len(response) != 0:
raise BuzzerError(f"Unerwartete Payload für RENAME: {len(response)} Bytes")
def rm_recursive(self, path: str, timeout: float = None) -> None:
payload = self.send_binary_command(CMD_RM_R, self._encode_path_payload(path), timeout=timeout)
if len(payload) != 0:
raise BuzzerError(f"Unerwartete Payload für RM_R: {len(payload)} Bytes")
def get_file_data(self, path: str, timeout: float = None, progress_callback=None) -> bytes:
if self.serial is None:
raise BuzzerError("Serielle Verbindung ist nicht geöffnet.")
eff_timeout = timeout if timeout is not None else self.timeout
self.serial.reset_input_buffer()
sequence = self._next_sequence()
frame = self._build_frame(FRAME_REQ, CMD_GET_FILE, sequence, self._encode_path_payload(path))
self._write_frame(frame)
start_response = self._read_frame(timeout=eff_timeout)
if start_response["sequence"] != sequence:
raise BuzzerError(
f"Antwort-Sequenz passt nicht: erwartet {sequence}, erhalten {start_response['sequence']}"
)
if start_response["command_id"] != CMD_GET_FILE:
raise BuzzerError(
f"Antwort-Kommando passt nicht: erwartet 0x{CMD_GET_FILE:02X}, erhalten 0x{start_response['command_id']:02X}"
)
if start_response["frame_type"] == FRAME_RESP_ERROR:
self._raise_error_from_payload(start_response["payload"])
if start_response["frame_type"] != FRAME_RESP_DATA or len(start_response["payload"]) != 4:
raise BuzzerError("Ungültige GET_FILE-Startantwort (erwartet DATA mit 4 Byte Länge)")
expected_len = struct.unpack("<I", start_response["payload"])[0]
received = 0
running_crc = 0
chunks = bytearray()
chunk_size = 4096
while received < expected_len:
to_read = min(chunk_size, expected_len - received)
chunk = self._read_exact(to_read, eff_timeout)
chunks.extend(chunk)
running_crc = binascii.crc32(chunk, running_crc) & 0xFFFFFFFF
received += len(chunk)
if progress_callback:
progress_callback(len(chunk), received, expected_len)
end_response = self._read_frame(timeout=eff_timeout)
if end_response["sequence"] != sequence:
raise BuzzerError(
f"Antwort-Sequenz passt nicht: erwartet {sequence}, erhalten {end_response['sequence']}"
)
if end_response["command_id"] != CMD_GET_FILE:
raise BuzzerError(
f"Antwort-Kommando passt nicht: erwartet 0x{CMD_GET_FILE:02X}, erhalten 0x{end_response['command_id']:02X}"
)
if end_response["frame_type"] == FRAME_RESP_ERROR:
self._raise_error_from_payload(end_response["payload"])
if end_response["frame_type"] != FRAME_RESP_DATA or len(end_response["payload"]) != 4:
raise BuzzerError("Ungültige GET_FILE-Endantwort (erwartet DATA mit 4 Byte CRC32)")
expected_crc32 = struct.unpack("<I", end_response["payload"])[0]
if running_crc != expected_crc32:
raise BuzzerError(
f"GET_FILE CRC32-Mismatch: empfangen 0x{running_crc:08X}, erwartet 0x{expected_crc32:08X}"
)
return bytes(chunks)
def put_file_start(self, path: str, total_len: int, expected_crc32: int, timeout: float = None) -> None:
if total_len < 0:
raise BuzzerError("Dateigröße darf nicht negativ sein.")
payload = self._encode_path_payload(path) + struct.pack("<II", int(total_len), int(expected_crc32) & 0xFFFFFFFF)
response = self.send_binary_command(CMD_PUT_FILE_START, payload, timeout=timeout)
if len(response) != 0:
raise BuzzerError(f"Unerwartete Payload für PUT_FILE_START: {len(response)} Bytes")
def put_file_chunk(self, chunk: bytes, timeout: float = None) -> None:
self._send_binary_frame_no_wait(CMD_PUT_FILE_CHUNK, chunk)
def put_file_end(self, timeout: float = None) -> None:
eff_timeout = timeout if timeout is not None else self.timeout
end_sequence = self._send_binary_frame_no_wait(CMD_PUT_FILE_END, b"")
deadline = time.monotonic() + eff_timeout
while True:
remaining = deadline - time.monotonic()
if remaining <= 0:
raise TimeoutError("Lese-Timeout beim Warten auf PUT_FILE_END Antwort.")
response = self._read_frame(timeout=remaining)
if response["frame_type"] == FRAME_RESP_ERROR and response["command_id"] in (
CMD_PUT_FILE_START,
CMD_PUT_FILE_CHUNK,
CMD_PUT_FILE_END,
):
self._raise_error_from_payload(response["payload"])
if response["command_id"] != CMD_PUT_FILE_END or response["sequence"] != end_sequence:
continue
if response["frame_type"] not in (FRAME_RESP_ACK, FRAME_RESP_DATA):
raise BuzzerError(f"Unerwarteter Response-Typ für PUT_FILE_END: 0x{response['frame_type']:02X}")
if len(response["payload"]) != 0:
raise BuzzerError(f"Unerwartete Payload für PUT_FILE_END: {len(response['payload'])} Bytes")
return
def put_file_data(
self,
path: str,
data: bytes,
timeout: float = None,
chunk_size: int = 4096,
progress_callback=None,
) -> int:
if data is None:
data = b""
if chunk_size <= 0:
raise BuzzerError("chunk_size muss größer als 0 sein.")
expected_crc32 = binascii.crc32(data) & 0xFFFFFFFF
if self.serial is None:
raise BuzzerError("Serielle Verbindung ist nicht geöffnet.")
eff_timeout = timeout if timeout is not None else self.timeout
self.serial.reset_input_buffer()
sequence = self._next_sequence()
start_payload = self._encode_path_payload(path) + struct.pack("<II", len(data), expected_crc32)
start_frame = self._build_frame(FRAME_REQ, CMD_PUT_FILE_START, sequence, start_payload)
self._write_frame(start_frame)
sent = 0
while sent < len(data):
chunk = data[sent:sent + chunk_size]
try:
self.serial.write(chunk)
except Exception as e:
if e.__class__.__name__ == "SerialTimeoutException":
raise TimeoutError(f"Schreib-Timeout am Port {self.port}. Ist das Gerät blockiert?") from e
raise
sent += len(chunk)
if progress_callback:
progress_callback(len(chunk), sent, len(data))
self.serial.flush()
response = self._read_frame(timeout=eff_timeout)
if response["sequence"] != sequence:
raise BuzzerError(
f"Antwort-Sequenz passt nicht: erwartet {sequence}, erhalten {response['sequence']}"
)
if response["command_id"] != CMD_PUT_FILE_START:
raise BuzzerError(
f"Antwort-Kommando passt nicht: erwartet 0x{CMD_PUT_FILE_START:02X}, erhalten 0x{response['command_id']:02X}"
)
if response["frame_type"] == FRAME_RESP_ERROR:
self._raise_error_from_payload(response["payload"])
if response["frame_type"] not in (FRAME_RESP_ACK, FRAME_RESP_DATA):
raise BuzzerError(f"Unerwarteter Response-Typ für PUT_FILE_START: 0x{response['frame_type']:02X}")
if len(response["payload"]) != 0:
raise BuzzerError(f"Unerwartete Payload für PUT_FILE_START: {len(response['payload'])} Bytes")
return expected_crc32
def fw_put_data(
self,
data: bytes,
timeout: float = None,
chunk_size: int = 4096,
progress_callback=None,
) -> int:
if data is None:
data = b""
if chunk_size <= 0:
raise BuzzerError("chunk_size muss größer als 0 sein.")
if len(data) == 0:
raise BuzzerError("Firmware-Datei ist leer.")
expected_crc32 = binascii.crc32(data) & 0xFFFFFFFF
if self.serial is None:
raise BuzzerError("Serielle Verbindung ist nicht geöffnet.")
eff_timeout = timeout if timeout is not None else self.timeout
old_write_timeout = self.serial.write_timeout
self.serial.write_timeout = max(float(old_write_timeout or 0.0), float(eff_timeout))
self.serial.reset_input_buffer()
try:
sequence = self._next_sequence()
start_payload = struct.pack("<II", len(data), expected_crc32)
start_frame = self._build_frame(FRAME_REQ, CMD_PUT_FW_START, sequence, start_payload)
self._write_frame(start_frame)
sent = 0
while sent < len(data):
chunk = data[sent:sent + chunk_size]
try:
self.serial.write(chunk)
except Exception as e:
if e.__class__.__name__ == "SerialTimeoutException":
raise TimeoutError(f"Schreib-Timeout am Port {self.port}. Ist das Gerät blockiert?") from e
raise
sent += len(chunk)
if progress_callback:
progress_callback(len(chunk), sent, len(data))
self.serial.flush()
response = self._read_frame(timeout=eff_timeout)
finally:
self.serial.write_timeout = old_write_timeout
if response["sequence"] != sequence:
raise BuzzerError(
f"Antwort-Sequenz passt nicht: erwartet {sequence}, erhalten {response['sequence']}"
)
if response["command_id"] != CMD_PUT_FW_START:
raise BuzzerError(
f"Antwort-Kommando passt nicht: erwartet 0x{CMD_PUT_FW_START:02X}, erhalten 0x{response['command_id']:02X}"
)
if response["frame_type"] == FRAME_RESP_ERROR:
self._raise_error_from_payload(response["payload"])
if response["frame_type"] not in (FRAME_RESP_ACK, FRAME_RESP_DATA):
raise BuzzerError(f"Unerwarteter Response-Typ für PUT_FW_START: 0x{response['frame_type']:02X}")
if len(response["payload"]) != 0:
raise BuzzerError(f"Unerwartete Payload für PUT_FW_START: {len(response['payload'])} Bytes")
return expected_crc32
def get_tag_blob(self, path: str, timeout: float = None) -> bytes:
if self.serial is None:
raise BuzzerError("Serielle Verbindung ist nicht geöffnet.")
eff_timeout = timeout if timeout is not None else self.timeout
self.serial.reset_input_buffer()
sequence = self._next_sequence()
frame = self._build_frame(FRAME_REQ, CMD_GET_TAG_BLOB, sequence, self._encode_path_payload(path))
try:
self.serial.write(frame)
self.serial.flush()
except Exception as e:
if e.__class__.__name__ == "SerialTimeoutException":
raise TimeoutError(f"Schreib-Timeout am Port {self.port}. Ist das Gerät blockiert?") from e
raise
expected_len = None
chunks = bytearray()
while True:
response = self._read_frame(timeout=eff_timeout)
if response["sequence"] != sequence:
raise BuzzerError(
f"Antwort-Sequenz passt nicht: erwartet {sequence}, erhalten {response['sequence']}"
)
if response["command_id"] != CMD_GET_TAG_BLOB:
raise BuzzerError(
f"Antwort-Kommando passt nicht: erwartet 0x{CMD_GET_TAG_BLOB:02X}, erhalten 0x{response['command_id']:02X}"
)
frame_type = response["frame_type"]
payload = response["payload"]
if frame_type == FRAME_RESP_ERROR:
error_code = payload[0] if len(payload) >= 1 else 0x32
raise BuzzerError(self._parse_controller_error_code(error_code))
if frame_type == FRAME_RESP_STREAM_START:
if len(payload) != 4:
raise BuzzerError("Ungültiger GET_TAG_BLOB START-Frame")
expected_len = struct.unpack("<I", payload)[0]
continue
if frame_type == FRAME_RESP_STREAM_CHUNK:
chunks.extend(payload)
continue
if frame_type == FRAME_RESP_STREAM_END:
if expected_len is not None and len(chunks) != expected_len:
raise BuzzerError(
f"Tag-Blob-Länge inkonsistent: erwartet {expected_len}, erhalten {len(chunks)}"
)
return bytes(chunks)
if frame_type == FRAME_RESP_DATA:
return payload
raise BuzzerError(f"Unerwarteter GET_TAG_BLOB Response-Typ: 0x{frame_type:02X}")
def set_tag_blob(self, path: str, blob: bytes, timeout: float = None, chunk_size: int = 192) -> None:
if blob is None:
blob = b""
if len(blob) > 1024:
raise BuzzerError("Tag-Blob ist zu groß (max. 1024 Bytes).")
path_payload = self._encode_path_payload(path)
start_payload = path_payload + struct.pack("<H", len(blob))
self.send_binary_command(CMD_SET_TAG_BLOB_START, start_payload, timeout=timeout)
offset = 0
while offset < len(blob):
chunk = blob[offset:offset + chunk_size]
self.send_binary_command(CMD_SET_TAG_BLOB_CHUNK, chunk, timeout=timeout)
offset += len(chunk)
self.send_binary_command(CMD_SET_TAG_BLOB_END, b"", timeout=timeout)
def send_command(self, command: str, custom_timeout: float = None) -> list:
eff_timeout = custom_timeout if custom_timeout is not None else self.timeout
self.serial.reset_input_buffer()
try:
self.serial.write(f"{command}\n".encode('utf-8'))
self.serial.flush()
except Exception as e:
if e.__class__.__name__ == "SerialTimeoutException":
raise TimeoutError(f"Schreib-Timeout am Port {self.port}. Ist das Gerät blockiert?") from e
raise
lines = []
start_time = time.monotonic()
while (time.monotonic() - start_time) < eff_timeout:
if self.serial.in_waiting > 0:
try:
line = self.serial.readline().decode('utf-8', errors='ignore').strip()
if not line:
continue
if line == "OK":
return lines
elif line.startswith("ERR"):
raise BuzzerError(self._parse_controller_error(line))
else:
lines.append(line)
except BuzzerError:
raise
except Exception as e:
raise BuzzerError(f"Fehler beim Lesen der Antwort: {e}")
else:
time.sleep(POLL_SLEEP_SECONDS)
raise TimeoutError(f"Lese-Timeout ({eff_timeout}s) beim Warten auf Antwort für: '{command}'")
def send_binary(self, filepath: str, chunk_size: int = 4096, timeout: float = 10.0, progress_callback=None):
"""
Überträgt eine Binärdatei in Chunks, nachdem das READY-Signal empfangen wurde.
"""
# 1. Warte auf die READY-Bestätigung vom Controller
start_time = time.time()
ready = False
while (time.time() - start_time) < timeout:
if self.serial.in_waiting > 0:
line = self.serial.readline().decode('utf-8', errors='ignore').strip()
if line == "READY":
ready = True
break
elif line.startswith("ERR"):
raise BuzzerError(f"Fehler vor Binärtransfer: {self._parse_controller_error(line)}")
time.sleep(POLL_SLEEP_SECONDS)
if not ready:
raise TimeoutError("Kein READY-Signal vom Controller empfangen.")
# 2. Sende die Datei in Blöcken
file_size = os.path.getsize(filepath)
bytes_sent = 0
with open(filepath, 'rb') as f:
while bytes_sent < file_size:
# 1. Nicht blockierende Fehlerprüfung vor jedem Chunk
if self.serial.in_waiting > 0:
line = self.serial.readline().decode('utf-8', errors='ignore').strip()
if line.startswith("ERR"):
raise BuzzerError(f"Controller hat Transfer abgebrochen: {self._parse_controller_error(line)}")
# 2. Chunk lesen und schreiben
chunk = f.read(chunk_size)
if not chunk:
break
self.serial.write(chunk)
# WICHTIG: self.serial.flush() hier entfernen.
# Dies verhindert den Deadlock mit dem OS-USB-Puffer.
bytes_sent += len(chunk)
# 3. Callback für UI
if progress_callback:
progress_callback(len(chunk))
# 3. Warte auf das finale OK (oder ERR bei CRC/Schreib-Fehlern)
start_time = time.time()
while (time.time() - start_time) < timeout:
if self.serial.in_waiting > 0:
line = self.serial.readline().decode('utf-8', errors='ignore').strip()
if line == "OK":
return True
elif line.startswith("ERR"):
raise BuzzerError(f"Fehler beim Speichern der Binärdatei: {self._parse_controller_error(line)}")
time.sleep(POLL_SLEEP_SECONDS)
raise TimeoutError("Zeitüberschreitung nach Binärtransfer (kein OK empfangen).")