"""Write the firmware's own firmware slots over the serial link. The multi-system release exposes its slots to a host (App/app/uart.c, "Firmware Slots"): 0x0720 slot info Data[0] = slot -> 0x0721 Slot, Status, Hdr[64] 0x0722 slot erase Data[0] = slot, Data[2..5] = ts -> 0x0723 Slot, Status 0x0724 slot write Data[0] = slot, Data[2..5] = offset, Data[6..7] = len, Data[8..11] = ts, Data[12..] = data -> 0x0725 Slot, Status 0x0726 slot validate Data[0] = slot -> 0x0727 Crc32, Slot, Status Every one of them is gated on the timestamp the 0x0514 session handshake latched (UART_Timestamp), exactly like the EEPROM write (CMD_051D): send a different one and the firmware answers MB_ERR_AUTH without doing anything. The frames are the AB CD .. DC BA protocol the rest of the programming interface uses, so uvk5_serial_flash builds them and this only adds the message ids and payload layouts. Nothing in the emulator's own path needs this: the page writes slots straight into the flash image. It exists because it is the path a real radio takes, and the only way to write a slot on hardware. """ from __future__ import annotations import argparse import os import socket import struct import sys import threading import time import zlib sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import uvk5_serial_flash as proto # build(), Frames import uvk5_slots as slots # the header layout, in one place MSG_SLOT_INFO = 0x0720 MSG_SLOT_INFO_ACK = 0x0721 MSG_SLOT_ERASE = 0x0722 MSG_SLOT_ERASE_ACK = 0x0723 MSG_SLOT_WRITE = 0x0724 MSG_SLOT_WRITE_ACK = 0x0725 MSG_SLOT_VALIDATE = 0x0726 MSG_SLOT_VALIDATE_ACK = 0x0727 STATUS = { 0: "ok", 1: "no/invalid slot header", 2: "header format too new", 3: "image not marked committed", 4: "image size out of range", 5: "image CRC mismatch", 6: "external flash timed out", 7: "slot index out of range", 8: "timestamp mismatch", 9: "restore stub RAM mismatch", } CHUNK = 200 # see Radio.write class SlotError(RuntimeError): pass class Radio: """A connection to the firmware's slot commands.""" def __init__(self, endpoint, timeout: float = 6.0, log=print): """Talk over @endpoint (host:port, or a device path), or an open socket. An already-connected socket is accepted because a test that wants QEMU to be the one connecting has to listen first, and handing the accepted socket in is simpler than racing on a free port. """ self.log = log if hasattr(endpoint, "recv"): self.sock = endpoint self.sock.settimeout(timeout) else: self.sock = proto.open_transport(endpoint) self.sock.settimeout(timeout) self.frames = proto.Frames() self.timestamp = (int(time.time() * 100) & 0xFFFFFFFF) self._lock = threading.Lock() self._stop = False # Drain the port on a thread of its own. The firmware streams its screen over # this same link (K5Viewer), so a client that only reads when it is waiting for # a reply backs the socket up and the *guest* then blocks writing to it: # measured, a single 64-byte slot write took six seconds that way, and longer # transfers lost replies and stalled. Reading continuously is what keeps the # radio responsive. self._reader = threading.Thread(target=self._read_loop, daemon=True) self._reader.start() def close(self): self._stop = True try: self.sock.close() except OSError: pass def _read_loop(self): while not self._stop: try: chunk = self.sock.recv(65536) except (socket.timeout, OSError): continue if not chunk: return if os.environ.get("UVK5_SLOT_DEBUG"): self.log("rx %s" % chunk.hex()[:120]) with self._lock: self.frames.feed(chunk) def _pump(self, seconds: float, want=None): """Wait for @want, or @seconds, whichever comes first. The reading itself happens on the reader thread; this only looks at what it has reassembled, so waiting never stops the port being drained. """ end = time.monotonic() + seconds got = {} while time.monotonic() < end: with self._lock: while True: msg = self.frames.take() if msg is None: break got[msg[0]] = msg[1] if want is not None and want in got: return got time.sleep(0.002) return got def session(self): """0x0514, which latches our timestamp on the device.""" # CMD_0514_t: the timestamp is what matters; the rest is padding. body = struct.pack(" 1 and (chunk_no - 1) % 25 == 0: # The firmware leaves its serial mode after ~6 s without a session # handshake (gSerialConfigCountDown_500ms = 12), and this transfer runs # longer than that. Measured: the writes stop dead at that point and only # a fresh 0x0514 revives them, so renew well inside the window. self.session() data = (bytes([slot, 0]) + struct.pack("= len(blob): log(" %6d/%d bytes, %.1fs" % (start + len(piece), len(blob), time.monotonic() - began)) def validate(self, slot: int): ack = self._command(MSG_SLOT_VALIDATE, bytes([slot]), MSG_SLOT_VALIDATE_ACK, wait=20.0) if ack is None: raise SlotError("0x0726 got no reply") crc = struct.unpack_from("