From b536294a7a9e1b137d9582e6ac957ba9fa7af959 Mon Sep 17 00:00:00 2001 From: michael Date: Tue, 14 Jul 2026 16:17:11 -0700 Subject: [PATCH] =?UTF-8?q?fix(lf):=20read()=20acquired=20with=20the=20fie?= =?UTF-8?q?ld=20OFF=20(SNIFF)=20=E2=80=94=20no=20tag=20was=20ever=20powere?= =?UTF-8?q?d?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit lf.read() sent CMD_LF_SNIFF_RAW_ADC (passive sniff, reader field OFF), so it captured only a dead field — a real hardware dump came back flat at 0x7e-0x80 (±1 count of ADC noise) with no tag modulation, which is why LF identify found nothing. Reader-mode reads must use CMD_LF_ACQ_RAW_ADC (SampleLF -> field ON) to power the tag and capture its load modulation. - read() -> CMD_LF_ACQ_RAW_ADC (field on); sniff() -> CMD_LF_SNIFF_RAW_ADC (field off), no longer delegating to read(). Shared _acquire() packs the real lf_sample_payload_t (PACKED bitfield samples:30/realtime:1/verbose:1 + cotag byte = 5 bytes) instead of a bare uint32. Tests: read() uses ACQ with a 5-byte payload and correct samples field; sniff() uses SNIFF. Co-Authored-By: Claude Opus 4.8 --- pm3py/core/lf.py | 27 ++++++++++++++++++--------- tests/test_lf.py | 24 ++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/pm3py/core/lf.py b/pm3py/core/lf.py index 6fd25c6..2ba289f 100644 --- a/pm3py/core/lf.py +++ b/pm3py/core/lf.py @@ -111,23 +111,32 @@ class LFCommands: return result - async def read(self, samples: int = 12288, verbose: bool = False, - on_progress: ProgressCallback = None) -> dict: - """Read LF ADC samples into device buffer.""" - val = (samples & 0x3FFFFFFF) + async def _acquire(self, cmd: int, samples: int, verbose: bool = False) -> dict: + """Capture LF ADC samples into BigBuf. Shared by read (reader field ON) and sniff + (field OFF). Payload is lf_sample_payload_t — a PACKED bitfield: samples:30, realtime:1, + verbose:1, cotag:1 (5 bytes).""" + word = (samples & 0x3FFFFFFF) if verbose: - val |= (1 << 31) - payload = struct.pack("= 4 else 0, } + async def read(self, samples: int = 12288, verbose: bool = False, + on_progress: ProgressCallback = None) -> dict: + """Reader-mode LF acquisition: turn the reader field ON and sample the tag's load + modulation (this is ``lf read`` — CMD_LF_ACQ_RAW_ADC). Use this before download_samples + to read an EM4100/T5577/HID. (SNIFF, field-off, captures nothing from a tag.)""" + return await self._acquire(Cmd.LF_ACQ_RAW_ADC, samples, verbose) + async def sniff(self, samples: int = 12288, on_progress: ProgressCallback = None) -> dict: - """Sniff LF signal.""" - return await self.read(samples=samples, on_progress=on_progress) + """Sniff LF: reader field OFF, capture an external reader<->tag exchange + (CMD_LF_SNIFF_RAW_ADC).""" + return await self._acquire(Cmd.LF_SNIFF_RAW_ADC, samples) async def sim(self, gap: int = 0, data: bytes = b"", on_progress: ProgressCallback = None) -> dict: diff --git a/tests/test_lf.py b/tests/test_lf.py index bddd9ae..1687f01 100644 --- a/tests/test_lf.py +++ b/tests/test_lf.py @@ -76,3 +76,27 @@ def test_lf_t55_config_client_side(): assert len(result["modes"]) == 4 # No firmware round-trip: SET_CONFIG is write-only and never replies t.send_ng.assert_not_called() + + +def test_lf_read_uses_reader_field_acq(): + # read() must turn the reader field ON (LF_ACQ_RAW_ADC) — SNIFF is field-off and reads nothing + t = AsyncMock() + lf = LFCommands(t) + t.send_ng.return_value = make_response(Cmd.LF_ACQ_RAW_ADC, 0, struct.pack(" 1 else kwargs["payload"] + assert len(payload) == 5 # lf_sample_payload_t: 30+1+1 bits + cotag byte + word = struct.unpack_from("> 30) & 1 == 0 # realtime = 0 + + +def test_lf_sniff_uses_field_off(): + t = AsyncMock() + lf = LFCommands(t) + t.send_ng.return_value = make_response(Cmd.LF_SNIFF_RAW_ADC, 0, struct.pack("