feat(lf): AWID FSK decode (parity-validated Wiegand)
protocols.decode_awid: FSK2a (no Manchester) via the ported detectAWID path, then removeParity — the 88 bits after the preamble are 22 groups of 3 data + 1 odd-parity bit, so stripping parity gives 66 bits whose first byte is the format length (26/34/36/37/50) and the rest the facility/card fields (cmdlfawid.c index map). The 22 parity checks stand in for AWID's missing CRC, so it can't false-positive. Added to the identify decoder chain. Tests: AWID-26 round-trip across fc/card, parity gate rejects noise/EM4100. Full suite green. LF stack now covers EM4100, HID, AWID, FDX-B + T55xx config. Indala (PSK) queued — the one path needing real PSK phase-tracking (DetectPSKClock/pskRawDemod), best tuned against a hardware capture. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,7 @@ _SAMPLE_COUNT = 15000
|
||||
_DECODERS = (
|
||||
protocols.decode_em4100,
|
||||
protocols.decode_hid,
|
||||
protocols.decode_awid,
|
||||
protocols.decode_fdxb,
|
||||
)
|
||||
|
||||
|
||||
@@ -103,6 +103,58 @@ def decode_hid(samples) -> dict | None:
|
||||
return None
|
||||
|
||||
|
||||
# AWID format length -> (facility offset/width, card offset/width) in the 66 de-parity'd bits
|
||||
# (cmdlfawid.c index map). fmtLen is bits[0:8]; bit 8 is a Wiegand parity.
|
||||
_AWID_FORMATS = {
|
||||
26: ((9, 8), (17, 16)),
|
||||
34: ((9, 8), (17, 24)),
|
||||
36: ((14, 11), (25, 18)),
|
||||
37: ((9, 13), (22, 18)),
|
||||
50: ((9, 16), (25, 32)),
|
||||
}
|
||||
|
||||
|
||||
def _strip_parity(bits, group: int = 4, want_odd: bool = True):
|
||||
"""Port of ``removeParity`` for AWID: each ``group`` bits are (group-1) data + 1 parity;
|
||||
verify the parity and keep the data. Returns the compacted bits, or ``None`` on a parity
|
||||
fault (the check that makes AWID trustworthy without a CRC)."""
|
||||
out = []
|
||||
for i in range(0, len(bits) - group + 1, group):
|
||||
g = bits[i:i + group]
|
||||
if (sum(g) & 1) != (1 if want_odd else 0):
|
||||
return None
|
||||
out.extend(g[:group - 1])
|
||||
return out
|
||||
|
||||
|
||||
def decode_awid(samples) -> dict | None:
|
||||
"""Demodulate an AWID envelope (FSK2a, no Manchester) to its Wiegand credential.
|
||||
|
||||
``detectAWID`` gives the 96 frame bits; the 88 after the preamble are 22 groups of 3 data +
|
||||
1 odd-parity bit, so stripping parity yields 66 bits whose first byte is the format length
|
||||
(26/34/36/37/50). The 22 parity checks stand in for AWID's missing CRC — a bad frame fails
|
||||
them — so this doesn't false-positive."""
|
||||
frame = fsk.awid_demod_fsk(samples)
|
||||
if frame is None:
|
||||
return None
|
||||
data = _strip_parity(frame[8:8 + 88], group=4, want_odd=True)
|
||||
if data is None or len(data) != 66:
|
||||
return None
|
||||
fmt_len = dsp.bits_to_int(data[0:8])
|
||||
spec = _AWID_FORMATS.get(fmt_len)
|
||||
if spec is None:
|
||||
return None
|
||||
(fc_off, fc_w), (cn_off, cn_w) = spec
|
||||
fc = dsp.bits_to_int(data[fc_off:fc_off + fc_w])
|
||||
cardnum = dsp.bits_to_int(data[cn_off:cn_off + cn_w])
|
||||
return {
|
||||
"protocol": "AWID",
|
||||
"format": f"AWID-{fmt_len}",
|
||||
"facility_code": fc,
|
||||
"card_number": cardnum,
|
||||
}
|
||||
|
||||
|
||||
def _reflect(v: int, width: int) -> int:
|
||||
r = 0
|
||||
for i in range(width):
|
||||
|
||||
Reference in New Issue
Block a user