Files
buzzer/send_file.py
2026-02-24 17:36:20 +01:00

322 lines
8.8 KiB
Python

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,
)
)