sync
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
# core/connection.py
|
||||
import serial
|
||||
import time
|
||||
import os
|
||||
import struct
|
||||
import binascii
|
||||
|
||||
PROTOCOL_ERROR_MESSAGES = {
|
||||
0x01: "Ungültiger Befehl.",
|
||||
@@ -23,6 +24,40 @@ PROTOCOL_ERROR_MESSAGES = {
|
||||
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
|
||||
|
||||
@@ -31,11 +66,20 @@ class BuzzerConnection:
|
||||
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(
|
||||
@@ -61,6 +105,670 @@ class BuzzerConnection:
|
||||
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()
|
||||
@@ -68,8 +776,10 @@ class BuzzerConnection:
|
||||
try:
|
||||
self.serial.write(f"{command}\n".encode('utf-8'))
|
||||
self.serial.flush()
|
||||
except serial.SerialTimeoutException:
|
||||
raise TimeoutError(f"Schreib-Timeout am Port {self.port}. Ist das Gerät blockiert?")
|
||||
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()
|
||||
@@ -91,7 +801,7 @@ class BuzzerConnection:
|
||||
except Exception as e:
|
||||
raise BuzzerError(f"Fehler beim Lesen der Antwort: {e}")
|
||||
else:
|
||||
time.sleep(0.01)
|
||||
time.sleep(POLL_SLEEP_SECONDS)
|
||||
|
||||
raise TimeoutError(f"Lese-Timeout ({eff_timeout}s) beim Warten auf Antwort für: '{command}'")
|
||||
|
||||
@@ -110,7 +820,7 @@ class BuzzerConnection:
|
||||
break
|
||||
elif line.startswith("ERR"):
|
||||
raise BuzzerError(f"Fehler vor Binärtransfer: {self._parse_controller_error(line)}")
|
||||
time.sleep(0.01)
|
||||
time.sleep(POLL_SLEEP_SECONDS)
|
||||
|
||||
if not ready:
|
||||
raise TimeoutError("Kein READY-Signal vom Controller empfangen.")
|
||||
@@ -151,6 +861,6 @@ class BuzzerConnection:
|
||||
return True
|
||||
elif line.startswith("ERR"):
|
||||
raise BuzzerError(f"Fehler beim Speichern der Binärdatei: {self._parse_controller_error(line)}")
|
||||
time.sleep(0.01)
|
||||
time.sleep(POLL_SLEEP_SECONDS)
|
||||
|
||||
raise TimeoutError("Zeitüberschreitung nach Binärtransfer (kein OK empfangen).")
|
||||
Reference in New Issue
Block a user