From a06f4d12b877499f940b756b39937b2dc035dd9b Mon Sep 17 00:00:00 2001 From: michael Date: Tue, 14 Jul 2026 17:34:23 -0700 Subject: [PATCH] fix(lf): download only the acquired sample count, not a fixed span MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With the debug-frame fix the BigBuf download now returns clean sample frames (verified on hardware: offsets 0..N, real envelope data, then ACK). But identify downloaded a fixed 15000 bytes while the acquisition captured 12288 samples (98304 bits / 8), so the tail was BigBuf memory past the samples (heap / the 0xDEADBEEF stack canary) — which flagged as "garbage" and poisoned the demod. read_emitted now sizes the download to lf.read()'s reported count (firmware returns the size in BITS; /8 = samples at 8 bits/sample); read_config downloads the readbl acquisition span. No trailing memory -> the demod runs on clean samples. Co-Authored-By: Claude Opus 4.8 --- pm3py/lf/capture.py | 22 ++++++++++++++++++---- tests/test_lf_demod.py | 9 +++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/pm3py/lf/capture.py b/pm3py/lf/capture.py index 6994ccb..4d12941 100644 --- a/pm3py/lf/capture.py +++ b/pm3py/lf/capture.py @@ -58,24 +58,38 @@ def _download(device, count: int = _SAMPLE_COUNT) -> bytes: return b"" +def _acquired_bytes(read_result, default: int) -> int: + """How many BigBuf bytes hold valid samples, from an ``lf.read()`` result. The firmware + reports the sample size in BITS (count * bits_per_sample); at 8 bits/sample that is one byte + per sample. Downloading exactly this avoids the BigBuf memory *past* the acquired samples + (heap / the 0xDEADBEEF stack canary) that otherwise poisons the demod.""" + if isinstance(read_result, dict): + bits = read_result.get("samples", 0) + if isinstance(bits, int) and bits > 0: + return max(512, min(bits // 8, 60000)) + return default + + def read_emitted(device) -> dict | None: """Capture and decode whatever the tag is emitting in the field (no downlink) — the EM4100 ID a tag (native or T5577-emulated) streams continuously.""" try: - device.lf.read(samples=_SAMPLE_COUNT) + result = device.lf.read(samples=_SAMPLE_COUNT) except Exception: return None - return demod_samples(_download(device)) + return demod_samples(_download(device, count=_acquired_bytes(result, _SAMPLE_COUNT))) def read_config(device) -> dict | None: """Probe for a T55xx by reading config block 0 and demodulating the response. A clean, - sane config both proves the chip is a T5577 and says what it's programmed to emulate.""" + sane config both proves the chip is a T5577 and says what it's programmed to emulate. + The block-0 read (DoPartialAcquisition) captures ~12000 samples and returns no count, so we + download that fixed span — enough for the short block response, without a memory tail.""" try: device.lf.t55.readbl(0) # fills BigBuf with the block-0 response envelope except Exception: return None - return protocols.decode_t55xx_config(_download(device)) + return protocols.decode_t55xx_config(_download(device, count=12000)) def identify_lf(device, emit=None) -> dict | None: diff --git a/tests/test_lf_demod.py b/tests/test_lf_demod.py index 6aa969e..503abed 100644 --- a/tests/test_lf_demod.py +++ b/tests/test_lf_demod.py @@ -362,3 +362,12 @@ class TestIdentifyLF: def test_no_device(self): assert identify_lf(None) is None + + +class TestAcquiredBytes: + def test_bit_count_to_bytes(self): + from pm3py.lf.capture import _acquired_bytes + assert _acquired_bytes({"samples": 98304}, 15000) == 12288 # 98304 bits / 8 = 12288 samples + assert _acquired_bytes({"samples": 0}, 15000) == 15000 # no count -> fallback + assert _acquired_bytes(None, 15000) == 15000 + assert _acquired_bytes({"samples": 100}, 15000) == 512 # clamped up to a floor