pureboot: the info block is what proves a knock landed
A prompt byte alone does not: one left over from a previous session can still be in the pipeline while the port opening resets the device into a fresh window, where the bare command that follows is discarded. Each attempt is now the whole handshake, retried until the block comes back. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -280,6 +280,68 @@ def main():
|
||||
if device.writes != pb.RETRIES + 1:
|
||||
fail(f"unrepairable page took {device.writes} writes, expected {pb.RETRIES + 1}")
|
||||
|
||||
# The knock handshake against a device that is not listening yet — the
|
||||
# state a port open leaves behind: it resets the chip into a fresh
|
||||
# activation window while the previous session's prompt is still in
|
||||
# flight, so the first knock is lost and a prompt arrives anyway.
|
||||
class FakePort:
|
||||
"""A loader in its activation window, plus `lost` leading writes the
|
||||
reset swallows and one stale prompt still on the wire."""
|
||||
|
||||
def __init__(self, info_raw, lost=0, stale=b"", active=False):
|
||||
self.info_raw = info_raw
|
||||
self.lost = lost
|
||||
self.inflight = bytearray(stale)
|
||||
self.rx = bytearray()
|
||||
self.active = active
|
||||
self.last = None
|
||||
|
||||
def flush_input(self):
|
||||
self.rx.clear()
|
||||
|
||||
def write(self, data):
|
||||
if self.lost:
|
||||
self.lost -= 1
|
||||
return
|
||||
for byte in bytes(data):
|
||||
if not self.active:
|
||||
self.active = self.last == ord("p") and byte == ord("b")
|
||||
self.last = byte
|
||||
if self.active:
|
||||
self.rx += pb.PROMPT
|
||||
elif byte == ord("b"):
|
||||
self.rx += self.info_raw + pb.PROMPT
|
||||
else:
|
||||
self.rx += pb.PROMPT
|
||||
|
||||
def read_available(self, wait):
|
||||
self.rx = self.inflight + self.rx # the stale prompt lands late
|
||||
self.inflight.clear()
|
||||
out, self.rx = bytes(self.rx), bytearray()
|
||||
return out
|
||||
|
||||
def read_exact(self, count, timeout):
|
||||
if len(self.rx) < count:
|
||||
raise pb.Error(f"timeout: got {len(self.rx)} of {count} bytes")
|
||||
out, self.rx = bytes(self.rx[:count]), self.rx[count:]
|
||||
return out
|
||||
|
||||
raw = info_of(pb, 0x7E00, 128, False, 0x8000).raw
|
||||
for what, port in (
|
||||
("clean window", FakePort(raw)),
|
||||
("stale prompt over a lost knock", FakePort(raw, lost=1, stale=pb.PROMPT)),
|
||||
("live session", FakePort(raw, active=True)),
|
||||
):
|
||||
info = pb.Loader(port).connect(5)
|
||||
if info.raw != raw:
|
||||
fail(f"connect ({what}) returned {info.raw.hex()}")
|
||||
|
||||
# A device that never answers still says so, and a version the tool cannot
|
||||
# speak is reported as such rather than retried into a timeout.
|
||||
expect_error("dead device", lambda: pb.Loader(FakePort(raw, lost=99)).connect(0), "no answer")
|
||||
old = bytes(raw[:2]) + bytes((pb.NEWEST_LOADER + 1,)) + bytes(raw[3:])
|
||||
expect_error("unspeakable version", lambda: pb.Loader(FakePort(old)).connect(5), "needs a newer tool")
|
||||
|
||||
print("test_planner: all planner and policy checks pass")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user