From e82b94a490a23052d4ddc7c5e1053c4e71fbec20 Mon Sep 17 00:00:00 2001 From: michael Date: Tue, 14 Jul 2026 20:58:30 -0700 Subject: [PATCH] fix(lf): identify_lf fails clearly on a raw async device (not "coroutine has no len") MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- pm3py/lf/capture.py | 11 +++++++++++ tests/test_lf_demod.py | 14 ++++++++++++++ 2 files changed, 25 insertions(+) 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):