322 lines
8.8 KiB
Python
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,
|
|
)
|
|
)
|