diff --git a/firmware/application/src/data_cmd.h b/firmware/application/src/data_cmd.h index 4022a0e..abf91f3 100644 --- a/firmware/application/src/data_cmd.h +++ b/firmware/application/src/data_cmd.h @@ -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) diff --git a/firmware/application/src/rfid/nfctag/lf/protocols/pac.c b/firmware/application/src/rfid/nfctag/lf/protocols/pac.c index a607a32..25a9b2f 100644 --- a/firmware/application/src/rfid/nfctag/lf/protocols/pac.c +++ b/firmware/application/src/rfid/nfctag/lf/protocols/pac.c @@ -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. diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index c7889ab..4e94725 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -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="") + group = parser.add_mutually_exclusive_group(required=required) + group.add_argument("--cn", type=str, + help="Card number (8 ASCII characters, e.g. CARD0001)", + metavar="") + group.add_argument("--raw", type=str, + help="T55XX bitstream (32 hex chars, PM3 raw format)", + metavar="") 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) diff --git a/software/script/chameleon_enum.py b/software/script/chameleon_enum.py index e07f580..d29e10d 100644 --- a/software/script/chameleon_enum.py +++ b/software/script/chameleon_enum.py @@ -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