fix(lf): detect_clock keys off the shortest well-supported interval, not the min

detect_clock estimated the bit clock from the *minimum* edge interval, so a few
stray sub-bit edges (which every real ADC capture carries) collapsed the estimate.
On a strongly-coupled, rail-clipping EM4100 the true half-bit of 32 dropped to ~3,
snapping the data clock to 8 and turning the ASK demod into all phase-error markers
— no config, no credential decoded.

Base the symbol on the shortest interval with real support (>=20% of the modal
count) instead of the raw minimum. Verified: clean synthetic still reads 32; the
hardware capture that broke it now clocks correctly. Live on a T5577 emulating
EM4100 00FFFFFFFF: DETECT decodes config 00148040, identify returns the credential.

Regression test reproduces the exact failure (sparse stray edges): old estimator
3.67 (dead demod), new 31.99.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
michael
2026-07-16 17:59:14 -07:00
parent 8f136e7dc4
commit 8833753c45
2 changed files with 28 additions and 6 deletions

View File

@@ -88,6 +88,19 @@ class TestDSP:
half = dsp.detect_clock(dsp.binarize(env))
assert half is not None and abs(half - 32) < 4 # RF/64 -> 32-sample half-bit
def test_clock_survives_sparse_stray_edges(self):
# hardware regression: a strongly-coupled EM4100 gives a clean RF/64 signal (intervals
# dominated by 32/64) with a few sub-bit stray edges from ADC ripple. The old min-based
# estimator keyed off the bare shortest interval and collapsed to ~3 (-> data clock 8 ->
# dead demod). The dominant interval is still 32, so detect_clock must ride through it.
env = bytearray(_em4100_env(0x0102030405, rf=64, high=255, low=0))
for i in range(200, len(env), 900): # sparse single-sample spikes
env[i] = 0 if env[i] > 128 else 255
half = dsp.detect_clock(dsp.binarize(bytes(env)))
assert half is not None and abs(half - 32) < 4 # not dragged down to the ~3 outlier
r = protocols.decode_em4100(bytes(env))
assert r and r["id_hex"] == "0102030405" # end-to-end decode survives
def test_manchester_roundtrip(self):
# (1,0)->1, (0,1)->0 at phase 0
assert dsp.manchester_decode([1, 0, 0, 1, 1, 0]) == [1, 0, 1]