#!/usr/bin/env python3 """Program a UV-K5 over its serial bootloader protocol. Not the same protocol as the EEPROM read/write commands (0x0514/0x051B). This one is the firmware-update flow, and its message set and framing come from the firmware's own host tool (`tools/serialtool` in armel/uv-k1-k5v3-firmware-custom, MIT licensed): 0x0518 device -> host UID and bootloader version, announced repeatedly 0x0530 host -> device the bootloader version we expect (handshake) 0x0519 host -> device timestamp, page index, page count, then up to 256 bytes 0x051A device -> host timestamp, page index, error code Framing is `AB CD | len | payload | crc | DC BA` with the payload XOR-ed by a fixed 16-byte table. The CRC is only checked on the host side: the device does not send a useful one, which is why the reference client ignores it. The transport here is a socket, because that is what the emulator offers through `-serial tcp:host:port`. A real radio needs a serial port; pass one as *endpoint* and pyserial is used instead (`pip install pyserial`). """ import argparse import socket import struct import sys import time MSG_NOTIFY_DEV_INFO = 0x0518 MSG_NOTIFY_BL_VER = 0x0530 MSG_PROG_FW = 0x0519 MSG_PROG_FW_RESP = 0x051A PAGE_SIZE = 256 MAGIC = b"\xab\xcd" END = b"\xdc\xba" # The obfuscation table, copied from tools/serialtool/msg.py. OBFUS = bytes([0x16, 0x6C, 0x14, 0xE6, 0x2E, 0x91, 0x0D, 0x40, 0x21, 0x35, 0xD5, 0x40, 0x13, 0x03, 0xE9, 0x80]) def obfuscate(data: bytes) -> bytes: return bytes(b ^ OBFUS[i % len(OBFUS)] for i, b in enumerate(data)) def crc16_xmodem(data: bytes) -> int: crc = 0 for byte in data: crc ^= byte << 8 for _ in range(8): crc = ((crc << 1) ^ 0x1021) & 0xFFFF if crc & 0x8000 else (crc << 1) & 0xFFFF return crc def build(msg_type: int, data: bytes = b"") -> bytes: """One frame: header, payload, CRC, footer, obfuscated in one pass. The length field counts the *message* -- type, length and data -- and excludes the CRC. A device announcement reads `ab cd 24 00` for a 36-byte message (a 32-byte UID+version body), which is what pins this down. Counting the CRC as well makes every frame two bytes too long, and the device then drops all of them without a word: the handshake is ignored and no page is ever acknowledged. """ payload = struct.pack(" int: ap = argparse.ArgumentParser(description="program a UV-K5 over its serial bootloader") ap.add_argument("--endpoint", default="127.0.0.1:4568", help="host:port of a socket chardev, or a serial port name") ap.add_argument("--image", required=True, help="firmware image to program") ap.add_argument("--pages", type=int, default=None, help="program only the first N pages (for testing)") args = ap.parse_args() with open(args.image, "rb") as fh: image = fh.read() transport = open_transport(args.endpoint) flasher = Flasher(transport) bl_ver = flasher.wait_for_device() flasher.handshake(bl_ver) written = flasher.program(image, pages=args.pages) print("programmed %d page(s) from %s" % (written, args.image)) return 0 if __name__ == "__main__": sys.exit(main())