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:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user