Files
uv-k5-v3-emulator/tools/test_scan.py
mckero 2667e046e8 Emulator: multiboot slots from the page, flash controller, portable tests
flash controller: store ACR/OPTKEYR instead of swallowing them, which is what stopped the factory bootloader from starting

slots over the firmware's own serial protocol (0x0720 family); uvk5_socket/uvk5_testenv so a fresh checkout skips instead of failing; web UI slot table and Multiboot button; quick start, CONTRIBUTING, and stop tracking firmware images and radio dumps
2026-10-01 14:54:34 +08:00

142 lines
4.7 KiB
Python
Executable File

#!/usr/bin/env python3
"""A scan must keep stepping, even though the receiver always reports a signal.
This guards a regression the BK4819 model could easily cause. bk4819_eval_receiver
reports RSSI comfortably above any sane squelch threshold, and a scan halts when it
finds a busy channel -- so a band that is permanently busy could stop the scan dead on
its first step. It does not, and this keeps it that way.
The check deliberately does not count distinct frames. RSSI varies on every poll, so
the meter and its dBm readout change constantly and would give a perfect "all frames
differ" score on a completely stationary radio. Instead it compares the framebuffer
rows holding the large frequency digits: those only change if the radio retunes.
The framebuffer is 128x64 as 8 pages of 128 bytes, page p covering rows 8p..8p+7. The
frequency digits for the upper VFO occupy pages 1-2.
"""
import gzip
import json
import os
import pathlib
import socket
import subprocess
import sys
import tempfile
import time
import uvk5_socket
import uvk5_testenv
SIM = pathlib.Path(__file__).resolve().parent.parent
QEMU = uvk5_testenv.qemu()
ELF = uvk5_testenv.firmware()
PRISTINE = SIM / "assets/pristine/flash-pristine.img.gz"
FRAME_ADDR = 0x200013DC
FRAME_BYTES = 1024
PAGE = 128
FREQ_PAGES = slice(PAGE * 1, PAGE * 3)
BOOT_SECONDS = 24
SAMPLES = 6
SAMPLE_GAP = 1.5
class Qmp:
def __init__(self, path):
self.s = uvk5_socket.connect(path, timeout=25)
self.buf = b""
self._read()
self.cmd("qmp_capabilities")
def _read(self):
while b"\n" not in self.buf:
chunk = self.s.recv(65536)
if not chunk:
raise RuntimeError("QMP closed")
self.buf += chunk
line, self.buf = self.buf.split(b"\n", 1)
return json.loads(line)
def cmd(self, name, **args):
msg = {"execute": name}
if args:
msg["arguments"] = args
self.s.sendall(json.dumps(msg).encode() + b"\n")
while True:
reply = self._read()
if "return" in reply or "error" in reply:
return reply
def frame(self, tmp):
"""memsave, not pmemsave: the latter takes a physical address and silently
returns zeros here, which looks like a blank screen with no error."""
out = pathlib.Path(tmp) / "f.bin"
self.cmd("memsave", val=FRAME_ADDR, size=FRAME_BYTES, filename=str(out))
return out.read_bytes()
def key(self, name, hold=0.12):
self.cmd("qom-set", path="/machine/keypad", property="press", value=name)
time.sleep(hold)
self.cmd("qom-set", path="/machine/keypad", property="press", value="")
def main():
for tool, what in ((QEMU, "QEMU"), (ELF, "firmware"), (PRISTINE, "pristine flash image")):
if tool is None or not tool.exists():
return uvk5_testenv.skip("%s is missing (%s); see the README Quick start"
% (what, tool or "not found"))
with tempfile.TemporaryDirectory() as tmp:
img = pathlib.Path(tmp) / "flash.img"
img.write_bytes(gzip.decompress(PRISTINE.read_bytes()))
sock = uvk5_socket.server_endpoint("qmp", directory=str(tmp))
proc = subprocess.Popen(
[str(QEMU), "-M", f"uv-k5-v3,flash-image={img}",
"-nographic", "-monitor", "none",
"-qmp", sock,
"-kernel", str(ELF)],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
try:
# Qmp() below connects, and uvk5_socket retries until QEMU's QMP answers, so
# there is no socket path to wait for -- on Windows there would not be one.
time.sleep(BOOT_SECONDS)
time.sleep(BOOT_SECONDS)
qmp = Qmp(str(sock))
# Long-press STAR starts a scan.
qmp.key("STAR", hold=0.8)
time.sleep(1.5)
frames = []
for _ in range(SAMPLES):
frames.append(qmp.frame(tmp))
time.sleep(SAMPLE_GAP)
tunings = {f[FREQ_PAGES] for f in frames}
print(f"frequency display: {len(tunings)} distinct over "
f"{SAMPLES} samples")
qmp.key("EXIT")
if len(tunings) > 1:
print("PASS the scan steps through frequencies")
print("\na busy band does not stall the scan")
return 0
print("FAIL the frequency never changed; the scan is stuck, which is "
"what an always-busy receiver would cause")
return 1
finally:
proc.terminate()
try:
proc.wait(timeout=10)
except subprocess.TimeoutExpired:
proc.kill()
if __name__ == "__main__":
sys.exit(main())