On the flaky NG link a command's response can arrive *after* its read times out, and the
next command's read then picks up that stale frame — observed in rawcli as every raw 14a
command returning "04 BB" (the truncated UID from _ensure_selected's timed-out scan) instead
of its own response.
read_response/_read_any_response now set a _resync_needed flag on timeout, and the next
send_frame drains any late/stale input before writing. Cheap — only fires after a timeout
(rare on a healthy link) and no-ops when the stream is clean. Hardware-verified: identify ->
NTAG216, READ(4) 6/6 correct, GET_VERSION correct, with the hardening in place.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Debug-heavy firmware emits DEBUG_PRINT_STRING/INTEGERS/BYTES (0x0100-0x0102) frames
asynchronously, independent of any command. pm3py's read_response returned the FIRST
frame it saw, so a debug line landing mid-command was misparsed as the reply — observed
on hardware as garbage LF sample counts (e.g. 1593903105), a config-SET that "timed
out", and desynced BigBuf downloads (reading firmware memory / the 0xDEADBEEF stack
canary). The C client routes debug frames to its logger and keeps reading for the real
reply; pm3py did not.
read_response and _read_any_response now loop past DEBUG_PRINT_* (and malformed) frames
up to a bound, returning the actual command reply — for both NG replies and the OLD-frame
bulk-download stream. This is what makes LF read/download reliable on this device.
Tests: single + multiple interleaved debug frames are skipped to reach the real reply.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A real download came back as BigBuf memory (0xDEADBEEF poison) with a PM3 response
magic (0x62334d50) embedded — the classic signature of a desynced read: the stream
had stale bytes (a prior partial transaction, or the C client having touched the
port), and the bulk reader consumes fixed 544-byte OLD frames that carry no magic to
resync on, so once misaligned it stays misaligned and reads memory as "samples".
transport.download_bulk now flushes the input first (reset_input_buffer + drain the
asyncio StreamReader until an 80ms quiet gap), so the transfer always begins byte-
aligned. Drains no-op when disconnected. Test asserts the flush runs before the request.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Phase 1 of package refactor. Moves all source modules into pm3py/core/
with file renames (hf_14a→hf_iso14a, hf_15→hf_iso15, hf_mf→hf_mfc)
and client attribute normalization (hf.a14→hf.iso14a, hf.mf→hf.mfc).
pm3py/__init__.py re-exports from core for backward compat.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>