feat(lf): FDX-B (ISO 11784/85) biphase demod + CRC-validated framing

- dsp.biphase_raw_decode: faithful port of lfdemod.c BiphaseRawDecode (pairwise
  differential-Manchester decode with phase-fault offset nudge + error markers).
- protocols.decode_fdxb: ASK + biphase, find the 00000000001 preamble, de-interleave
  the 8-bit data groups (every 9th bit a control 1), reassemble the 10-bit country +
  38-bit national code. Accepted only when the CRC-16 over the eight data bytes matches
  the stored one, so it can't false-positive. Both biphase polarities tried.
- protocols.crc16 / crc16_fdxb: parametric bitwise CRC-16 (Rocksoft model) matching the
  firmware crc16_fast; FDX-B is poly 0x1021, init 0, refin=false, refout=true.
  Added to the identify decoder chain (FDX-B is common in implants).

Tests: FDX-B round-trip (country/national/animal), CRC gate rejects noise/EM4100, and
a CRC known-answer test pinning the impl to CRC-16/XMODEM (0x31C3) and KERMIT (0x2189).
Full suite green.

Indala (PSK) + AWID queued.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
michael
2026-07-14 15:55:45 -07:00
parent e4a3aec46f
commit 7e8d924045
4 changed files with 215 additions and 10 deletions

View File

@@ -164,6 +164,91 @@ class TestHID:
assert r["protocol"] == "HID Prox" and r["card_number"] == 9999
# --- FDX-B (biphase) --------------------------------------------------------------------------
def _fdxb_bits(country, national, animal=0):
bits = [1] * 128
for i in range(10):
bits[i] = 0
bits[10] = 1
def put(val, n, off):
for k in range(n):
bits[off + k] = (val >> k) & 1 # LSB-first, like num_to_bytebitsLSBF
put(national & 0xFF, 8, 11)
put((national >> 8) & 0xFF, 8, 20)
put((national >> 16) & 0xFF, 8, 29)
put((national >> 24) & 0xFF, 8, 38)
put((national >> 32) & 0x3F, 6, 47)
put(country & 0x3, 2, 53)
put((country >> 2) & 0xFF, 8, 56)
bits[65] = 0
put(0, 7, 66)
put(0, 7, 74)
bits[81] = animal
raw = bytes(sum(bits[11 + i * 9 + k] << k for k in range(8)) for i in range(8))
crc = protocols.crc16_fdxb(raw)
put(crc & 0xFF, 8, 83)
put((crc >> 8) & 0xFF, 8, 92)
return bits
def _biphase_env(databits, rf=32, high=200, low=60, repeats=3):
"""Differential-biphase ASK envelope: toggle level at each bit boundary, and for data 1 add
a mid-bit toggle (so the two half-bits differ) — the inverse of biphase_raw_decode."""
half = []
level = 0
for _ in range(repeats):
for d in databits:
level ^= 1
h1 = level
if d:
level ^= 1
half.append(h1)
half.append(level)
sph = rf // 2
env = []
for hb in half:
env += [high if hb else low] * sph
return bytes(env)
def _fdxb_env(country, national, animal=0, **kw):
return _biphase_env(_fdxb_bits(country, national, animal), **kw)
class TestCRC16:
def test_known_vectors(self):
# "123456789" -> CRC-16/XMODEM 0x31C3, CRC-16/KERMIT 0x2189 (anchors the CRC impl)
d = b"123456789"
assert protocols.crc16(d, 0, 0x1021, refin=False, refout=False) == 0x31C3
assert protocols.crc16(d, 0, 0x1021, refin=True, refout=True) == 0x2189
class TestFDXB:
def test_roundtrip(self):
r = protocols.decode_fdxb(_fdxb_env(999, 0x3FA1B2C3D))
assert r and r["protocol"] == "FDX-B"
assert r["country_code"] == 999 and r["national_code"] == 0x3FA1B2C3D
def test_various(self):
for country, national in ((1, 1), (840, 123456789), (0x3FF, 0x3FFFFFFFFF)):
r = protocols.decode_fdxb(_fdxb_env(country, national))
assert r and r["country_code"] == country and r["national_code"] == national
def test_animal_flag(self):
assert protocols.decode_fdxb(_fdxb_env(124, 555, animal=1))["animal"] is True
def test_crc_gate_rejects_noise(self):
assert protocols.decode_fdxb(bytes([128] * 6000)) is None
assert protocols.decode_fdxb(_em4100_env(0x0102030405)) is None
def test_demod_samples_picks_fdxb(self):
r = demod_samples(_fdxb_env(56, 77))
assert r["protocol"] == "FDX-B" and r["country_code"] == 56
# --- T55xx config -----------------------------------------------------------------------------
class TestT55xxConfig: