mirror of
https://github.com/RfidResearchGroup/ChameleonUltra.git
synced 2026-10-23 08:32:54 +00:00
Add PAC/Stanley LF tag reading support
Implements NRZ/Direct modulation decoder for PAC/Stanley 125kHz cards using SAADC ADC sampling with spike-aware threshold calibration. The LC antenna produces brief high-amplitude transients at NRZ transitions which are clipped before the moving-average filter to isolate the actual data levels.
This commit is contained in:
1 parent
e5d615d512
commit
c494a2cc81
13 files changed
+521
-1
No files matched your search
@@ -757,6 +757,7 @@ lf_em_410x = lf_em.subgroup("410x", "EM410x commands")
|
||||
lf_hid = lf.subgroup("hid", "HID commands")
|
||||
lf_hid_prox = lf_hid.subgroup("prox", "HID Prox commands")
|
||||
lf_ioprox = lf.subgroup("ioprox", "ioProx commands")
|
||||
lf_pac = lf.subgroup("pac", "PAC/Stanley commands")
|
||||
lf_viking = lf.subgroup("viking", "Viking commands")
|
||||
lf_generic = lf.subgroup("generic", "Generic commands")
|
||||
|
||||
@@ -5763,6 +5764,110 @@ class LFIOProxEconfig(SlotIndexArgsAndGoUnit, LFIOProxIdArgsUnit):
|
||||
print(f" ID: {color_string((CY, cn))}")
|
||||
print(f" Raw: {color_string((CY, raw8.hex().upper()))}")
|
||||
|
||||
@lf_pac.command('read')
|
||||
class LFPacRead(ReaderRequiredUnit):
|
||||
def args_parser(self) -> ArgumentParserNoExit:
|
||||
parser = ArgumentParserNoExit()
|
||||
parser.description = 'Scan PAC/Stanley tag and print card ID'
|
||||
return parser
|
||||
|
||||
def on_exec(self, args: argparse.Namespace):
|
||||
card_id = self.cmd.pac_scan()
|
||||
card_id_ascii = ''.join(chr(b) if 0x20 <= b < 0x7f else '.' for b in card_id)
|
||||
print(f" PAC/Stanley Card ID: {color_string((CG, card_id_ascii))} ({card_id.hex().upper()})")
|
||||
|
||||
|
||||
@lf_pac.command('debug')
|
||||
class LFPacDebug(ReaderRequiredUnit):
|
||||
def args_parser(self) -> ArgumentParserNoExit:
|
||||
parser = ArgumentParserNoExit()
|
||||
parser.description = 'Capture raw ADC data and analyze NRZ signal for PAC debugging'
|
||||
return parser
|
||||
|
||||
def on_exec(self, args: argparse.Namespace):
|
||||
resp = self.cmd.adc_generic_read()
|
||||
if resp is None:
|
||||
print("ADC read failed")
|
||||
return
|
||||
|
||||
data = list(resp)
|
||||
min_val = min(data)
|
||||
max_val = max(data)
|
||||
|
||||
print(f" Samples: {len(data)}")
|
||||
print(f" ADC range: {min_val} - {max_val} (spread: {max_val - min_val})")
|
||||
|
||||
# Spike removal: clip values above 3x the minimum (LC ringing transients)
|
||||
spike_cap = min_val * 3 if min_val > 0 else max_val
|
||||
clipped = [min(v, spike_cap) for v in data]
|
||||
c_min = min(clipped)
|
||||
c_max = max(clipped)
|
||||
c_thresh = (c_min + c_max) / 2
|
||||
|
||||
print(f" Spike cap: {spike_cap} (3x raw min {min_val})")
|
||||
print(f" Clipped range: {c_min} - {c_max} (spread: {c_max - c_min})")
|
||||
|
||||
# Moving-average of clipped signal (32-sample window = 1 NRZ bit)
|
||||
window = 32
|
||||
if len(clipped) >= window:
|
||||
filtered = []
|
||||
s = sum(clipped[:window])
|
||||
for i in range(window, len(clipped)):
|
||||
filtered.append(s / window)
|
||||
s += clipped[i] - clipped[i - window]
|
||||
filtered.append(s / window)
|
||||
f_min = min(filtered)
|
||||
f_max = max(filtered)
|
||||
f_thresh = (f_min + f_max) / 2
|
||||
f_hyst = (f_max - f_min) / 4
|
||||
print(f"\n Spike-free moving average (window={window}):")
|
||||
print(f" Range: {f_min:.1f} - {f_max:.1f} (spread: {f_max - f_min:.1f})")
|
||||
print(f" Threshold: {f_thresh:.1f}, Hysteresis: {f_hyst:.1f}")
|
||||
|
||||
# Binary decode with hysteresis
|
||||
f_binary = []
|
||||
state = filtered[0] > f_thresh
|
||||
for v in filtered:
|
||||
if v > f_thresh + f_hyst:
|
||||
state = True
|
||||
elif v < f_thresh - f_hyst:
|
||||
state = False
|
||||
f_binary.append(1 if state else 0)
|
||||
|
||||
f_trans = sum(1 for i in range(1, len(f_binary)) if f_binary[i] != f_binary[i - 1])
|
||||
f_runs = []
|
||||
cur = f_binary[0]
|
||||
cnt = 1
|
||||
for i in range(1, len(f_binary)):
|
||||
if f_binary[i] == cur:
|
||||
cnt += 1
|
||||
else:
|
||||
f_runs.append(cnt)
|
||||
cur = f_binary[i]
|
||||
cnt = 1
|
||||
f_runs.append(cnt)
|
||||
print(f" Transitions: {f_trans}")
|
||||
print(f" Run lengths: {f_runs[:40]}{'...' if len(f_runs) > 40 else ''}")
|
||||
run_bits = [f"{r / 32:.1f}" for r in f_runs[:40]]
|
||||
print(f" Run bits: {run_bits}")
|
||||
|
||||
# Show bit-period averages (each = avg of 32 clipped samples)
|
||||
print(f"\n Bit-period averages (32 samples each):")
|
||||
bit_avgs = []
|
||||
for i in range(0, len(clipped) - window + 1, window):
|
||||
chunk = clipped[i:i + window]
|
||||
bit_avgs.append(sum(chunk) / len(chunk))
|
||||
bit_str = " ".join(f"{a:.0f}" for a in bit_avgs)
|
||||
print(f" {bit_str}")
|
||||
|
||||
# Raw hex dump (first 512 bytes)
|
||||
print(f"\n Raw ADC (8-bit, min={min_val} max={max_val}):")
|
||||
for i in range(0, min(len(data), 512), 50):
|
||||
chunk = data[i:i + 50]
|
||||
hexpart = " ".join(f"{b:02x}" for b in chunk)
|
||||
print(f" {i:04x} {hexpart}")
|
||||
|
||||
|
||||
@lf_viking.command("read")
|
||||
class LFVikingRead(ReaderRequiredUnit):
|
||||
def args_parser(self) -> ArgumentParserNoExit:
|
||||
|
||||
@@ -580,6 +580,18 @@ class ChameleonCMD:
|
||||
data = struct.pack(f'!4s4s{4*len(old_keys)}s', id_bytes, new_key, b''.join(old_keys))
|
||||
return self.device.send_cmd_sync(Command.VIKING_WRITE_TO_T55XX, data)
|
||||
|
||||
@expect_response(Status.LF_TAG_OK)
|
||||
def pac_scan(self):
|
||||
"""
|
||||
Read the card ID of PAC/Stanley.
|
||||
|
||||
:return:
|
||||
"""
|
||||
resp = self.device.send_cmd_sync(Command.PAC_SCAN)
|
||||
if resp.status == Status.LF_TAG_OK:
|
||||
resp.parsed = resp.data[:8]
|
||||
return resp
|
||||
|
||||
@expect_response(Status.LF_TAG_OK)
|
||||
def adc_generic_read(self):
|
||||
"""
|
||||
|
||||
@@ -83,6 +83,7 @@ class Command(enum.IntEnum):
|
||||
HIDPROX_WRITE_TO_T55XX = 3003
|
||||
VIKING_SCAN = 3004
|
||||
VIKING_WRITE_TO_T55XX = 3005
|
||||
PAC_SCAN = 3010
|
||||
ADC_GENERIC_READ = 3009
|
||||
IOPROX_SCAN = 3010
|
||||
IOPROX_WRITE_TO_T55XX = 3011
|
||||
@@ -276,6 +277,7 @@ class TagSpecificType(enum.IntEnum):
|
||||
# securakey
|
||||
# gallagher
|
||||
# PAC/Stanley
|
||||
PAC = 150
|
||||
# Presco
|
||||
# Visa2000
|
||||
Viking = 170
|
||||
@@ -369,6 +371,8 @@ class TagSpecificType(enum.IntEnum):
|
||||
return "HIDProx"
|
||||
elif self == TagSpecificType.ioProx:
|
||||
return "ioProx"
|
||||
elif self == TagSpecificType.PAC:
|
||||
return "PAC/Stanley"
|
||||
elif self == TagSpecificType.Viking:
|
||||
return "Viking"
|
||||
elif self == TagSpecificType.MIFARE_Mini:
|
||||
|
||||
Reference in new issue
Block a user