"""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_APP_INFO = 0x0730 MSG_APP_INFO_ACK = 0x0731 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(" 0x0731), or None. The Labs edition answers this; a build without overlay apps does not answer at all, which is how UVStudio decides whether the Apps tab applies, and it is the honest way to ask "does the radio see this app" after writing one. """ return self._command(MSG_APP_INFO, bytes([slot]), MSG_APP_INFO_ACK, wait=4.0) def info(self, slot: int): """(status, header bytes) for @slot, without a CRC pass.""" ack = self._command(MSG_SLOT_INFO, bytes([slot]), MSG_SLOT_INFO_ACK) if ack is None: raise SlotError("0x0720 got no reply") return ack[1], ack[2:66] def erase(self, slot: int): data = bytes([slot, 0]) + 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("