Let the page ask the radio whether it sees an installed app

GET /api/apps/radio opens the firmware's serial port and sends 0x0730 for all sixteen slots, so the answer comes from the running firmware rather than from our reading of the file -- which is the check that matters, because the bytes can be right and the firmware still refuse a slot. Measured through the page after installing Beam.app into slot 0: slot 0 -> Beam 1.0, 1100 B, crc 0xd976058, shortcut beam, committed; slots 1..3 -> status 2 with unrelated data, the resource-block overlap the install guard refuses. A button beside the table asks it and shows the answer in its own column.

The server gives its own emulator a serial port (--serial-port, default 4445) and uvk5_slots_serial.Radio gained a public app_info(slot), so nothing reaches into a private helper. uvk5_apps.parse_radio_reply decodes the answer and is tested without a radio. Also recorded: QEMU needs the mingw64 DLLs on PATH, and started by hand without them it exits before opening QMP, which surfaces only as 'QMP socket never appeared'.
This commit is contained in:
mckero committed 2026-10-01 17:25:32 +08:00
1 parent 0267371e28
commit b855b0b047
9 files changed
+169 -6

No files matched your search

+28
View File
@@ -166,3 +166,31 @@ class TestRealFile(unittest.TestCase):
if __name__ == "__main__":
unittest.main()
class TestRadioReply(unittest.TestCase):
"""The firmware's own answer about a slot, decoded without a radio."""
def test_a_reply_carrying_an_app_header_decodes(self):
blob = A.build(b"\x01" * 32, "Beam", "1.0", shortcut="beam")
reply = bytes([2, 0]) + blob[:A.HDR_SIZE]
info = A.parse_radio_reply(reply)
self.assertEqual(info["slot"], 2)
self.assertEqual(info["status"], 0)
self.assertEqual(info["name"], "Beam")
self.assertEqual(info["version"], "1.0")
self.assertEqual(info["code_size"], 32)
self.assertEqual(info["shortcut"], "beam")
self.assertTrue(info["committed"])
def test_a_reply_from_a_slot_holding_something_else_is_not_an_app(self):
"""Measured on a real image: slots 1..3 answer status 2 with unrelated data."""
info = A.parse_radio_reply(bytes([1, 2]) + b"\xd4\x56\xd1\xf4" + b"\x00" * 60)
self.assertEqual(info["status"], 2)
self.assertNotEqual(info.get("magic"), "FAP1")
self.assertNotIn("name", info)
def test_a_short_reply_says_so_instead_of_inventing_a_header(self):
info = A.parse_radio_reply(bytes([0]))
self.assertIsNone(info["status"])
self.assertIn("short", info["note"])