PAC/Stanley CLI: replace --id with --cn/--raw (PM3 parity)

Split the single --id argument into --cn (8 ASCII chars) and --raw
(32 hex char T55XX bitstream, directly compatible with PM3 raw output).
Add Python-side PAC bitstream encoder/decoder for raw format support.
Output now shows CN and Raw labels matching PM3's format.

Add NRF_LOG module registration to pac.c for debug logging,
consistent with other protocol implementations.

Reassign PAC command IDs (3014/3015) to avoid collision with ioProx
(3010/3011) after rebase onto upstream/main.
This commit is contained in:
Kevin Yuan
2026-03-24 15:04:41 +00:00
parent 69327ded7d
commit f5d721bbfd
4 changed files with 144 additions and 38 deletions
+2 -2
View File
@@ -93,8 +93,8 @@
#define DATA_CMD_EM410X_ELECTRA_WRITE_TO_T55XX (3006)
#define DATA_CMD_HIDPROX_SCAN (3002)
#define DATA_CMD_HIDPROX_WRITE_TO_T55XX (3003)
#define DATA_CMD_PAC_SCAN (3010)
#define DATA_CMD_PAC_WRITE_TO_T55XX (3011)
#define DATA_CMD_PAC_SCAN (3014)
#define DATA_CMD_PAC_WRITE_TO_T55XX (3015)
#define DATA_CMD_VIKING_SCAN (3004)
#define DATA_CMD_VIKING_WRITE_TO_T55XX (3005)
#define DATA_CMD_ADC_GENERIC_READ (3009)
@@ -9,6 +9,12 @@
#include "t55xx.h"
#include "tag_base_type.h"
#define NRF_LOG_MODULE_NAME pac_protocol
#include "nrf_log.h"
#include "nrf_log_ctrl.h"
#include "nrf_log_default_backends.h"
NRF_LOG_MODULE_REGISTER();
#define PAC_DATA_SIZE 8 // 8-byte ASCII card ID
// NRZ at RF/32: 32 carrier cycles per bit.
+134 -34
View File
@@ -5661,12 +5661,12 @@ class LFIOProxRead(LFIOProxReadArgsUnit, ReaderRequiredUnit):
return self.add_card_arg(parser, required=False)
def on_exec(self, args: argparse.Namespace):
ver, fc, cn, raw8, *futureuse = self.cmd.ioprox_scan()
print(f"ioProx XSF format")
ver, fc, cn, raw8, *futureuse = self.cmd.ioprox_scan()
print(f"ioProx XSF format")
print(f" Version: {color_string((CG, ver))}")
print(f" Facility: {color_string((CG, f'{fc} [0x{fc:02X}]'))}")
print(f" ID: {color_string((CY, cn))}")
print(f" Raw: {color_string((CY, raw8.hex().upper()))}")
print(f" Raw: {color_string((CY, raw8.hex().upper()))}")
@lf_ioprox.command("write")
class LFIOProxWriteT55xx(LFIOProxIdArgsUnit, ReaderRequiredUnit):
@@ -5686,9 +5686,9 @@ class LFIOProxWriteT55xx(LFIOProxIdArgsUnit, ReaderRequiredUnit):
raw8 = self.parse_raw8(args.raw8)
ver, fc, cn, raw8, *futureuse = self.cmd.ioprox_decode_raw(raw8)
else:
res = self.cmd.ioprox_compose_id(args.ver, args.fc, args.cn)
res = self.cmd.ioprox_compose_id(args.ver, args.fc, args.cn)
raw8 = res[3]
payload16 = struct.pack(
">BBH8s4x",
ver & 0xFF,
@@ -5696,14 +5696,14 @@ class LFIOProxWriteT55xx(LFIOProxIdArgsUnit, ReaderRequiredUnit):
cn & 0xFFFF,
raw8
)
result = self.cmd.ioprox_write_to_t55xx(payload16)
print(f"ioProx XSF format")
print(f"ioProx XSF format")
print(f" Version: {color_string((CG, ver))}")
print(f" Facility: {color_string((CG, f'{fc} [0x{fc:02X}]'))}")
print(f" ID: {color_string((CY, cn))}")
print(f" Raw: {color_string((CY, raw8.hex().upper()))}")
print(f" Raw: {color_string((CY, raw8.hex().upper()))}")
print("Write done.")
@lf_ioprox.command("econfig")
@@ -5736,9 +5736,9 @@ class LFIOProxEconfig(SlotIndexArgsAndGoUnit, LFIOProxIdArgsUnit):
raw8 = self.parse_raw8(args.raw8)
ver, fc, cn, raw8, *futureuse = self.cmd.ioprox_decode_raw(raw8)
else:
res = self.cmd.ioprox_compose_id(args.ver, args.fc, args.cn)
res = self.cmd.ioprox_compose_id(args.ver, args.fc, args.cn)
raw8 = res[3]
payload16 = struct.pack(
">BBH8s4x",
ver & 0xFF,
@@ -5748,21 +5748,102 @@ class LFIOProxEconfig(SlotIndexArgsAndGoUnit, LFIOProxIdArgsUnit):
)
result = self.cmd.ioprox_set_emu_id(payload16)
print(f"ioProx XSF format")
print(f"ioProx XSF format")
print(f" Version: {color_string((CG, ver))}")
print(f" Facility: {color_string((CG, f'{fc} [0x{fc:02X}]'))}")
print(f" ID: {color_string((CY, cn))}")
print(f" Raw: {color_string((CY, raw8.hex().upper()))}")
print(f" Raw: {color_string((CY, raw8.hex().upper()))}")
else:
# GET
ver, fc, cn, raw8, *futureuse = self.cmd.ioprox_get_emu_id()
print(f"ioProx XSF format")
print(f"ioProx XSF format")
print(f" Version: {color_string((CG, ver))}")
print(f" Facility: {color_string((CG, f'{fc} [0x{fc:02X}]'))}")
print(f" ID: {color_string((CY, cn))}")
print(f" Raw: {color_string((CY, raw8.hex().upper()))}")
print(f" Raw: {color_string((CY, raw8.hex().upper()))}")
def pac_encode_raw(card_id: bytes) -> bytes:
"""Encode 8-byte card ID to 16-byte T55XX bitstream (128 bits).
Frame: 0xFF sync (8 bits) + 12 × 10-bit UART frames.
UART frame: start(0) + 7 data bits LSB-first + odd parity + stop(1).
Payload: STX(0x02), '2', '0', card_id[0..7], XOR checksum.
"""
payload = [0x02, 0x32, 0x30] + list(card_id)
xor_check = 0
for b in card_id:
xor_check ^= b
payload.append(xor_check)
bits = [1] * 8 # sync marker 0xFF
for byte_val in payload:
bits.append(0) # start bit
ones = 0
for i in range(7):
bit = (byte_val >> i) & 1
bits.append(bit)
ones += bit
bits.append(0 if (ones & 1) else 1) # odd parity
bits.append(1) # stop bit
raw = bytearray(16)
for i in range(128):
if bits[i]:
raw[i >> 3] |= 1 << (7 - (i & 7))
return bytes(raw)
def pac_decode_raw(raw: bytes) -> bytes:
"""Decode 16-byte T55XX bitstream to 8-byte card ID.
Validates sync, UART framing, header, and checksum.
"""
if len(raw) != 16:
raise ValueError("Raw data must be exactly 16 bytes (128 bits)")
def get_bit(pos):
return (raw[pos >> 3] >> (7 - (pos & 7))) & 1
# Sync marker
for i in range(8):
if not get_bit(i):
raise ValueError("Invalid sync marker (expected 0xFF)")
decoded = []
for f in range(12):
start = 8 + f * 10
if get_bit(start):
raise ValueError(f"Invalid start bit in UART frame {f}")
byte_val = 0
ones = 0
for i in range(7):
if get_bit(start + 1 + i):
byte_val |= 1 << i
ones += 1
if get_bit(start + 8):
ones += 1
if (ones & 1) == 0:
raise ValueError(f"Parity error in UART frame {f}")
if not get_bit(start + 9):
raise ValueError(f"Invalid stop bit in UART frame {f}")
decoded.append(byte_val)
if decoded[0] != 0x02:
raise ValueError(f"Invalid STX: 0x{decoded[0]:02X}")
if decoded[1] != 0x32 or decoded[2] != 0x30:
raise ValueError("Invalid header (expected '20')")
card_id = bytes(decoded[3:11])
xor_check = 0
for b in card_id:
xor_check ^= b
if xor_check != decoded[11]:
raise ValueError(f"Checksum error: expected 0x{xor_check:02X}, got 0x{decoded[11]:02X}")
return card_id
@lf_pac.command('read')
class LFPacRead(ReaderRequiredUnit):
@@ -5774,32 +5855,47 @@ class LFPacRead(ReaderRequiredUnit):
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()})")
raw = pac_encode_raw(card_id)
print(f" PAC/Stanley - CN: {color_string((CG, card_id_ascii))} | Raw: {raw.hex().upper()}")
class LFPacIdArgsUnit(DeviceRequiredUnit):
@staticmethod
def add_card_arg(parser: ArgumentParserNoExit, required=False):
parser.add_argument("--id", type=str, required=required,
help="PAC/Stanley card ID: 16 hex chars (e.g. 4141414141414141) or 8 ASCII chars (e.g. AAAAAAAA)",
metavar="<hex|ascii>")
group = parser.add_mutually_exclusive_group(required=required)
group.add_argument("--cn", type=str,
help="Card number (8 ASCII characters, e.g. CARD0001)",
metavar="<ascii>")
group.add_argument("--raw", type=str,
help="T55XX bitstream (32 hex chars, PM3 raw format)",
metavar="<hex>")
return parser
def before_exec(self, args: argparse.Namespace):
if not super().before_exec(args):
return False
if args.id is not None:
if re.match(r"^[a-fA-F0-9]{16}$", args.id):
pass # already hex
elif len(args.id) == 8:
# treat as 8 ASCII characters, convert to hex
args.id = args.id.encode('ascii').hex()
else:
raise ArgsParserError("ID must be 16 hex chars (8 bytes) or 8 ASCII characters")
# PAC uses 7-bit UART frames; MSB of each byte is not encoded
id_bytes = bytes.fromhex(args.id)
if any(b > 0x7F for b in id_bytes):
raise ArgsParserError("PAC card IDs are 7-bit ASCII only (each byte must be 0x00-0x7F)")
if args.cn is not None:
if len(args.cn) != 8:
raise ArgsParserError("Card number must be exactly 8 characters")
try:
args.id = args.cn.encode('ascii').hex()
except UnicodeEncodeError:
raise ArgsParserError("Card number must be ASCII characters only")
elif args.raw is not None:
if not re.match(r"^[a-fA-F0-9]{32}$", args.raw):
raise ArgsParserError("Raw must be exactly 32 hex characters (128-bit T55XX bitstream)")
try:
card_id = pac_decode_raw(bytes.fromhex(args.raw))
except ValueError as e:
raise ArgsParserError(f"Invalid PAC raw data: {e}")
args.id = card_id.hex()
else:
args.id = None
return True
# PAC uses 7-bit UART frames; MSB of each byte is not encoded
id_bytes = bytes.fromhex(args.id)
if any(b > 0x7F for b in id_bytes):
raise ArgsParserError("PAC card IDs are 7-bit only (each byte must be 0x00-0x7F)")
return True
def args_parser(self) -> ArgumentParserNoExit:
@@ -5819,7 +5915,8 @@ class LFPacWriteT55xx(LFPacIdArgsUnit, ReaderRequiredUnit):
id_bytes = bytes.fromhex(args.id)
self.cmd.pac_write_to_t55xx(id_bytes)
id_ascii = ''.join(chr(b) if 0x20 <= b < 0x7f else '.' for b in id_bytes)
print(f" - PAC/Stanley ID write done: {id_ascii} ({args.id.upper()})")
raw = pac_encode_raw(id_bytes)
print(f" - PAC/Stanley write done - CN: {id_ascii} | Raw: {raw.hex().upper()}")
@lf_pac.command('econfig')
class LFPacEconfig(SlotIndexArgsAndGoUnit, LFPacIdArgsUnit):
@@ -5842,8 +5939,9 @@ class LFPacEconfig(SlotIndexArgsAndGoUnit, LFPacIdArgsUnit):
else:
response = self.cmd.pac_get_emu_id()
card_id_ascii = ''.join(chr(b) if 0x20 <= b < 0x7f else '.' for b in response)
raw = pac_encode_raw(response)
print(' - Get PAC/Stanley tag id success.')
print(f'ID: {card_id_ascii} ({response.hex().upper()})')
print(f'CN: {card_id_ascii} | Raw: {raw.hex().upper()}')
@lf_viking.command("read")
class LFVikingRead(ReaderRequiredUnit):
@@ -6103,7 +6201,9 @@ class HWSlotList(DeviceRequiredUnit):
if lf_tag_type == TagSpecificType.PAC:
id = self.cmd.pac_get_emu_id()
id_ascii = ''.join(chr(b) if 0x20 <= b < 0x7f else '.' for b in id)
print(f" {'ID:':40}{color_string((CY, f'{id_ascii} ({id.hex().upper()})'))}")
raw = pac_encode_raw(id)
print(f" {'CN:':40}{color_string((CY, id_ascii))}")
print(f" {'Raw:':40}{color_string((CY, raw.hex().upper()))}")
if current != selected:
self.cmd.set_active_slot(selected)
+2 -2
View File
@@ -83,8 +83,8 @@ class Command(enum.IntEnum):
HIDPROX_WRITE_TO_T55XX = 3003
VIKING_SCAN = 3004
VIKING_WRITE_TO_T55XX = 3005
PAC_SCAN = 3010
PAC_WRITE_TO_T55XX = 3011
PAC_SCAN = 3014
PAC_WRITE_TO_T55XX = 3015
ADC_GENERIC_READ = 3009
IOPROX_SCAN = 3010
IOPROX_WRITE_TO_T55XX = 3011