mirror of
https://github.com/MCKero6423/uv-k5-v3-emulator.git
synced 2026-10-02 03:15:36 +00:00
Guard scanning against the always-busy receiver
bk4819_eval_receiver reports RSSI above any sane squelch threshold on every poll, and a scan halts when it finds a busy channel -- so the S-meter work could plausibly have stopped scanning dead on its first step. Measured: it does not, 6 distinct tunings over 6 samples. The check compares the framebuffer rows holding the frequency digits rather than counting distinct frames. Frame comparison would pass on a completely parked radio, because RSSI varies every poll and the meter redraws constantly -- an initial run scored 8/8 distinct frames and proved nothing. Narrowing to pages 1-2 answers the actual question: did the radio retune. Incidentally established that page 4 is not the meter row: it was byte-identical across all six samples while the frequency changed.
This commit is contained in:
1 parent
a310429eff
commit
3d1dad0617
1 file changed
+146
Executable
+146
@@ -0,0 +1,146 @@
|
||||
#!/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
|
||||
|
||||
SIM = pathlib.Path(__file__).resolve().parent.parent
|
||||
QEMU = pathlib.Path(os.environ.get(
|
||||
"QEMU", "/root/qemu-build/qemu-7.2+dfsg/build/qemu-system-arm"))
|
||||
ELF = pathlib.Path(os.environ.get(
|
||||
"ELF", "/root/uvk5-port/uvk5-sat/build/CW/nr7y.cw.elf"))
|
||||
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 = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
self.s.settimeout(25)
|
||||
self.s.connect(path)
|
||||
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 in (QEMU, ELF, PRISTINE):
|
||||
if not tool.exists():
|
||||
print(f"SKIP missing {tool}")
|
||||
return 0
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
img = pathlib.Path(tmp) / "flash.img"
|
||||
img.write_bytes(gzip.decompress(PRISTINE.read_bytes()))
|
||||
sock = pathlib.Path(tmp) / "qmp.sock"
|
||||
|
||||
proc = subprocess.Popen(
|
||||
[str(QEMU), "-M", f"uv-k5-v3,flash-image={img}",
|
||||
"-nographic", "-monitor", "none",
|
||||
"-qmp", f"unix:{sock},server=on,wait=off",
|
||||
"-kernel", str(ELF)],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
try:
|
||||
for _ in range(BOOT_SECONDS * 4):
|
||||
if sock.exists():
|
||||
break
|
||||
time.sleep(0.25)
|
||||
else:
|
||||
print("FAIL QMP socket never appeared")
|
||||
return 1
|
||||
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())
|
||||
Reference in new issue
Block a user