866 lines
35 KiB
Python
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).") |