fix(lf): identify_lf fails clearly on a raw async device (not "coroutine has no len")

capture drives the device synchronously (as Proxmark3.sync()'s proxy exposes it). Passing a
bare async Proxmark3() — whose methods return un-awaited coroutines, and which isn't even
connected — blew up deep in the demod with "object of type 'coroutine' has no len()". Now
identify_lf detects a coroutine-function device up front and raises an actionable TypeError
pointing at Proxmark3.sync().

The mock-based tests returned plain values, so they never exercised the async method shape;
added a test using a real coroutine function that reproduces and guards it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
michael
2026-07-14 20:58:30 -07:00
parent e57e6ed268
commit e82b94a490
2 changed files with 25 additions and 0 deletions

View File

@@ -7,6 +7,8 @@ combines the two into "is it a T5577, and what is it (emulating)?".
"""
from __future__ import annotations
import inspect
from . import protocols
# how many envelope samples to pull from BigBuf after a capture (a T55xx/EM frame is a few
@@ -120,6 +122,15 @@ def identify_lf(device, emit=None) -> dict | None:
if device is None:
return None
# capture drives the device *synchronously* (as Proxmark3.sync()'s proxy exposes it). A raw
# async Proxmark3() returns un-awaited coroutines here — fail with an actionable message
# instead of a cryptic "coroutine has no len()" deep in the demod.
if inspect.iscoroutinefunction(getattr(getattr(device, "lf", None), "read", None)):
raise TypeError(
"identify_lf needs a synchronous device — use Proxmark3.sync(), not Proxmark3(). "
"(A bare Proxmark3() is the async API and isn't connected.)"
)
# Force the device debug level to NONE for the duration: on debug-heavy firmware a raised
# g_dbglevel makes LF acquisition emit Dbprintf frames over USB, which interleave with the
# sample download and desync it. This is the device-side knob (not the C client's g_debugMode

View File

@@ -363,6 +363,20 @@ class TestIdentifyLF:
def test_no_device(self):
assert identify_lf(None) is None
def test_rejects_async_device(self):
# a raw async Proxmark3() exposes coroutine-function methods — must fail with a clear
# message pointing at Proxmark3.sync(), not blow up deep in the demod on an un-awaited
# coroutine (the MagicMock tests couldn't catch this since mocks return plain values)
dev = MagicMock()
async def _aread(*a, **k):
return {}
dev.lf.read = _aread
try:
identify_lf(dev)
assert False, "expected TypeError for an async device"
except TypeError as e:
assert "sync" in str(e).lower()
class TestCaptureValidation:
def test_looks_like_samples_rejects_memory(self):