fix(lf): read() acquired with the field OFF (SNIFF) — no tag was ever powered
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 <noreply@anthropic.com>
This commit is contained in:
@@ -111,23 +111,32 @@ class LFCommands:
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
async def read(self, samples: int = 12288, verbose: bool = False,
|
async def _acquire(self, cmd: int, samples: int, verbose: bool = False) -> dict:
|
||||||
on_progress: ProgressCallback = None) -> dict:
|
"""Capture LF ADC samples into BigBuf. Shared by read (reader field ON) and sniff
|
||||||
"""Read LF ADC samples into device buffer."""
|
(field OFF). Payload is lf_sample_payload_t — a PACKED bitfield: samples:30, realtime:1,
|
||||||
val = (samples & 0x3FFFFFFF)
|
verbose:1, cotag:1 (5 bytes)."""
|
||||||
|
word = (samples & 0x3FFFFFFF)
|
||||||
if verbose:
|
if verbose:
|
||||||
val |= (1 << 31)
|
word |= (1 << 31) # bit 31 = verbose; bit 30 = realtime (0)
|
||||||
payload = struct.pack("<I", val)
|
payload = struct.pack("<IB", word, 0) # trailing byte carries cotag (0)
|
||||||
resp = await self._t.send_ng(Cmd.LF_SNIFF_RAW_ADC, payload, timeout=10.0)
|
resp = await self._t.send_ng(cmd, payload, timeout=10.0)
|
||||||
return {
|
return {
|
||||||
"status": resp.status,
|
"status": resp.status,
|
||||||
"samples": struct.unpack_from("<I", resp.data, 0)[0] if len(resp.data) >= 4 else 0,
|
"samples": struct.unpack_from("<I", resp.data, 0)[0] if len(resp.data) >= 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,
|
async def sniff(self, samples: int = 12288,
|
||||||
on_progress: ProgressCallback = None) -> dict:
|
on_progress: ProgressCallback = None) -> dict:
|
||||||
"""Sniff LF signal."""
|
"""Sniff LF: reader field OFF, capture an external reader<->tag exchange
|
||||||
return await self.read(samples=samples, on_progress=on_progress)
|
(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"",
|
async def sim(self, gap: int = 0, data: bytes = b"",
|
||||||
on_progress: ProgressCallback = None) -> dict:
|
on_progress: ProgressCallback = None) -> dict:
|
||||||
|
|||||||
@@ -76,3 +76,27 @@ def test_lf_t55_config_client_side():
|
|||||||
assert len(result["modes"]) == 4
|
assert len(result["modes"]) == 4
|
||||||
# No firmware round-trip: SET_CONFIG is write-only and never replies
|
# No firmware round-trip: SET_CONFIG is write-only and never replies
|
||||||
t.send_ng.assert_not_called()
|
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("<I", 98304))
|
||||||
|
result = asyncio.get_event_loop().run_until_complete(lf.read(samples=12288))
|
||||||
|
assert result["samples"] == 98304
|
||||||
|
args, kwargs = t.send_ng.call_args
|
||||||
|
assert args[0] == Cmd.LF_ACQ_RAW_ADC
|
||||||
|
payload = args[1] if len(args) > 1 else kwargs["payload"]
|
||||||
|
assert len(payload) == 5 # lf_sample_payload_t: 30+1+1 bits + cotag byte
|
||||||
|
word = struct.unpack_from("<I", payload, 0)[0]
|
||||||
|
assert (word & 0x3FFFFFFF) == 12288 # samples field
|
||||||
|
assert (word >> 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("<I", 100))
|
||||||
|
asyncio.get_event_loop().run_until_complete(lf.sniff(samples=4096))
|
||||||
|
assert t.send_ng.call_args[0][0] == Cmd.LF_SNIFF_RAW_ADC
|
||||||
|
|||||||
Reference in New Issue
Block a user