fix(lf): BigBuf download spoke the wrong protocol — LF sample download hung
download_samples sent a DOWNLOAD_BIGBUF request per chunk and read replies with the NG-only reader. But the firmware answers ONE request by streaming the data as OLD-format CMD_DOWNLOADED_BIGBUF frames (no magic) then a single NG ACK (appmain.c) — so the NG reader scanned for a magic that never came and timed out (hardware repro: lf.read() ok, download_samples -> Response timeout). This blocked all LF identify on real hardware. - transport.download_bulk: send the request once, then read with the existing NG/OLD- agnostic reader, accumulating CMD_DOWNLOADED_BIGBUF payloads (trimmed to oldarg[1], not the 512-byte padding) until the ACK or `count` bytes; drains the trailing ACK so it can't poison the next command. Reusable for EML/SPIFFS/flashmem bulk reads. - download_samples: one call into download_bulk instead of the broken per-chunk loop. Tests: count-terminated + ACK-terminated streams, oldarg-length trimming, single request. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -180,34 +180,18 @@ class LFCommands:
|
|||||||
on_progress: ProgressCallback = None) -> bytes:
|
on_progress: ProgressCallback = None) -> bytes:
|
||||||
"""Download raw ADC samples from the device's BigBuf.
|
"""Download raw ADC samples from the device's BigBuf.
|
||||||
|
|
||||||
Must call read() or search() first to capture samples.
|
Must call read() or search() first to capture samples. Returns raw bytes (1 byte per
|
||||||
Returns raw bytes (1 byte per sample, unsigned 0-255).
|
sample, unsigned 0-255). The device streams the data as OLD-format CMD_DOWNLOADED_BIGBUF
|
||||||
|
frames then a final ACK — handled by the transport's bulk-download reader.
|
||||||
"""
|
"""
|
||||||
buf = bytearray()
|
data = await self._t.download_bulk(
|
||||||
remaining = count
|
Cmd.DOWNLOAD_BIGBUF, start, count, Cmd.DOWNLOADED_BIGBUF, Cmd.ACK, timeout=5.0,
|
||||||
offset = start
|
|
||||||
|
|
||||||
while remaining > 0:
|
|
||||||
chunk = min(remaining, PM3_CMD_DATA_SIZE)
|
|
||||||
resp = await self._t.send_mix(
|
|
||||||
Cmd.DOWNLOAD_BIGBUF, offset, chunk, 0, timeout=5.0,
|
|
||||||
)
|
)
|
||||||
# Device responds with CMD_DOWNLOADED_BIGBUF containing the data
|
|
||||||
dl_resp = await self._t.read_response(timeout=5.0)
|
|
||||||
if dl_resp.data:
|
|
||||||
buf.extend(dl_resp.data)
|
|
||||||
|
|
||||||
if on_progress:
|
if on_progress:
|
||||||
ret = on_progress({
|
ret = on_progress({"type": "download", "downloaded": len(data), "total": count})
|
||||||
"type": "download", "downloaded": len(buf), "total": count,
|
|
||||||
})
|
|
||||||
if asyncio.iscoroutine(ret):
|
if asyncio.iscoroutine(ret):
|
||||||
await ret
|
await ret
|
||||||
|
return data
|
||||||
offset += chunk
|
|
||||||
remaining -= chunk
|
|
||||||
|
|
||||||
return bytes(buf[:count])
|
|
||||||
|
|
||||||
async def search(self, on_progress: ProgressCallback = None) -> "LFSearchResult":
|
async def search(self, on_progress: ProgressCallback = None) -> "LFSearchResult":
|
||||||
"""Search for LF tags. Captures samples and returns a result with callable next steps.
|
"""Search for LF tags. Captures samples and returns a result with callable next steps.
|
||||||
|
|||||||
@@ -278,6 +278,38 @@ class PM3Transport:
|
|||||||
frame = encode_old_frame(cmd, arg0, arg1, arg2, data)
|
frame = encode_old_frame(cmd, arg0, arg1, arg2, data)
|
||||||
await self.send_frame(frame)
|
await self.send_frame(frame)
|
||||||
|
|
||||||
|
async def download_bulk(self, cmd: int, start: int, count: int, data_cmd: int,
|
||||||
|
ack_cmd: int, timeout: float = 5.0, on_bytes=None) -> bytes:
|
||||||
|
"""Run a bulk BigBuf/EML/SPIFFS download and return the concatenated bytes.
|
||||||
|
|
||||||
|
The firmware answers one download request (a MIX frame) by streaming ``count`` bytes as
|
||||||
|
a run of *OLD*-format ``data_cmd`` frames (no magic — that's why the plain NG reader
|
||||||
|
can't see them and the naive download hangs), then a final ``ack_cmd`` frame. We read
|
||||||
|
with the NG/OLD-agnostic reader until the ACK, or until we've collected ``count`` bytes
|
||||||
|
(draining the trailing ACK best-effort so it can't poison the next command)."""
|
||||||
|
async with self._lock:
|
||||||
|
await self.send_frame(encode_mix_frame(cmd, start, count, 0))
|
||||||
|
buf = bytearray()
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
resp = await self._read_any_response(timeout=timeout)
|
||||||
|
except PM3Error:
|
||||||
|
break # timeout (e.g. ACK lost) — return what we have
|
||||||
|
if resp.cmd == ack_cmd:
|
||||||
|
break
|
||||||
|
if resp.cmd == data_cmd and resp.data:
|
||||||
|
length = resp.oldarg[1] if resp.oldarg and len(resp.oldarg) > 1 else len(resp.data)
|
||||||
|
buf.extend(resp.data[:max(0, length)])
|
||||||
|
if on_bytes is not None:
|
||||||
|
on_bytes(len(buf))
|
||||||
|
if count and len(buf) >= count:
|
||||||
|
try: # got all the data; consume the trailing ACK
|
||||||
|
await self._read_any_response(timeout=1.0)
|
||||||
|
except PM3Error:
|
||||||
|
pass
|
||||||
|
break
|
||||||
|
return bytes(buf[:count]) if count else bytes(buf)
|
||||||
|
|
||||||
async def _read_any_response(self, timeout: float = 5.0) -> PM3Response:
|
async def _read_any_response(self, timeout: float = 5.0) -> PM3Response:
|
||||||
"""Read one frame that may be NG or OLD and return a PM3Response."""
|
"""Read one frame that may be NG or OLD and return a PM3Response."""
|
||||||
if not self._reader:
|
if not self._reader:
|
||||||
|
|||||||
@@ -79,3 +79,57 @@ def test_decode_response_frame_error_status():
|
|||||||
resp = decode_response_frame(frame)
|
resp = decode_response_frame(frame)
|
||||||
assert resp["status"] == -4 # ETIMEOUT
|
assert resp["status"] == -4 # ETIMEOUT
|
||||||
assert resp["data"] == b""
|
assert resp["data"] == b""
|
||||||
|
|
||||||
|
|
||||||
|
# --- bulk download (BigBuf): one request -> OLD data frames -> ACK -----------------------------
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from unittest.mock import AsyncMock
|
||||||
|
from pm3py.core.transport import PM3Transport, PM3Response
|
||||||
|
from pm3py.core.protocol import PM3_CMD_DATA_SIZE
|
||||||
|
|
||||||
|
|
||||||
|
def _run(coro):
|
||||||
|
return asyncio.get_event_loop().run_until_complete(coro)
|
||||||
|
|
||||||
|
|
||||||
|
def _bigbuf_frame(offset, data):
|
||||||
|
# OLD-format CMD_DOWNLOADED_BIGBUF: oldarg=[offset, len, tracelen], data padded to 512
|
||||||
|
return PM3Response(cmd=int(Cmd.DOWNLOADED_BIGBUF), status=0, reason=0, ng=False,
|
||||||
|
data=data + b"\x00" * (PM3_CMD_DATA_SIZE - len(data)),
|
||||||
|
oldarg=[offset, len(data), 4242])
|
||||||
|
|
||||||
|
|
||||||
|
def _ack():
|
||||||
|
return PM3Response(cmd=int(Cmd.ACK), status=0, reason=0, ng=False, data=b"", oldarg=[1, 0, 0])
|
||||||
|
|
||||||
|
|
||||||
|
def _transport_with(frames):
|
||||||
|
t = PM3Transport("/dev/null")
|
||||||
|
t.send_frame = AsyncMock()
|
||||||
|
t._read_any_response = AsyncMock(side_effect=frames)
|
||||||
|
return t
|
||||||
|
|
||||||
|
|
||||||
|
def test_download_bulk_count_terminated():
|
||||||
|
payload = bytes((i * 7) % 256 for i in range(1224))
|
||||||
|
t = _transport_with([_bigbuf_frame(0, payload[0:512]), _bigbuf_frame(512, payload[512:1024]),
|
||||||
|
_bigbuf_frame(1024, payload[1024:1224]), _ack()])
|
||||||
|
got = _run(t.download_bulk(Cmd.DOWNLOAD_BIGBUF, 0, 1224, Cmd.DOWNLOADED_BIGBUF, Cmd.ACK))
|
||||||
|
assert got == payload # exactly count bytes, reassembled in order
|
||||||
|
assert t.send_frame.call_count == 1 # ONE request, not one per chunk
|
||||||
|
|
||||||
|
|
||||||
|
def test_download_bulk_ack_terminated_early():
|
||||||
|
# device has fewer bytes than requested -> stops at the ACK, returns what came
|
||||||
|
payload = bytes(range(200))
|
||||||
|
t = _transport_with([_bigbuf_frame(0, payload), _ack()])
|
||||||
|
got = _run(t.download_bulk(Cmd.DOWNLOAD_BIGBUF, 0, 99999, Cmd.DOWNLOADED_BIGBUF, Cmd.ACK))
|
||||||
|
assert got == payload
|
||||||
|
|
||||||
|
|
||||||
|
def test_download_bulk_uses_oldarg_length_not_padding():
|
||||||
|
# the OLD frame is always 512 bytes; only oldarg[1] bytes are valid (rest is padding)
|
||||||
|
t = _transport_with([_bigbuf_frame(0, b"\xAA\xBB\xCC"), _ack()])
|
||||||
|
got = _run(t.download_bulk(Cmd.DOWNLOAD_BIGBUF, 0, 3, Cmd.DOWNLOADED_BIGBUF, Cmd.ACK))
|
||||||
|
assert got == b"\xAA\xBB\xCC" # trimmed to oldarg length, not 512 of padding
|
||||||
|
|||||||
Reference in New Issue
Block a user