mirror of
https://github.com/MCKero6423/uv-k5-v3-emulator.git
synced 2026-10-02 03:15:36 +00:00
Fix the two pixel bugs behind "the other firmware looks shifted"
uvk5_stream.py used STATUS_BYTES without importing it, so the panel branch raised NameError on every frame and a bare except swallowed it: every screen the page drew came from guest RAM at one build's addresses. The pump now reports which source it used and why, webui exposes it (/api/panel and frame_source), and test_uvk5_stream asserts the panel wins when reachable and that a fallback is announced. The ST7565 column counter wrapped at 128 instead of the controller's 132, so addresses 128..131 came back as 0..3, fell outside the col>=4 store, and were dropped: every row lost its last four pixels, which is where the battery icon lives. Pre-fix, filling a page with 0xFF left columns 124..127 blank; now they carry content and the page's frame matches the panel memory 8192/8192.
This commit is contained in:
1 parent
f4c9d343fc
commit
57106c0a66
6 files changed
+202
-7
No files matched your search
@@ -5,6 +5,7 @@ import threading
|
||||
import time
|
||||
import unittest
|
||||
|
||||
from uvk5_lcd import LCD_HEIGHT, LCD_WIDTH, STATUS_BYTES, TOTAL_ROWS, unpack
|
||||
from uvk5_stream import FramePump
|
||||
|
||||
|
||||
@@ -130,5 +131,66 @@ class TestFramePump(unittest.TestCase):
|
||||
self.assertTrue(wait_for_frame(pump), "did not resume after rebind")
|
||||
|
||||
|
||||
class FakePanel:
|
||||
"""A grabber whose panel works, and whose guest RAM must never be touched.
|
||||
|
||||
read() raises, so any test that silently takes the fallback fails loudly instead of
|
||||
comparing two wrong pictures -- which is how the bug below stayed invisible: the
|
||||
fake client used by the tests above answers memsave and nothing else, so the panel
|
||||
branch failed there too, the bare except swallowed it, and every one of them was
|
||||
really exercising the fallback.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.gram = bytes((i * 7) & 0xFF for i in range(TOTAL_ROWS * LCD_WIDTH))
|
||||
|
||||
def panel_gram(self):
|
||||
return self.gram
|
||||
|
||||
def panel_pixels(self):
|
||||
return unpack(self.gram[:STATUS_BYTES], self.gram[STATUS_BYTES:])
|
||||
|
||||
def raw(self):
|
||||
raise AssertionError("guest RAM was read while the panel was available")
|
||||
|
||||
|
||||
class TestFrameSource(unittest.TestCase):
|
||||
"""Which memory the page draws is not a detail -- it decides whose screen is right.
|
||||
|
||||
The panel is the only firmware-independent source: every build pushes pixels through
|
||||
the same controller, while guest RAM is correct only for the build whose buffer
|
||||
addresses were passed in. So when the panel is reachable it must be used.
|
||||
|
||||
uvk5_stream.py used STATUS_BYTES without importing it. The panel branch therefore
|
||||
raised NameError on every frame, a bare except swallowed it, and every screen the
|
||||
page drew came from guest RAM at one firmware's addresses -- right-looking for that
|
||||
firmware, plausible and offset for any other. These two tests fail on that code.
|
||||
"""
|
||||
|
||||
def test_the_panel_is_used_when_it_is_there(self):
|
||||
pump = FramePump(None, 0, 0)
|
||||
status, frame, pixels = pump._grab(FakePanel())
|
||||
self.assertEqual(pump.source()[0], "panel",
|
||||
"the panel is reachable, so guest RAM must not be used")
|
||||
self.assertIsNone(pump.source()[1])
|
||||
self.assertEqual(len(status), STATUS_BYTES)
|
||||
self.assertEqual(len(frame), TOTAL_ROWS * LCD_WIDTH - STATUS_BYTES)
|
||||
# unpack() returns one entry per LCD line, not per page: eight pages of eight.
|
||||
self.assertEqual(len(pixels), LCD_HEIGHT)
|
||||
self.assertEqual(len(pixels[0]), LCD_WIDTH)
|
||||
|
||||
def test_a_broken_panel_falls_back_and_says_why(self):
|
||||
notes = []
|
||||
pump = FramePump(None, 0, 0, on_fallback=notes.append)
|
||||
broken = FakePanel()
|
||||
broken.panel_gram = lambda: (_ for _ in ()).throw(RuntimeError("no panel model"))
|
||||
broken.raw = lambda: (b"\x00" * STATUS_BYTES,
|
||||
b"\x00" * (TOTAL_ROWS * LCD_WIDTH - STATUS_BYTES))
|
||||
pump._grab(broken)
|
||||
self.assertEqual(pump.source()[0], "framebuffer")
|
||||
self.assertIn("no panel model", pump.source()[1])
|
||||
self.assertEqual(len(notes), 1, "the fallback is announced once, with its reason")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+40
-3
@@ -17,12 +17,24 @@ looks live.
|
||||
import threading
|
||||
import time
|
||||
|
||||
from uvk5_lcd import FrameGrabber, default_spool_dir, encode_png, unpack
|
||||
# STATUS_BYTES is not decoration: _grab() splits the controller's memory into the
|
||||
# status line and the frame with it. It was missing from this import for the whole
|
||||
# life of the panel path, so the panel branch raised NameError on every frame, the
|
||||
# bare except below swallowed it, and every picture the page ever drew came from the
|
||||
# guest-RAM fallback instead -- which needs *that build's* buffer addresses. With the
|
||||
# CN addresses and a CN firmware that looked right; with any other firmware the screen
|
||||
# was plausible and offset, which is exactly how it was reported.
|
||||
from uvk5_lcd import STATUS_BYTES, FrameGrabber, default_spool_dir, encode_png, unpack
|
||||
|
||||
|
||||
class FramePump:
|
||||
def __init__(self, client, frame_addr: int, status_addr: int,
|
||||
fps: int = 15, scale: int = 4, spool_dir: str = None):
|
||||
fps: int = 15, scale: int = 4, spool_dir: str = None,
|
||||
on_fallback=None):
|
||||
# Called once with the reason the first time a frame has to come from guest RAM.
|
||||
# It used to be swallowed whole, which is how a NameError in the panel branch
|
||||
# went unnoticed for as long as the fallback kept producing plausible pictures.
|
||||
self._on_fallback = on_fallback
|
||||
self._frame_addr = frame_addr
|
||||
self._status_addr = status_addr
|
||||
# Resolved by FrameGrabber: /dev/shm on Linux, the temp directory on
|
||||
@@ -36,6 +48,13 @@ class FramePump:
|
||||
self._png = None
|
||||
self._raw = None
|
||||
self._generation = 0
|
||||
# Which memory the pixels came from. The panel is preferred and is the only
|
||||
# source that is firmware-independent; the guest-RAM fallback needs that
|
||||
# build's own buffer addresses, so getting it wrong shows up as a picture
|
||||
# that is plausible but offset. Reporting it is what turns "the screen looks
|
||||
# wrong" into "it came from the fallback, and here is why".
|
||||
self._source = None
|
||||
self._note = None
|
||||
self._stop = threading.Event()
|
||||
self._thread = None
|
||||
|
||||
@@ -91,6 +110,11 @@ class FramePump:
|
||||
self._stop.wait(slack)
|
||||
|
||||
|
||||
def source(self):
|
||||
"""(which memory the last frame came from, why the fallback happened)."""
|
||||
with self._lock:
|
||||
return self._source, self._note
|
||||
|
||||
def _grab(self, grabber):
|
||||
"""One frame: (status, frame, pixels), from the panel if it is there.
|
||||
|
||||
@@ -105,8 +129,21 @@ class FramePump:
|
||||
try:
|
||||
pixels = grabber.panel_pixels()
|
||||
gram = grabber.panel_gram()
|
||||
self._source = "panel"
|
||||
self._note = None
|
||||
return gram[:STATUS_BYTES], gram[STATUS_BYTES:], pixels
|
||||
except Exception:
|
||||
except Exception as exc:
|
||||
# Remember the first reason rather than overwriting it every frame: the
|
||||
# cause does not change while the emulator runs, and the first one is
|
||||
# the informative one.
|
||||
if self._source != "framebuffer":
|
||||
self._note = "%s: %s" % (type(exc).__name__, exc)
|
||||
if self._on_fallback is not None:
|
||||
try:
|
||||
self._on_fallback(self._note)
|
||||
except Exception:
|
||||
pass
|
||||
self._source = "framebuffer"
|
||||
status, frame = grabber.raw()
|
||||
return status, frame, unpack(status, frame)
|
||||
def latest(self):
|
||||
|
||||
+32
-2
@@ -154,7 +154,10 @@ 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.
|
||||
pump = FramePump(client, frame_addr, status_addr, fps=TARGET_FPS, scale=scale)
|
||||
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 "
|
||||
f"0x{frame_addr:08X}: {note}"))
|
||||
pump.start()
|
||||
app.config["PUMP"] = pump
|
||||
app.config["SUPERVISOR"] = supervisor
|
||||
@@ -270,7 +273,34 @@ def create_app(client, frame_addr: int, status_addr: int, scale: int = 4,
|
||||
# The emulator can die under us; that is a state to report, not a 500.
|
||||
return jsonify(powered=False, status="unreachable", error=str(exc))
|
||||
return jsonify(powered=True, speaker=speaker_on(),
|
||||
panel=panel_state(), firmware=firmware_info(), **info)
|
||||
panel=panel_state(), firmware=firmware_info(),
|
||||
frame_source=pump.source()[0], **info)
|
||||
|
||||
@app.get("/api/panel")
|
||||
def api_panel():
|
||||
"""The display controller's own memory, and where /frame.png came from.
|
||||
|
||||
/frame.png prefers the panel and falls back to guest RAM when the panel model
|
||||
is not there. The fallback needs the addresses of *that* build's buffers, so
|
||||
it is the path that can look right and be offset at the same time -- and a
|
||||
caller cannot tell which it got from the picture. This says which, gives the
|
||||
reason for a fallback, and hands back the controller's own bytes so the two
|
||||
can be compared without guessing.
|
||||
"""
|
||||
source, note = pump.source()
|
||||
body = {"source": source, "note": note,
|
||||
"frame_addr": frame_addr, "status_addr": status_addr}
|
||||
target = active_client()
|
||||
if target is None:
|
||||
return jsonify(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,
|
||||
property="invert"))
|
||||
body["bytes"] = len(body["gram"]) // 2
|
||||
except Exception as exc:
|
||||
body["panel_error"] = str(exc)
|
||||
return jsonify(body)
|
||||
|
||||
@app.get("/api/firmware")
|
||||
def api_firmware():
|
||||
|
||||
Reference in new issue
Block a user