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:
@@ -55,23 +55,32 @@ def _edges(binary: list[int]) -> list[int]:
|
||||
return [i for i in range(1, len(binary)) if binary[i] != binary[i - 1]]
|
||||
|
||||
|
||||
def detect_clock(binary: list[int], min_run: int = 3) -> float | None:
|
||||
def detect_clock(binary: list[int], min_run: int = 3, support: float = 0.2) -> float | None:
|
||||
"""Estimate the shortest symbol width (samples) from the edge-interval distribution.
|
||||
|
||||
For Manchester this is the *half-bit* (there is always a mid-bit transition, so runs are
|
||||
one or two half-bits); for plain OOK it is the bit itself. Runs shorter than ``min_run``
|
||||
are treated as noise/jitter and ignored. Returns the mean of the shortest interval cluster,
|
||||
or ``None`` when there aren't enough clean edges to decide."""
|
||||
or ``None`` when there aren't enough clean edges to decide.
|
||||
|
||||
The base symbol is the shortest interval with *real support* — a count at least ``support``×
|
||||
the most-common interval's count — not the raw minimum. A real ADC capture always carries a
|
||||
few stray sub-bit edges; keying off the bare minimum let a single 3-sample outlier collapse a
|
||||
clean RF/64 tag's estimate from 32 to ~3 (hardware-observed on a strongly-coupled EM4100),
|
||||
snapping the data clock to 8 and killing the demod. The dominant interval is robust to that."""
|
||||
edges = _edges(binary)
|
||||
if len(edges) < 4:
|
||||
return None
|
||||
intervals = [edges[i] - edges[i - 1] for i in range(1, len(edges)) if edges[i] - edges[i - 1] >= min_run]
|
||||
if len(intervals) < 3:
|
||||
return None
|
||||
intervals.sort()
|
||||
base = intervals[0]
|
||||
# average the cluster within 1.5x of the shortest interval -> the fundamental symbol width
|
||||
cluster = [d for d in intervals if d <= base * 1.5]
|
||||
counts: dict[int, int] = {}
|
||||
for d in intervals:
|
||||
counts[d] = counts.get(d, 0) + 1
|
||||
floor = max(counts.values()) * support
|
||||
base = min(d for d, n in counts.items() if n >= floor) # shortest well-supported interval
|
||||
# average the cluster around the base (0.75x–1.5x) -> the fundamental symbol width
|
||||
cluster = [d for d in intervals if base * 0.75 <= d <= base * 1.5]
|
||||
return sum(cluster) / len(cluster)
|
||||
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user