Find the screen buffers in the firmware instead of hardcoding one build's

The page was told --frame-addr 0x200012BE --status-addr 0x2000163E and used them as a fallback. The firmware the user actually flashed keeps its buffers at 0x2000129E/0x2000161E, 32 bytes earlier, so every line landed 32 bytes off: that is the "other firmware looks shifted" report. The images here are minimal ELFs with no symbol table, so there is nothing to read -- but the firmware's own buffers hold the same bytes the controller holds, and tools/uvk5_buffers.py finds them by matching (1024/1024 bytes for that file).

The two address flags are optional now, work/run-webui.ps1 passes no machine-specific values at all, and the page reports what it found in /api/status and /api/panel. tools/uvk5_testenv.qemu() also looks in the sibling qemu-7.2/build the rest of the repo assumes. Fixed /api/panel's emulator-off branch, which called jsonify with both a dict and kwargs and 500'd.

Tests: test_uvk5_buffers (the search must count matches, not pairs -- its first version scored every offset full marks and always answered the first one).
This commit is contained in:
mckero committed 2026-10-01 16:09:26 +08:00
1 parent 57106c0a66
commit 1e9fdf685c
8 files changed
+419 -21

No files matched your search

+52 -8
View File
@@ -144,7 +144,7 @@ def image_has_multiboot(path):
return any(marker in blob for marker in MULTIBOOT_MARKERS)
def create_app(client, frame_addr: int, status_addr: int, scale: int = 4,
def create_app(client, frame_addr: int = None, status_addr: int = None, scale: int = 4,
supervisor=None, log=None, image=None, boot_key=None, flash=None):
app = Flask(__name__)
@@ -154,6 +154,46 @@ def create_app(client, frame_addr: int, status_addr: int, scale: int = 4,
# One background grabber for every client. client may be None: the emulator
# can be powered off, and the page still has to load.
told_addresses = frame_addr is not None and status_addr is not None
frame_addr = frame_addr or 0
status_addr = status_addr or 0
buffers = {"info": None}
def ensure_buffers():
"""Ask the firmware where it keeps its screen, instead of being told.
The addresses move between builds, and passing one build's for another is how
the page ended up drawing a picture that was plausible and offset. The panel
path needs no addresses at all, so this concerns only the guest-RAM fallback:
it stays unset, and says so, rather than guessing. A --frame-addr on the
command line skips the search and is trusted.
"""
if told_addresses:
return {"frame": frame_addr, "status": status_addr, "how": "command line"}
if buffers["info"] is not None:
return buffers["info"]
target = active_client()
if target is None:
return None
try:
import uvk5_buffers
path = image.current.path if image is not None and image.current else None
info = uvk5_buffers.discover(target, image_path=path)
except Exception as exc:
info = {"frame": None, "status": None,
"how": "discovery failed: %s" % exc, "score": 0, "total": 0}
buffers["info"] = info
if info.get("frame"):
pump.set_buffers(info["frame"], info["status"])
log.add("qemu", "screen buffers read from the firmware: frame 0x%08X, "
"status 0x%08X (%s; %s/%s bytes agree)"
% (info["frame"], info["status"], info["how"],
info["score"], info["total"]))
else:
log.add("qemu", "no screen buffers found for this firmware (%s); the panel "
"path does not need them" % info["how"])
return info
pump = FramePump(client, frame_addr, status_addr, fps=TARGET_FPS, scale=scale,
on_fallback=lambda note: log.add(
"qemu", "panel unavailable, drawing from guest RAM at "
@@ -274,7 +314,7 @@ def create_app(client, frame_addr: int, status_addr: int, scale: int = 4,
return jsonify(powered=False, status="unreachable", error=str(exc))
return jsonify(powered=True, speaker=speaker_on(),
panel=panel_state(), firmware=firmware_info(),
frame_source=pump.source()[0], **info)
frame_source=pump.source()[0], buffers=ensure_buffers(), **info)
@app.get("/api/panel")
def api_panel():
@@ -288,11 +328,11 @@ def create_app(client, frame_addr: int, status_addr: int, scale: int = 4,
can be compared without guessing.
"""
source, note = pump.source()
body = {"source": source, "note": note,
body = {"source": source, "note": note, "buffers": ensure_buffers(),
"frame_addr": frame_addr, "status_addr": status_addr}
target = active_client()
if target is None:
return jsonify(body, powered=False)
return jsonify(dict(body, powered=False))
try:
body["gram"] = target.command("qom-get", path=PANEL_PATH, property="gram")
body["invert"] = bool(target.command("qom-get", path=PANEL_PATH,
@@ -1220,10 +1260,14 @@ def _default_firmware():
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument("--qmp", default="/tmp/uvk5-qmp.sock")
ap.add_argument("--frame-addr", type=lambda v: int(v, 0), required=True,
help="address of gFrameBuffer (moves between builds)")
ap.add_argument("--status-addr", type=lambda v: int(v, 0), required=True,
help="address of gStatusLine")
# Optional, and normally omitted: the addresses move between builds, so the page
# finds them by asking the firmware (tools/uvk5_buffers.py). They are only needed
# by the guest-RAM fallback -- the panel path, which is what the page uses, needs
# no addresses at all. Passing one skips discovery and trusts the value.
ap.add_argument("--frame-addr", type=lambda v: int(v, 0), default=None,
help="address of gFrameBuffer; omit to find it in the firmware")
ap.add_argument("--status-addr", type=lambda v: int(v, 0), default=None,
help="address of gStatusLine; omit to find it in the firmware")
ap.add_argument("--host", default="127.0.0.1")
ap.add_argument("--port", type=int, default=8080)
ap.add_argument("--scale", type=int, default=4)