mirror of
https://github.com/MCKero6423/uv-k5-v3-emulator.git
synced 2026-10-02 03:15:36 +00:00
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:
1 parent
57106c0a66
commit
1e9fdf685c
8 files changed
+419
-21
No files matched your search
@@ -493,6 +493,38 @@ substitutes a different source turns a hard error into a plausible wrong answer*
|
|||||||
byte that is off by four is invisible until something that matters lives in those four
|
byte that is off by four is invisible until something that matters lives in those four
|
||||||
columns. Report the source, and test that the preferred path is actually taken.
|
columns. Report the source, and test that the preferred path is actually taken.
|
||||||
|
|
||||||
|
### The screen buffers are found, not hardcoded
|
||||||
|
|
||||||
|
The flag was `--frame-addr 0x200012BE --status-addr 0x2000163E` -- one build's
|
||||||
|
addresses, in the launcher, as a default. Pointed at another firmware that reads
|
||||||
|
somewhere else, the picture is plausible and wrong: measured, the build the user
|
||||||
|
actually flashed keeps its buffers at `0x2000129E` / `0x2000161E`, exactly 32 bytes
|
||||||
|
earlier, so every line landed 32 bytes off. That is what "the other firmware looks
|
||||||
|
shifted" was.
|
||||||
|
|
||||||
|
Nothing needs to be assumed. The firmware images here are minimal ELFs -- one program
|
||||||
|
header, no section headers, no symbol table (tools/bin2elf.py writes them) -- so there
|
||||||
|
are no `gFrameBuffer` symbols to read, but there is behaviour: the firmware's own
|
||||||
|
buffers hold the same bytes the controller holds, because that is where the driver
|
||||||
|
copied them from. `tools/uvk5_buffers.py` slides the controller's memory through SRAM
|
||||||
|
and keeps the offset that agrees; it reported 1024/1024 bytes and the right pair of
|
||||||
|
addresses for the exact file the user flashed.
|
||||||
|
|
||||||
|
So `--frame-addr` and `--status-addr` are optional now, `work/run-webui.ps1` no
|
||||||
|
longer passes them (or any machine-specific path), and the page reports what it found:
|
||||||
|
|
||||||
|
buffers: {"frame": 0x2000129E, "status": 0x2000161E, "how": "sram search",
|
||||||
|
"score": 1024, "total": 1024}
|
||||||
|
|
||||||
|
Two habits from this, both already in this file in other words: **a default that names
|
||||||
|
one machine's or one build's value is a bug waiting for a second build**, and **when
|
||||||
|
there are no symbols to read, ask the thing itself** -- the bytes in the buffers are
|
||||||
|
the answer, and they can be found by matching rather than guessed.
|
||||||
|
|
||||||
|
The panel path needs none of this, and is what the page draws from: the controller's
|
||||||
|
memory is the screen for every firmware. The addresses only serve the guest-RAM
|
||||||
|
fallback, which is why a failed search is reported and does not stop anything.
|
||||||
|
|
||||||
## The keypad: two real bugs, both fixed
|
## The keypad: two real bugs, both fixed
|
||||||
|
|
||||||
The old note here said "keys reach the firmware but the UI does not react" and
|
The old note here said "keys reach the firmware but the UI does not react" and
|
||||||
|
|||||||
@@ -400,6 +400,31 @@ PTT+SIDE1/SIDE2 与 MENU(那是应用的几个特殊模式)、开机窗口
|
|||||||
错答案**;而一个偏了四列的字节,在"重要的东西恰好住在那四列里"之前,是看不出来的。要报出来源,
|
错答案**;而一个偏了四列的字节,在"重要的东西恰好住在那四列里"之前,是看不出来的。要报出来源,
|
||||||
并且要测"该走的那条优选路径确实被走了"。
|
并且要测"该走的那条优选路径确实被走了"。
|
||||||
|
|
||||||
|
### 屏幕缓冲是**找出来**的,不是写死的
|
||||||
|
|
||||||
|
原来是个默认值:`--frame-addr 0x200012BE --status-addr 0x2000163E` —— 那是**某一份构建**的地址,
|
||||||
|
被写进启动脚本当默认值。换成把缓冲放在别处的固件,画面就"貌似合理但错":实测用户真正刷进去的那份,
|
||||||
|
缓冲在 `0x2000129E` / `0x2000161E`,正好**早 32 字节**,于是**每一行都偏 32 字节**。这就是「换个
|
||||||
|
固件就偏移」的本来面目。
|
||||||
|
|
||||||
|
其实什么都不用假设。这里的固件镜像是**最小 ELF** —— 一个程序头、没有节头、没有符号表
|
||||||
|
(`tools/bin2elf.py` 就是这么写的)—— 所以**没有** `gFrameBuffer` 符号可读;但它有**行为**:
|
||||||
|
固件自己的缓冲里存着与控制器**相同的字节**,因为驱动就是从那拷贝过去的。
|
||||||
|
`tools/uvk5_buffers.py` 把控制器显存沿着 SRAM 滑动,取吻合度最高的那个偏移;对用户刷的那份文件,
|
||||||
|
它给出 1024/1024 字节吻合和**正确**的一对地址。
|
||||||
|
|
||||||
|
于是 `--frame-addr` / `--status-addr` 现在都是可选的 ✓,`work/run-webui.ps1` 不再传它们
|
||||||
|
(也不再有任何本机路径 ✓),页面会报出自己找到了什么:
|
||||||
|
|
||||||
|
buffers: {"frame": 0x2000129E, "status": 0x2000161E, "how": "sram search",
|
||||||
|
"score": 1024, "total": 1024}
|
||||||
|
|
||||||
|
两条习惯,这份文件里用别的话说过:**默认值里写进"某台机器/某份构建"的具体值,就是在等第二份构建来踩**;
|
||||||
|
以及 **没有符号可读时,就直接问它本人** —— 缓冲里的字节就是答案,靠**匹配**找出来,而不是猜。
|
||||||
|
|
||||||
|
面板路径完全不需要这些,而页面画的正是它:对任何固件,控制器的显存就是屏幕。这些地址只服务
|
||||||
|
guest RAM 回落路径 —— 所以搜索失败会被报出来,而不会挡住任何东西。
|
||||||
|
|
||||||
## 键盘:两个真 bug,都已修复
|
## 键盘:两个真 bug,都已修复
|
||||||
|
|
||||||
这里原来的笔记写的是"按键到达了固件但界面不反应",并且归咎于机器模型。结果发现有**两个
|
这里原来的笔记写的是"按键到达了固件但界面不反应",并且归咎于机器模型。结果发现有**两个
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""The buffer search must count *matches*, not just bytes compared.
|
||||||
|
|
||||||
|
Its first version summed one per zipped pair and forgot `if a == b`, so every
|
||||||
|
offset scored the full total and the search always answered "the first offset" --
|
||||||
|
which looked like a working discovery and pointed the fallback at whatever happened
|
||||||
|
to sit at the start of SRAM. These tests use synthetic memory, so they need no
|
||||||
|
emulator and fail on that code.
|
||||||
|
"""
|
||||||
|
import struct
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import uvk5_buffers
|
||||||
|
from uvk5_lcd import FRAME_BYTES, STATUS_BYTES
|
||||||
|
|
||||||
|
|
||||||
|
def panel_memory(seed=1):
|
||||||
|
"""A controller memory with a recognisable, non-repeating-ish pattern."""
|
||||||
|
frame = bytes(((i * 13) + seed) & 0xFF for i in range(FRAME_BYTES))
|
||||||
|
status = bytes(((i * 29) + seed) & 0xFF for i in range(STATUS_BYTES))
|
||||||
|
return status + frame # the panel's order: the status page comes first
|
||||||
|
|
||||||
|
|
||||||
|
class TestLocate(unittest.TestCase):
|
||||||
|
def test_it_finds_the_buffers_where_they_are(self):
|
||||||
|
gram = panel_memory()
|
||||||
|
pad = 0x1234
|
||||||
|
# In SRAM the frame comes first and the status line follows it.
|
||||||
|
sram = bytes(pad) + gram[STATUS_BYTES:] + gram[:STATUS_BYTES] + bytes(0x100)
|
||||||
|
|
||||||
|
frame, status, best, total = uvk5_buffers.locate(sram, gram)
|
||||||
|
|
||||||
|
self.assertEqual(frame, uvk5_buffers.SRAM_BASE + pad)
|
||||||
|
self.assertEqual(status, uvk5_buffers.SRAM_BASE + pad + FRAME_BYTES)
|
||||||
|
self.assertEqual(best, total, "a perfect match must score the whole window")
|
||||||
|
self.assertEqual(total, FRAME_BYTES + STATUS_BYTES)
|
||||||
|
|
||||||
|
def test_a_single_match_does_not_win_by_being_first(self):
|
||||||
|
"""The bug this guards: scoring pairs instead of matches picked offset 0."""
|
||||||
|
gram = panel_memory()
|
||||||
|
pad = 0x0800
|
||||||
|
sram = bytes(pad) + gram[STATUS_BYTES:] + gram[:STATUS_BYTES] + bytes(0x40)
|
||||||
|
_, _, best, total = uvk5_buffers.locate(sram, gram)
|
||||||
|
self.assertGreater(best, total * 0.9)
|
||||||
|
self.assertNotEqual(uvk5_buffers.score(sram, 0, gram), total,
|
||||||
|
"offset 0 is blank here and must not score full marks")
|
||||||
|
|
||||||
|
def test_a_partly_stale_buffer_still_wins(self):
|
||||||
|
"""The panel can be a frame ahead of the buffer it was copied from."""
|
||||||
|
gram = panel_memory()
|
||||||
|
pad = 0x0400
|
||||||
|
frame = bytearray(gram[STATUS_BYTES:])
|
||||||
|
for i in range(0, 60):
|
||||||
|
frame[i] ^= 0x5A
|
||||||
|
sram = bytes(pad) + bytes(frame) + gram[:STATUS_BYTES] + bytes(0x80)
|
||||||
|
found, _, best, total = uvk5_buffers.locate(sram, gram)
|
||||||
|
self.assertEqual(found, uvk5_buffers.SRAM_BASE + pad)
|
||||||
|
# What matters is that the right offset wins, not that it is perfect: the
|
||||||
|
# panel can be a frame ahead of the buffer it was copied from.
|
||||||
|
runner_up = max(uvk5_buffers.score(sram, off, gram)
|
||||||
|
for off in range(0, len(sram) - total + 1)
|
||||||
|
if off != pad)
|
||||||
|
self.assertGreater(best, runner_up)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSymbols(unittest.TestCase):
|
||||||
|
def test_a_minimal_elf_has_no_symbols_to_offer(self):
|
||||||
|
"""tools/bin2elf.py writes program headers only, which is the usual case."""
|
||||||
|
elf = bytearray(0x34 + 32 + 0x40)
|
||||||
|
elf[0:4] = b"\x7fELF"
|
||||||
|
elf[4] = 1 # 32-bit
|
||||||
|
elf[5] = 1 # little endian
|
||||||
|
struct.pack_into("<I", elf, 0x1C, 0x34) # one program header
|
||||||
|
struct.pack_into("<H", elf, 0x2A, 32)
|
||||||
|
struct.pack_into("<H", elf, 0x2C, 1)
|
||||||
|
struct.pack_into("<I", elf, 0x20, 0) # no section headers
|
||||||
|
struct.pack_into("<H", elf, 0x30, 0)
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
path = os.path.join(tempfile.mkdtemp(), "minimal.elf")
|
||||||
|
with open(path, "wb") as fh:
|
||||||
|
fh.write(bytes(elf))
|
||||||
|
self.assertEqual(uvk5_buffers.from_symbols(path), (None, None))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Where a firmware keeps its screen: found by looking, not assumed.
|
||||||
|
|
||||||
|
The display controller's memory is the screen: every firmware pushes its pixels
|
||||||
|
through the same controller, so the panel needs no per-build knowledge and is the
|
||||||
|
path the page uses. The *guest* buffers are the fallback, and they move between
|
||||||
|
builds -- so passing one build's addresses for another renders a picture that is
|
||||||
|
plausible and wrong, which is exactly how "the other firmware looks shifted" arrived.
|
||||||
|
|
||||||
|
The firmware files here are minimal ELFs: one program header, no section headers and
|
||||||
|
no symbol table (tools/bin2elf.py writes them), so there are no `gFrameBuffer`
|
||||||
|
symbols to read. What there *is*, is behaviour: the firmware's own buffers hold the
|
||||||
|
same bytes the controller holds, because that is where the driver copied them from.
|
||||||
|
So the addresses are found by sliding the controller's memory through SRAM and
|
||||||
|
keeping the offset that agrees.
|
||||||
|
|
||||||
|
tools/uvk5_buffers.py --qmp 127.0.0.1:4444 # say where this firmware keeps it
|
||||||
|
|
||||||
|
Agreement is not assumed to be perfect: the panel can be a frame ahead of the buffer
|
||||||
|
it was copied from, so the score is reported and a caller decides what to trust.
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
import uvk5_lcd
|
||||||
|
|
||||||
|
# 16 KB of SRAM. The buffers are inside it; nothing else is near.
|
||||||
|
SRAM_BASE = 0x20000000
|
||||||
|
SRAM_SIZE = 0x4000
|
||||||
|
|
||||||
|
# The frame is seven pages and the status line is one, and in SRAM the *frame* comes
|
||||||
|
# first (gFrameBuffer at 0x200012BE, gStatusLine at 0x2000163E for the 5.9.0.CN build).
|
||||||
|
# The panel's own memory has them the other way round, because page 0 is the top line.
|
||||||
|
FRAME_BYTES = uvk5_lcd.FRAME_BYTES
|
||||||
|
STATUS_BYTES = uvk5_lcd.STATUS_BYTES
|
||||||
|
|
||||||
|
# Below this fraction of bytes agreeing, the match is a coincidence rather than a
|
||||||
|
# buffer. Measured: a real match scores above 0.99 unless the screen is mid-update.
|
||||||
|
CONFIDENT = 0.90
|
||||||
|
|
||||||
|
|
||||||
|
def score(sram: bytes, offset: int, gram: bytes) -> int:
|
||||||
|
"""How many bytes at @offset match the controller's memory, frame then status."""
|
||||||
|
if offset < 0 or offset + FRAME_BYTES + STATUS_BYTES > len(sram):
|
||||||
|
return -1
|
||||||
|
frame = sram[offset:offset + FRAME_BYTES]
|
||||||
|
status = sram[offset + FRAME_BYTES:offset + FRAME_BYTES + STATUS_BYTES]
|
||||||
|
return (sum(1 for a, b in zip(frame, gram[STATUS_BYTES:]) if a == b)
|
||||||
|
+ sum(1 for a, b in zip(status, gram[:STATUS_BYTES]) if a == b))
|
||||||
|
|
||||||
|
|
||||||
|
def locate(sram: bytes, gram: bytes):
|
||||||
|
"""(frame address, status address, matching bytes, total) for the best offset."""
|
||||||
|
total = FRAME_BYTES + STATUS_BYTES
|
||||||
|
best, best_at = -1, None
|
||||||
|
for offset in range(0, len(sram) - total + 1):
|
||||||
|
value = score(sram, offset, gram)
|
||||||
|
if value > best:
|
||||||
|
best, best_at = value, offset
|
||||||
|
if best_at is None:
|
||||||
|
return None, None, 0, total
|
||||||
|
return SRAM_BASE + best_at, SRAM_BASE + best_at + FRAME_BYTES, best, total
|
||||||
|
|
||||||
|
|
||||||
|
def from_symbols(path: str):
|
||||||
|
"""(frame, status) from a real ELF symbol table, or (None, None).
|
||||||
|
|
||||||
|
Kept because it is authoritative when it works -- a fully linked ELF (the CW
|
||||||
|
timing build, for instance) does name these buffers. The images this project
|
||||||
|
usually runs do not.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = open(path, "rb").read()
|
||||||
|
except OSError:
|
||||||
|
return None, None
|
||||||
|
if len(data) < 52 or data[0] != 0x7F or data[1:4] != b"ELF" or data[4] != 1:
|
||||||
|
return None, None
|
||||||
|
e_shoff, = struct.unpack_from("<I", data, 0x20)
|
||||||
|
e_shentsize, e_shnum, _ = struct.unpack_from("<HHH", data, 0x2E)
|
||||||
|
if not e_shoff or not e_shnum:
|
||||||
|
return None, None
|
||||||
|
sections = []
|
||||||
|
for i in range(e_shnum):
|
||||||
|
fields = struct.unpack_from("<IIIIIIIIII", data, e_shoff + i * e_shentsize)
|
||||||
|
sections.append(dict(type=fields[1], offset=fields[4], size=fields[5],
|
||||||
|
link=fields[6], entsize=fields[9]))
|
||||||
|
found = {}
|
||||||
|
for section in sections:
|
||||||
|
if section["type"] not in (2, 11):
|
||||||
|
continue
|
||||||
|
strings = sections[section["link"]]
|
||||||
|
blob = data[strings["offset"]:strings["offset"] + strings["size"]]
|
||||||
|
step = section["entsize"] or 16
|
||||||
|
for k in range(section["size"] // step):
|
||||||
|
name, value = struct.unpack_from("<II", data, section["offset"] + k * step)
|
||||||
|
if not name or not value:
|
||||||
|
continue
|
||||||
|
end = blob.find(b"\x00", name)
|
||||||
|
found[blob[name:end].decode("ascii", "replace")] = value
|
||||||
|
frame = found.get("gFrameBuffer")
|
||||||
|
status = found.get("gStatusLine")
|
||||||
|
if frame and status:
|
||||||
|
return frame, status
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def sram_from(client) -> bytes:
|
||||||
|
"""Read the whole of SRAM through QMP memsave."""
|
||||||
|
path = os.path.join(tempfile.mkdtemp(prefix="uvk5-buffers-"), "sram.bin")
|
||||||
|
client.command("memsave", val=SRAM_BASE, size=SRAM_SIZE, filename=path)
|
||||||
|
with open(path, "rb") as fh:
|
||||||
|
return fh.read()
|
||||||
|
|
||||||
|
|
||||||
|
def discover(client, gram: bytes = None, image_path: str = None) -> dict:
|
||||||
|
"""{frame, status, how, score, total} -- symbols first, then the search."""
|
||||||
|
if image_path:
|
||||||
|
frame, status = from_symbols(image_path)
|
||||||
|
if frame:
|
||||||
|
return dict(frame=frame, status=status, how="elf symbols",
|
||||||
|
score=None, total=None)
|
||||||
|
if client is None:
|
||||||
|
return dict(frame=None, status=None, how="no emulator", score=0, total=0)
|
||||||
|
gram = gram if gram is not None else uvk5_lcd.FrameGrabber(client, 0, 0).panel_gram()
|
||||||
|
frame, status, best, total = locate(sram_from(client), gram)
|
||||||
|
how = "sram search" if best >= CONFIDENT * total else "sram search (weak match)"
|
||||||
|
return dict(frame=frame, status=status, how=how, score=best, total=total)
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None):
|
||||||
|
ap = argparse.ArgumentParser(description=__doc__,
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
|
ap.add_argument("--qmp", default="127.0.0.1:4444")
|
||||||
|
ap.add_argument("--image", help="firmware file, to try its symbols first")
|
||||||
|
args = ap.parse_args(argv)
|
||||||
|
|
||||||
|
from uvk5_qmp import QmpClient
|
||||||
|
try:
|
||||||
|
client = QmpClient(args.qmp, timeout=20)
|
||||||
|
except Exception as exc:
|
||||||
|
print("cannot reach QMP at %s: %s" % (args.qmp, str(exc).splitlines()[0]), file=sys.stderr)
|
||||||
|
print("if the web UI is running it holds the one QMP client", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
found = discover(client, image_path=args.image)
|
||||||
|
print("frame 0x%08X" % found["frame"] if found["frame"] else "frame (not found)")
|
||||||
|
print("status 0x%08X" % found["status"] if found["status"] else "status (not found)")
|
||||||
|
print("how %s" % found["how"])
|
||||||
|
if found["score"] is not None:
|
||||||
|
print("match %d/%d bytes" % (found["score"], found["total"]))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -82,6 +82,21 @@ class FramePump:
|
|||||||
self._raw = None
|
self._raw = None
|
||||||
self._generation += 1
|
self._generation += 1
|
||||||
|
|
||||||
|
def set_buffers(self, frame_addr: int, status_addr: int):
|
||||||
|
"""Point the guest-RAM fallback at this firmware's buffers.
|
||||||
|
|
||||||
|
The addresses move between builds, so they are discovered from the firmware
|
||||||
|
itself (tools/uvk5_buffers.py) rather than hardcoded. Only the fallback uses
|
||||||
|
them: the panel path needs none.
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
self._frame_addr = frame_addr
|
||||||
|
self._status_addr = status_addr
|
||||||
|
grabber = self._grabber
|
||||||
|
if grabber is not None:
|
||||||
|
self._grabber = FrameGrabber(grabber._client, frame_addr, status_addr,
|
||||||
|
self._spool_dir)
|
||||||
|
|
||||||
def _run(self):
|
def _run(self):
|
||||||
while not self._stop.is_set():
|
while not self._stop.is_set():
|
||||||
started = time.monotonic()
|
started = time.monotonic()
|
||||||
|
|||||||
+10
-1
@@ -36,7 +36,16 @@ def qemu():
|
|||||||
if value and os.path.exists(value):
|
if value and os.path.exists(value):
|
||||||
return pathlib.Path(value)
|
return pathlib.Path(value)
|
||||||
found = shutil.which("qemu-system-arm")
|
found = shutil.which("qemu-system-arm")
|
||||||
return pathlib.Path(found) if found else None
|
if found:
|
||||||
|
return pathlib.Path(found)
|
||||||
|
# The sibling tree tools/setup_qemu.sh builds into, which is also what run_tests.sh
|
||||||
|
# assumes. Not a machine-specific path: it is this checkout's own convention, so a
|
||||||
|
# fresh clone that followed the README finds its QEMU without being told.
|
||||||
|
sibling = pathlib.Path(__file__).resolve().parent.parent.parent / "qemu-7.2" / "build"
|
||||||
|
for name in ("qemu-system-arm", "qemu-system-arm.exe"):
|
||||||
|
if (sibling / name).exists():
|
||||||
|
return sibling / name
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def gdb():
|
def gdb():
|
||||||
|
|||||||
+52
-8
@@ -144,7 +144,7 @@ def image_has_multiboot(path):
|
|||||||
return any(marker in blob for marker in MULTIBOOT_MARKERS)
|
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):
|
supervisor=None, log=None, image=None, boot_key=None, flash=None):
|
||||||
app = Flask(__name__)
|
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
|
# One background grabber for every client. client may be None: the emulator
|
||||||
# can be powered off, and the page still has to load.
|
# 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,
|
pump = FramePump(client, frame_addr, status_addr, fps=TARGET_FPS, scale=scale,
|
||||||
on_fallback=lambda note: log.add(
|
on_fallback=lambda note: log.add(
|
||||||
"qemu", "panel unavailable, drawing from guest RAM at "
|
"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=False, status="unreachable", error=str(exc))
|
||||||
return jsonify(powered=True, speaker=speaker_on(),
|
return jsonify(powered=True, speaker=speaker_on(),
|
||||||
panel=panel_state(), firmware=firmware_info(),
|
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")
|
@app.get("/api/panel")
|
||||||
def 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.
|
can be compared without guessing.
|
||||||
"""
|
"""
|
||||||
source, note = pump.source()
|
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}
|
"frame_addr": frame_addr, "status_addr": status_addr}
|
||||||
target = active_client()
|
target = active_client()
|
||||||
if target is None:
|
if target is None:
|
||||||
return jsonify(body, powered=False)
|
return jsonify(dict(body, powered=False))
|
||||||
try:
|
try:
|
||||||
body["gram"] = target.command("qom-get", path=PANEL_PATH, property="gram")
|
body["gram"] = target.command("qom-get", path=PANEL_PATH, property="gram")
|
||||||
body["invert"] = bool(target.command("qom-get", path=PANEL_PATH,
|
body["invert"] = bool(target.command("qom-get", path=PANEL_PATH,
|
||||||
@@ -1220,10 +1260,14 @@ def _default_firmware():
|
|||||||
def main() -> int:
|
def main() -> int:
|
||||||
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||||
ap.add_argument("--qmp", default="/tmp/uvk5-qmp.sock")
|
ap.add_argument("--qmp", default="/tmp/uvk5-qmp.sock")
|
||||||
ap.add_argument("--frame-addr", type=lambda v: int(v, 0), required=True,
|
# Optional, and normally omitted: the addresses move between builds, so the page
|
||||||
help="address of gFrameBuffer (moves between builds)")
|
# finds them by asking the firmware (tools/uvk5_buffers.py). They are only needed
|
||||||
ap.add_argument("--status-addr", type=lambda v: int(v, 0), required=True,
|
# by the guest-RAM fallback -- the panel path, which is what the page uses, needs
|
||||||
help="address of gStatusLine")
|
# 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("--host", default="127.0.0.1")
|
||||||
ap.add_argument("--port", type=int, default=8080)
|
ap.add_argument("--port", type=int, default=8080)
|
||||||
ap.add_argument("--scale", type=int, default=4)
|
ap.add_argument("--scale", type=int, default=4)
|
||||||
|
|||||||
+41
-12
@@ -3,6 +3,11 @@
|
|||||||
# powershell -File work\run-webui.ps1 # the page starts the emulator
|
# powershell -File work\run-webui.ps1 # the page starts the emulator
|
||||||
# powershell -File work\run-webui.ps1 -Attach # attach to run-emulator.ps1
|
# powershell -File work\run-webui.ps1 -Attach # attach to run-emulator.ps1
|
||||||
#
|
#
|
||||||
|
# Nothing here is machine-specific. The QEMU binary comes from QEMU or PATH, the
|
||||||
|
# interpreter is the one on PATH, the firmware is whatever the checkout has (or one
|
||||||
|
# uploaded from the page), the flash image is the last one a session used, and the
|
||||||
|
# screen-buffer addresses are read out of the running firmware rather than passed in.
|
||||||
|
#
|
||||||
# Owning the process is what puts the firmware's serial output in the page's log
|
# Owning the process is what puts the firmware's serial output in the page's log
|
||||||
# pane: the model prints it to stderr as "SERIAL ..." and the server reads QEMU's
|
# pane: the model prints it to stderr as "SERIAL ..." and the server reads QEMU's
|
||||||
# stderr. With -Attach the server never sees that stream, so the pane stays empty.
|
# stderr. With -Attach the server never sees that stream, so the pane stays empty.
|
||||||
@@ -10,28 +15,52 @@
|
|||||||
param(
|
param(
|
||||||
[int]$Port = 8080,
|
[int]$Port = 8080,
|
||||||
[int]$QmpPort = 4444,
|
[int]$QmpPort = 4444,
|
||||||
[string]$Frame = '0x200012BE', # gFrameBuffer, proven against the firmware source
|
[string]$Frame = '', # only to override the discovered gFrameBuffer
|
||||||
[string]$Status = '0x2000163E', # gStatusLine
|
[string]$Status = '', # only to override the discovered gStatusLine
|
||||||
[string]$Kernel = "$PSScriptRoot\f4hwn\EGZUMER+F4HWN-v5.9.0.CN.elf",
|
[string]$Kernel = '', # empty: whatever the checkout has, or upload one
|
||||||
[string]$Flash = [System.IO.Path]::GetFullPath("$PSScriptRoot\..\assets\flash.img"),
|
[string]$Flash = '', # empty: the last image a session used
|
||||||
[string]$Qemu = 'F:\dsh-build\qemu-7.2.0\build\qemu-system-arm.exe',
|
[string]$Qemu = '', # empty: QEMU or PATH
|
||||||
[switch]$Attach
|
[switch]$Attach
|
||||||
)
|
)
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
|
Set-Location (Join-Path $PSScriptRoot '..')
|
||||||
|
|
||||||
# The QEMU this server spawns is a native Windows build, so the MSYS2 mingw64 DLLs
|
# The QEMU this server spawns is a native Windows build, so the MSYS2 mingw64 DLLs
|
||||||
# have to be on the child's PATH -- inherited from here, since the child gets this
|
# have to be on the child's PATH -- inherited from here, since the child gets this
|
||||||
# environment. Without it QEMU dies at load and "power on" reports only that the QMP
|
# environment. Without it QEMU dies at load and "power on" reports only that the QMP
|
||||||
# port never appeared.
|
# port never appeared.
|
||||||
$env:PATH = 'F:\msys64\mingw64\bin;' + $env:PATH
|
if (Test-Path 'F:\msys64\mingw64\bin') {
|
||||||
$py = 'C:\Users\Administrator\.dsh\dsh-runtimes\dsh-primary-runtime\dependencies\python\python.exe'
|
$env:PATH = 'F:\msys64\mingw64\bin;' + $env:PATH
|
||||||
Set-Location (Join-Path $PSScriptRoot '..')
|
}
|
||||||
|
|
||||||
|
$py = if ($env:PYTHON) { $env:PYTHON } else {
|
||||||
|
$found = Get-Command python -ErrorAction SilentlyContinue
|
||||||
|
if (-not $found) { $found = Get-Command py -ErrorAction SilentlyContinue }
|
||||||
|
if (-not $found) { throw 'no python on PATH; set PYTHON to one with flask installed' }
|
||||||
|
$found.Source
|
||||||
|
}
|
||||||
|
|
||||||
|
$common = @('tools\webui.py', '--qmp', "127.0.0.1:$QmpPort", '--port', $Port)
|
||||||
|
if ($Frame) { $common += @('--frame-addr', $Frame) }
|
||||||
|
if ($Status) { $common += @('--status-addr', $Status) }
|
||||||
|
|
||||||
|
if (-not $Kernel -and $env:ELF) { $Kernel = $env:ELF }
|
||||||
|
if (-not $Flash) {
|
||||||
|
$candidates = @($env:UVK5_FLASH_IMAGE, 'work\user-flash.img', 'assets\flash.img')
|
||||||
|
foreach ($candidate in $candidates) {
|
||||||
|
if ($candidate -and (Test-Path $candidate)) { $Flash = $candidate; break }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (-not $Qemu -and $env:QEMU) { $Qemu = $env:QEMU }
|
||||||
|
|
||||||
$common = @('tools\webui.py', '--qmp', "127.0.0.1:$QmpPort",
|
|
||||||
'--frame-addr', $Frame, '--status-addr', $Status, '--port', $Port)
|
|
||||||
if ($Attach) {
|
if ($Attach) {
|
||||||
Write-Host "attaching to an emulator already listening on 127.0.0.1:$QmpPort"
|
Write-Host "attaching to an emulator already listening on 127.0.0.1:$QmpPort"
|
||||||
& $py @common --attach
|
& $py @common --attach
|
||||||
} else {
|
} else {
|
||||||
Write-Host "the page will start: $Qemu"
|
if ($Qemu) { $common += @('--qemu', $Qemu) }
|
||||||
& $py @common --qemu $Qemu --elf $Kernel --flash $Flash
|
if ($Kernel) { $common += @('--elf', $Kernel) }
|
||||||
|
if ($Flash) { $common += @('--flash', $Flash) }
|
||||||
|
Write-Host "the page will start: $(if ($Qemu) { $Qemu } else { 'qemu-system-arm from PATH' })"
|
||||||
|
if ($Flash) { Write-Host "flash image: $Flash" }
|
||||||
|
& $py @common
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user