import argparse import os import sys import time import zlib import serial REQUIRED_PROTOCOL_VERSION = 3 def calculate_crc32(file_path): with open(file_path, "rb") as file_handle: data = file_handle.read() return data, len(data), zlib.crc32(data) & 0xFFFFFFFF def wait_for_tokens(serial_port, timeout_s, accepted_tokens): deadline = time.monotonic() + timeout_s last_line = "" while time.monotonic() < deadline: raw = serial_port.readline() if not raw: continue line = raw.decode("utf-8", errors="ignore").strip() if not line: continue last_line = line if line in accepted_tokens: return True, line if line == "ERR": return False, line return False, last_line def query_device_info(serial_port, timeout_s): attempts = 3 per_attempt_timeout = max(timeout_s / attempts, 1.5) for _ in range(attempts): serial_port.reset_input_buffer() serial_port.write(b"INFO\n") serial_port.flush() deadline = time.monotonic() + per_attempt_timeout parsed_info = None while time.monotonic() < deadline: raw = serial_port.readline() if not raw: continue line = raw.decode("utf-8", errors="ignore").strip() if not line: continue if line == "ERR": break if line == "OK": if parsed_info is not None: return parsed_info continue parts = line.split(";") if len(parts) < 5: continue try: version = int(parts[0]) recommended = int(parts[4]) ack_window = int(parts[5]) if len(parts) >= 6 else 1 except ValueError: continue if recommended <= 0: recommended = None if ack_window <= 0: ack_window = 1 parsed_info = (version, recommended, ack_window) time.sleep(0.15) return None, None, None def send_file_once( port, baudrate, target_path, data, file_size, crc, chunk_size, timeout_s, write_timeout_s, pace_us, ): with serial.Serial(port, baudrate, timeout=0.2, write_timeout=write_timeout_s) as serial_port: serial_port.dtr = True time.sleep(0.1) serial_port.reset_input_buffer() protocol_version, recommended_chunk, ack_window = query_device_info(serial_port, timeout_s) if protocol_version is None: print("Fehler: Konnte INFO-Antwort des Geräts nicht auswerten.") return 1 if protocol_version != REQUIRED_PROTOCOL_VERSION: print( f"Fehler: Inkompatible Protokollversion {protocol_version} " f"(erwartet {REQUIRED_PROTOCOL_VERSION})." ) return 1 if recommended_chunk is not None: selected_chunk = recommended_chunk else: selected_chunk = chunk_size selected_chunk = max(64, selected_chunk) print( f"Gerät meldet Protokoll v{protocol_version}, " f"Chunk={selected_chunk}, ACK-Window={ack_window}" ) wait_window = ack_window command = f"SEND {target_path} {file_size} {crc} {selected_chunk}\n" print(f"Verbindung: {port} @ {baudrate}") print(f"Sende Befehl: {command.strip()} (CRC32=0x{crc:08X})") serial_port.write(command.encode("utf-8")) serial_port.flush() ready_ok, ready_response = wait_for_tokens(serial_port, timeout_s, {"OK"}) if not ready_ok: print(f"Fehler: Gerät nicht bereit. Antwort: '{ready_response}'") return 1 print(f"Übertrage {file_size} Bytes in Blöcken à {selected_chunk} Bytes ...") print(f"Warte auf CONT alle {wait_window} Chunks") sent = 0 chunks_since_ack = 0 start = time.monotonic() while sent < file_size: end = min(sent + selected_chunk, file_size) try: serial_port.write(data[sent:end]) except serial.SerialTimeoutException: time.sleep(0.02) continue sent = end chunks_since_ack += 1 if pace_us > 0: time.sleep(pace_us / 1_000_000.0) progress = int((sent * 100) / file_size) if file_size else 100 sys.stdout.write(f"\rFortschritt: {sent}/{file_size} Bytes ({progress}%)") sys.stdout.flush() if sent < file_size and chunks_since_ack >= wait_window: cont_ok, cont_response = wait_for_tokens(serial_port, timeout_s, {"CONT"}) if not cont_ok: print(f"\nFehler beim Chunk-Ack: '{cont_response}'") return 1 chunks_since_ack = 0 serial_port.flush() duration = max(time.monotonic() - start, 0.001) rate_kib_s = (file_size / 1024.0) / duration print(f"\nUpload beendet: {rate_kib_s:.1f} KiB/s") final_ok, final_response = wait_for_tokens(serial_port, timeout_s, {"OK"}) if final_ok: print("Übertragung erfolgreich abgeschlossen (CRC geprüft).") return 0 print(f"Fehler beim Abschluss: '{final_response}'") return 1 def send_file( port, baudrate, file_path, target_path, chunk_size, timeout_s, retries, write_timeout_s, pace_us, ): if not os.path.exists(file_path): print(f"Fehler: Lokale Datei '{file_path}' nicht gefunden.") return 1 if not target_path.startswith("/"): print("Fehler: Zielpfad muss mit '/' beginnen (z.B. /lfs/test).") return 1 data, file_size, crc = calculate_crc32(file_path) attempts = retries + 1 for attempt in range(1, attempts + 1): if attempt > 1: print(f"\nNeuer Versuch {attempt}/{attempts} ...") try: result = send_file_once( port, baudrate, target_path, data, file_size, crc, chunk_size, timeout_s, write_timeout_s, pace_us, ) if result == 0: return 0 except serial.SerialException as error: print(f"Serial Fehler: {error}") except Exception as error: print(f"Allgemeiner Fehler: {error}") if attempt < attempts: time.sleep(0.4) print(f"Upload fehlgeschlagen nach {attempts} Versuch(en).") return 1 if __name__ == "__main__": parser = argparse.ArgumentParser( description="Datei über Serial-Protokoll an Zephyr/nRF senden" ) parser.add_argument( "-p", "--port", required=True, help="Serieller Port (z.B. COM12 oder /dev/ttyACM0)", ) parser.add_argument( "-f", "--file", required=True, help="Lokaler Pfad zur Datei", ) parser.add_argument( "-t", "--target", default="/lfs/test", help="Zielpfad auf dem Gerät (Standard: /lfs/test)", ) parser.add_argument( "-b", "--baud", type=int, default=115200, help="Baudrate (Standard: 115200)", ) parser.add_argument( "--chunk-size", type=int, default=1024, help="Fallback-Chunkgröße in Bytes falls INFO keine Empfehlung liefert (Standard: 1024)", ) parser.add_argument( "--timeout", type=float, default=8.0, help="Timeout für Geräteantworten in Sekunden (Standard: 8)", ) parser.add_argument( "--retries", type=int, default=0, help="Anzahl automatischer Wiederholungen bei Fehlern (Standard: 0)", ) parser.add_argument( "--write-timeout", type=float, default=0.0, help="Serial write timeout in Sekunden (0 = blockierend, Standard: 0)", ) parser.add_argument( "--pace-us", type=int, default=300, help="Pause nach jedem Block in Mikrosekunden (Standard: 300)", ) arguments = parser.parse_args() retry_count = max(arguments.retries, 0) chunk_size = max(arguments.chunk_size, 64) pace_us = max(arguments.pace_us, 0) write_timeout_s = None if arguments.write_timeout <= 0 else arguments.write_timeout sys.exit( send_file( arguments.port, arguments.baud, arguments.file, arguments.target, chunk_size, arguments.timeout, retry_count, write_timeout_s, pace_us, ) )