feat(lf): validate + retry LF capture against a flaky NG link
On this device the NG link is intermittent — a read/download that returns clean samples one minute comes back as desynced BigBuf memory the next, and lf.read()'s reported count is sometimes garbage (so sizing the download off it over-reads into memory). Rather than trust either, capture now: - downloads a fixed safe span (_DOWNLOAD_BYTES) instead of the flaky read-count, - validates the result is an envelope, not memory (rejects the 0xDEADBEEF stack canary, an embedded PM3 response magic, or a mostly-zero unwritten buffer), - retries the read+download a few times, giving up cleanly to b'' if the device only returns memory (needs a power-cycle / lower debug level). read_emitted/read_config go through this, so identify recovers from transient desyncs instead of demodulating garbage. Tests: validator rejects memory patterns; capture retries past a garbage download to the good one, and gives up when all attempts are garbage. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -50,7 +50,34 @@ def demod_samples(samples) -> dict | None:
|
||||
return None
|
||||
|
||||
|
||||
def _download(device, count: int = _SAMPLE_COUNT) -> bytes:
|
||||
# download a fixed, safe span rather than trusting lf.read()'s reported count — on a flaky NG
|
||||
# link that count is sometimes garbage, and over-reading spills into BigBuf memory past the
|
||||
# samples. 12000 is under a normal acquisition, enough frames for any LF frame to repeat.
|
||||
_DOWNLOAD_BYTES = 12000
|
||||
|
||||
# markers that a "sample" buffer is actually desynced BigBuf memory, not an envelope
|
||||
_POISON = b"\xef\xbe\xad\xde" # 0xDEADBEEF — firmware stack canary
|
||||
_RESP_MAGIC = b"\x50\x4d\x33\x62" # 0x62334d50 — a PM3 response frame sitting in the buffer
|
||||
|
||||
|
||||
def _try(fn, default):
|
||||
try:
|
||||
return fn()
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
|
||||
def _looks_like_samples(data: bytes) -> bool:
|
||||
"""Reject a download that is clearly desynced BigBuf memory rather than an LF envelope: the
|
||||
DEADBEEF stack canary, an embedded PM3 response magic, or a mostly-zero (unwritten) buffer."""
|
||||
if len(data) < 256:
|
||||
return False
|
||||
if _POISON in data or _RESP_MAGIC in data:
|
||||
return False
|
||||
return data.count(0) < len(data) // 2
|
||||
|
||||
|
||||
def _download(device, count: int = _DOWNLOAD_BYTES) -> bytes:
|
||||
"""Pull the last capture's envelope out of the device BigBuf (best-effort -> b'')."""
|
||||
try:
|
||||
return device.lf.download_samples(count=count) or b""
|
||||
@@ -58,38 +85,31 @@ 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 _capture(device, do_read, count: int = _DOWNLOAD_BYTES, tries: int = 3) -> bytes:
|
||||
"""Read + download a clean LF envelope, retrying past the intermittent desyncs a flaky NG
|
||||
link produces. Returns validated sample bytes, or ``b''`` if every attempt came back as
|
||||
memory (device likely needs a power-cycle / lower debug level)."""
|
||||
for _ in range(tries):
|
||||
if do_read() is None:
|
||||
continue
|
||||
data = _download(device, count=count)
|
||||
if _looks_like_samples(data):
|
||||
return data
|
||||
return b""
|
||||
|
||||
|
||||
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:
|
||||
result = device.lf.read(samples=_SAMPLE_COUNT)
|
||||
except Exception:
|
||||
return None
|
||||
return demod_samples(_download(device, count=_acquired_bytes(result, _SAMPLE_COUNT)))
|
||||
data = _capture(device, lambda: _try(lambda: device.lf.read(samples=15000), None))
|
||||
return demod_samples(data) if data else None
|
||||
|
||||
|
||||
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.
|
||||
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, count=12000))
|
||||
sane config both proves the chip is a T5577 and says what it's programmed to emulate."""
|
||||
data = _capture(device, lambda: _try(lambda: device.lf.t55.readbl(0), None))
|
||||
return protocols.decode_t55xx_config(data) if data else None
|
||||
|
||||
|
||||
def identify_lf(device, emit=None) -> dict | None:
|
||||
|
||||
Reference in New Issue
Block a user