diff --git a/pm3py/lf/capture.py b/pm3py/lf/capture.py index 566905a..a160de0 100644 --- a/pm3py/lf/capture.py +++ b/pm3py/lf/capture.py @@ -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 diff --git a/tests/test_lf_demod.py b/tests/test_lf_demod.py index a0d7729..b301f96 100644 --- a/tests/test_lf_demod.py +++ b/tests/test_lf_demod.py @@ -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):