Kompletter OTA Flash-Prozess mit Server (hab zwischencommits vergessen ups)
This commit is contained in:
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
OTA-Update-Server fuer den Pico-Bootloader.
|
||||
|
||||
Wire-Protokoll:
|
||||
Pico -> Server (40 bytes):
|
||||
[0:4] magic 0xC0FFEE01 (LE)
|
||||
[4:8] pico_id (LE u32)
|
||||
[8:40] SHA256 des aktuellen App-Slots (3.5 MB, 0xFF-gepaddet)
|
||||
|
||||
Server -> Pico:
|
||||
[0:1] status: 0x00 = no update, 0x01 = update follows
|
||||
falls status == 0x01:
|
||||
[1:5] size der Binary in bytes (LE u32)
|
||||
[5:37] SHA256 des neuen App-Slots (auch 0xFF-gepaddet auf 3.5 MB)
|
||||
[37:] Binary-Payload (size Bytes)
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
MAGIC = 0xC0FFEE01
|
||||
APP_SLOT_SIZE = 3584 * 1024 # 3.5 MB
|
||||
SHA256_SIZE = 32
|
||||
REQUEST_SIZE = 4 + 4 + SHA256_SIZE # 40
|
||||
|
||||
STATUS_NO_UPDATE = 0x00
|
||||
STATUS_UPDATE = 0x01
|
||||
|
||||
LISTEN_HOST = "0.0.0.0"
|
||||
LISTEN_PORT = 9000
|
||||
RECV_TIMEOUT_S = 5.0
|
||||
|
||||
FIRMWARE_DIR = Path(__file__).parent / "firmware"
|
||||
|
||||
# pico_id -> binary path. Picos, die hier nicht gelistet sind, bekommen
|
||||
# immer status=no_update (server kennt das Geraet nicht).
|
||||
MANIFEST = {
|
||||
1: FIRMWARE_DIR / "pico_2.bin",
|
||||
2: FIRMWARE_DIR / "pico_1.bin",
|
||||
}
|
||||
|
||||
|
||||
def slot_hash(binary_path: Path) -> bytes:
|
||||
"""Hash der Datei, gepaddet mit 0xFF bis 3.5 MB — exakt das,
|
||||
was der Pico auf seinem App-Slot berechnet."""
|
||||
data = binary_path.read_bytes()
|
||||
if len(data) > APP_SLOT_SIZE:
|
||||
raise ValueError(
|
||||
f"{binary_path}: {len(data)} bytes > app slot ({APP_SLOT_SIZE})"
|
||||
)
|
||||
padded = data + b"\xff" * (APP_SLOT_SIZE - len(data))
|
||||
return hashlib.sha256(padded).digest()
|
||||
|
||||
|
||||
def recv_exact(conn: socket.socket, n: int) -> bytes:
|
||||
buf = b""
|
||||
while len(buf) < n:
|
||||
chunk = conn.recv(n - len(buf))
|
||||
if not chunk:
|
||||
raise ConnectionError(
|
||||
f"Verbindung geschlossen nach {len(buf)}/{n} Bytes"
|
||||
)
|
||||
buf += chunk
|
||||
return buf
|
||||
|
||||
|
||||
def handle_client(conn: socket.socket, addr) -> None:
|
||||
conn.settimeout(RECV_TIMEOUT_S)
|
||||
|
||||
try:
|
||||
request = recv_exact(conn, REQUEST_SIZE)
|
||||
except (ConnectionError, socket.timeout) as exc:
|
||||
print(f"[{addr}] Request-Abbruch: {exc}")
|
||||
return
|
||||
|
||||
magic, pico_id = struct.unpack("<II", request[:8])
|
||||
current_hash = request[8:]
|
||||
|
||||
print(
|
||||
f"[{addr}] pico_id={pico_id} hash={current_hash.hex()[:16]}... magic=0x{magic:08x}"
|
||||
)
|
||||
|
||||
if magic != MAGIC:
|
||||
print(f"[{addr}] FEHLER: ungueltiges Magic")
|
||||
return
|
||||
|
||||
binary_path = MANIFEST.get(pico_id)
|
||||
if binary_path is None:
|
||||
print(f"[{addr}] pico_id {pico_id} nicht im Manifest -> no_update")
|
||||
conn.sendall(bytes([STATUS_NO_UPDATE]))
|
||||
return
|
||||
|
||||
if not binary_path.exists():
|
||||
print(f"[{addr}] Manifest-Eintrag fehlt: {binary_path} -> no_update")
|
||||
conn.sendall(bytes([STATUS_NO_UPDATE]))
|
||||
return
|
||||
|
||||
target_hash = slot_hash(binary_path)
|
||||
|
||||
if current_hash == target_hash:
|
||||
print(f"[{addr}] aktuell -> no_update")
|
||||
conn.sendall(bytes([STATUS_NO_UPDATE]))
|
||||
return
|
||||
|
||||
binary = binary_path.read_bytes()
|
||||
print(
|
||||
f"[{addr}] update -> {binary_path.name} ({len(binary)} bytes, "
|
||||
f"hash {target_hash.hex()[:16]}...)"
|
||||
)
|
||||
conn.sendall(bytes([STATUS_UPDATE]))
|
||||
conn.sendall(struct.pack("<I", len(binary)))
|
||||
conn.sendall(target_hash)
|
||||
conn.sendall(binary)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not FIRMWARE_DIR.exists():
|
||||
FIRMWARE_DIR.mkdir(parents=True)
|
||||
print(f"firmware/ angelegt: {FIRMWARE_DIR}")
|
||||
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as srv:
|
||||
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
srv.bind((LISTEN_HOST, LISTEN_PORT))
|
||||
srv.listen(4)
|
||||
print(f"OTA-Server hoert auf {LISTEN_HOST}:{LISTEN_PORT}")
|
||||
print(f"Manifest: {[(k, str(v.name)) for k, v in MANIFEST.items()]}")
|
||||
try:
|
||||
while True:
|
||||
conn, addr = srv.accept()
|
||||
try:
|
||||
handle_client(conn, addr)
|
||||
except Exception as exc:
|
||||
print(f"[{addr}] Exception: {exc}")
|
||||
finally:
|
||||
conn.close()
|
||||
except KeyboardInterrupt:
|
||||
print("\nbeendet")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simuliert einen Pico, der den OTA-Server anfragt. Damit testen wir das
|
||||
Protokoll ohne echtes Geraet.
|
||||
|
||||
Usage:
|
||||
test_client.py <pico_id> [--current-binary PATH] [--host HOST] [--port PORT]
|
||||
|
||||
Beispiele:
|
||||
# so tun, als laege auf dem Pico ein leerer Slot (alles 0xFF):
|
||||
test_client.py 1
|
||||
|
||||
# so tun, als laege auf dem Pico schon pico_1.bin:
|
||||
test_client.py 1 --current-binary firmware/pico_1.bin
|
||||
|
||||
# unbekannte Pico-ID:
|
||||
test_client.py 99
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
MAGIC = 0xC0FFEE01
|
||||
APP_SLOT_SIZE = 3584 * 1024
|
||||
SHA256_SIZE = 32
|
||||
|
||||
|
||||
def slot_hash(binary_path: Path | None) -> bytes:
|
||||
if binary_path is None:
|
||||
# Leerer Slot: 3.5 MB 0xFF.
|
||||
padded = b"\xff" * APP_SLOT_SIZE
|
||||
else:
|
||||
data = binary_path.read_bytes()
|
||||
if len(data) > APP_SLOT_SIZE:
|
||||
raise ValueError(f"{binary_path}: zu gross ({len(data)})")
|
||||
padded = data + b"\xff" * (APP_SLOT_SIZE - len(data))
|
||||
return hashlib.sha256(padded).digest()
|
||||
|
||||
|
||||
def recv_exact(sock: socket.socket, n: int) -> bytes:
|
||||
buf = b""
|
||||
while len(buf) < n:
|
||||
chunk = sock.recv(n - len(buf))
|
||||
if not chunk:
|
||||
raise ConnectionError(f"Server-Disconnect nach {len(buf)}/{n}")
|
||||
buf += chunk
|
||||
return buf
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("pico_id", type=int)
|
||||
ap.add_argument("--current-binary", type=Path, default=None,
|
||||
help="Pfad einer Binary, die wir 'als bereits installiert' simulieren")
|
||||
ap.add_argument("--host", default="127.0.0.1")
|
||||
ap.add_argument("--port", type=int, default=9000)
|
||||
ap.add_argument("--save-update", type=Path, default=None,
|
||||
help="Falls Server ein Update sendet, hier speichern")
|
||||
args = ap.parse_args()
|
||||
|
||||
current_hash = slot_hash(args.current_binary)
|
||||
print(f"-> pico_id={args.pico_id} hash={current_hash.hex()[:16]}...")
|
||||
|
||||
req = struct.pack("<II", MAGIC, args.pico_id) + current_hash
|
||||
assert len(req) == 40
|
||||
|
||||
with socket.create_connection((args.host, args.port), timeout=5.0) as sock:
|
||||
sock.sendall(req)
|
||||
|
||||
status_byte = recv_exact(sock, 1)
|
||||
status = status_byte[0]
|
||||
print(f"<- status=0x{status:02x}")
|
||||
|
||||
if status == 0x00:
|
||||
print(" no update — boot the existing app")
|
||||
return 0
|
||||
|
||||
if status != 0x01:
|
||||
print(f" FEHLER: unerwartetes Status-Byte 0x{status:02x}")
|
||||
return 1
|
||||
|
||||
size_bytes = recv_exact(sock, 4)
|
||||
size = struct.unpack("<I", size_bytes)[0]
|
||||
new_hash = recv_exact(sock, SHA256_SIZE)
|
||||
print(f" size={size} new_hash={new_hash.hex()[:16]}...")
|
||||
|
||||
payload = recv_exact(sock, size)
|
||||
actual_slot_hash = hashlib.sha256(
|
||||
payload + b"\xff" * (APP_SLOT_SIZE - len(payload))
|
||||
).digest()
|
||||
match = actual_slot_hash == new_hash
|
||||
print(f" payload empfangen ({len(payload)} bytes) "
|
||||
f"hash-match={match}")
|
||||
|
||||
if not match:
|
||||
print(" FEHLER: gepaddeter Payload-Hash passt nicht zum angekuendigten Slot-Hash")
|
||||
return 2
|
||||
|
||||
if args.save_update:
|
||||
args.save_update.write_bytes(payload)
|
||||
print(f" gespeichert: {args.save_update}")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user