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:
michael
2026-07-14 16:11:11 -07:00
parent 5dd8e086cc
commit 2f221644f9
3 changed files with 97 additions and 27 deletions

View File

@@ -180,34 +180,18 @@ class LFCommands:
on_progress: ProgressCallback = None) -> bytes:
"""Download raw ADC samples from the device's BigBuf.
Must call read() or search() first to capture samples.
Returns raw bytes (1 byte per sample, unsigned 0-255).
Must call read() or search() first to capture samples. Returns raw bytes (1 byte per
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()
remaining = count
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:
ret = on_progress({
"type": "download", "downloaded": len(buf), "total": count,
})
if asyncio.iscoroutine(ret):
await ret
offset += chunk
remaining -= chunk
return bytes(buf[:count])
data = await self._t.download_bulk(
Cmd.DOWNLOAD_BIGBUF, start, count, Cmd.DOWNLOADED_BIGBUF, Cmd.ACK, timeout=5.0,
)
if on_progress:
ret = on_progress({"type": "download", "downloaded": len(data), "total": count})
if asyncio.iscoroutine(ret):
await ret
return data
async def search(self, on_progress: ProgressCallback = None) -> "LFSearchResult":
"""Search for LF tags. Captures samples and returns a result with callable next steps.

View File

@@ -278,6 +278,38 @@ class PM3Transport:
frame = encode_old_frame(cmd, arg0, arg1, arg2, data)
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:
"""Read one frame that may be NG or OLD and return a PM3Response."""
if not self._reader: