sync
This commit is contained in:
321
send_file.py
Normal file
321
send_file.py
Normal file
@@ -0,0 +1,321 @@
|
||||
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,
|
||||
)
|
||||
)
|
||||
Reference in New Issue
Block a user