From e16505e6a749864130cc32be8af59913dfe299dc Mon Sep 17 00:00:00 2001 From: Niel Nielsen Date: Sun, 5 Apr 2026 12:38:41 +0200 Subject: [PATCH] FEAT! Add T55 write commands --- firmware/application/src/app_cmd.c | 31 +++- firmware/application/src/data_cmd.h | 1 + .../src/rfid/nfctag/lf/protocols/t55xx.h | 4 +- .../src/rfid/reader/lf/lf_reader_main.c | 33 ++++ .../src/rfid/reader/lf/lf_reader_main.h | 3 + software/script/chameleon_cli_unit.py | 168 ++++++++++++++++++ software/script/chameleon_cmd.py | 1 + software/script/chameleon_enum.py | 1 + 8 files changed, 234 insertions(+), 8 deletions(-) diff --git a/firmware/application/src/app_cmd.c b/firmware/application/src/app_cmd.c index 212d1e2..36b2a39 100644 --- a/firmware/application/src/app_cmd.c +++ b/firmware/application/src/app_cmd.c @@ -833,17 +833,33 @@ static data_frame_tx_t *cmd_processor_viking_write_to_t55xx(uint16_t cmd, uint16 return data_frame_make(cmd, status, 0, NULL); } -static data_frame_tx_t *cmd_processor_pac_write_to_t55xx(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { +static data_frame_tx_t *cmd_processor_lf_t55xx_write(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { typedef struct { - uint8_t id[LF_PAC_TAG_ID_SIZE]; - uint8_t new_key[4]; - uint8_t old_keys[4]; + uint8_t block; /* block number */ + uint8_t word[4]; /* 32-bit data word, big-endian */ + uint8_t use_pwd; /* 1 = password write, 0 = open write */ + uint8_t pwd[4]; /* 32-bit password, big-endian (ignored when use_pwd == 0) */ + uint8_t page1; /* 1 = target page 1, 0 = page 0 */ } PACKED payload_t; - payload_t *payload = (payload_t *)data; - if (length < sizeof(payload_t) || (length - offsetof(payload_t, old_keys)) % sizeof(payload->old_keys) != 0) { + + if (length < sizeof(payload_t)) { return data_frame_make(cmd, STATUS_PAR_ERR, 0, NULL); } - status = write_pac_to_t55xx(payload->id, payload->new_key, payload->old_keys, (length - offsetof(payload_t, old_keys)) / sizeof(payload->old_keys)); + + payload_t *p = (payload_t *)data; + + bool page1 = (bool)p->page1; + uint8_t max_block = page1 ? 3u : 7u; + + if (p->block > max_block) { + return data_frame_make(cmd, STATUS_PAR_ERR, 0, NULL); + } + + uint32_t word = bytes_to_num(p->word, 4); + uint32_t passwd = bytes_to_num(p->pwd, 4); + bool use_pwd = (bool)p->use_pwd; + + status = lf_t55xx_write_block(p->block, word, passwd, use_pwd, page1); return data_frame_make(cmd, status, 0, NULL); } @@ -2361,6 +2377,7 @@ static cmd_data_map_t m_data_cmd_map[] = { { DATA_CMD_IOPROX_WRITE_TO_T55XX, before_reader_run, cmd_processor_ioprox_write_to_t55xx, NULL }, { DATA_CMD_PAC_SCAN, before_reader_run, cmd_processor_pac_scan, NULL }, { DATA_CMD_PAC_WRITE_TO_T55XX, before_reader_run, cmd_processor_pac_write_to_t55xx, NULL }, + { DATA_CMD_LF_T55XX_WRITE, before_reader_run, cmd_processor_lf_t55xx_write, NULL }, { DATA_CMD_ADC_GENERIC_READ, before_reader_run, cmd_processor_generic_read, NULL }, { DATA_CMD_HF14A_SET_FIELD_ON, before_reader_run, cmd_processor_hf14a_set_field_on, NULL }, diff --git a/firmware/application/src/data_cmd.h b/firmware/application/src/data_cmd.h index 2705a8e..b681d00 100644 --- a/firmware/application/src/data_cmd.h +++ b/firmware/application/src/data_cmd.h @@ -106,6 +106,7 @@ #define DATA_CMD_IOPROX_WRITE_TO_T55XX (3011) #define DATA_CMD_IOPROX_DECODE_RAW (3012) #define DATA_CMD_IOPROX_COMPOSE_ID (3013) +#define DATA_CMD_LF_T55XX_WRITE (3014) // // ****************************************************************** diff --git a/firmware/application/src/rfid/nfctag/lf/protocols/t55xx.h b/firmware/application/src/rfid/nfctag/lf/protocols/t55xx.h index c0383ba..4242f56 100644 --- a/firmware/application/src/rfid/nfctag/lf/protocols/t55xx.h +++ b/firmware/application/src/rfid/nfctag/lf/protocols/t55xx.h @@ -81,9 +81,11 @@ extern "C" { T5577_PWD | \ (4 << T5577_MAXBLOCK_SHIFT)) +#if defined(PROJECT_CHAMELEON_ULTRA) void t55xx_write_data(uint32_t passwd, uint32_t *blks, uint8_t blk_count); void t55xx_reset_passwd(uint32_t old_passwd, uint32_t new_passwd); - +void t55xx_send_cmd(uint8_t opcode, uint32_t *passwd, uint8_t data_len, uint32_t *data, uint8_t block); +#endif #ifdef __cplusplus } #endif diff --git a/firmware/application/src/rfid/reader/lf/lf_reader_main.c b/firmware/application/src/rfid/reader/lf/lf_reader_main.c index b96fa0d..6aa8383 100644 --- a/firmware/application/src/rfid/reader/lf/lf_reader_main.c +++ b/firmware/application/src/rfid/reader/lf/lf_reader_main.c @@ -1,5 +1,6 @@ #include "lf_reader_main.h" +#include #include "bsp_delay.h" #include "bsp_time.h" #include "hex_utils.h" @@ -209,3 +210,35 @@ uint8_t write_pac_to_t55xx(uint8_t *data, uint8_t *new_passwd, uint8_t *old_pass * Set the LF card scanning timeout value (in milliseconds). */ void set_scan_tag_timeout(uint32_t ms) { g_timeout_readem_ms = ms; } + +#if defined(PROJECT_CHAMELEON_ULTRA) +/** + * Write a single raw 32-bit word to a T55xx block. + * + * Unlike write_em410x_to_t55xx() and friends, this writes the exact word + * supplied with no protocol encoding — useful for custom configuration + * words, recovery of locked tags, or scripted programming. + * + * Only available on Chameleon Ultra (Lite has no LF writer hardware). + * + * @param block Block number (0-7 for page 0, 0-3 for page 1) + * @param word 32-bit data word to write + * @param passwd Password for password-protected write (ignored when use_passwd is false) + * @param use_passwd true = password-protected write, false = open write + * @param page1 true = target page 1, false = page 0 + * @return STATUS_LF_TAG_OK always (T55xx gives no ACK; verify by reading back) + */ +uint8_t lf_t55xx_write_block(uint8_t block, uint32_t word, uint32_t passwd, bool use_passwd, bool page1) { + uint8_t opcode = page1 ? T5577_OPCODE_PAGE1 : T5577_OPCODE_PAGE0; + uint32_t *pwd_ptr = use_passwd ? &passwd : NULL; + + start_lf_125khz_radio(); + bsp_delay_ms(1); // Delay for a while after starting the field + + t55xx_send_cmd(opcode, pwd_ptr, 0, &word, block); + t55xx_send_cmd(T5577_OPCODE_RESET, NULL, 0, NULL, 0); + + stop_lf_125khz_radio(); + return STATUS_LF_TAG_OK; +} +#endif diff --git a/firmware/application/src/rfid/reader/lf/lf_reader_main.h b/firmware/application/src/rfid/reader/lf/lf_reader_main.h index 629f015..df4a820 100644 --- a/firmware/application/src/rfid/reader/lf/lf_reader_main.h +++ b/firmware/application/src/rfid/reader/lf/lf_reader_main.h @@ -24,3 +24,6 @@ uint8_t write_hidprox_to_t55xx(uint8_t format, uint32_t fc, uint64_t cn, uint32_ uint8_t write_ioprox_to_t55xx(uint8_t *raw_data, uint8_t *new_passwd, uint8_t *old_passwds, uint8_t old_passwd_count); uint8_t write_viking_to_t55xx(uint8_t *uid, uint8_t *newkey, uint8_t *old_keys, uint8_t old_key_count); uint8_t write_pac_to_t55xx(uint8_t *data, uint8_t *new_passwd, uint8_t *old_passwds, uint8_t old_passwd_count); +#if defined(PROJECT_CHAMELEON_ULTRA) +uint8_t lf_t55xx_write_block(uint8_t block, uint32_t word, uint32_t passwd, bool use_passwd, bool page1); +#endif diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index f1fcc4d..f62a5d2 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -764,6 +764,7 @@ 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_t55xx = lf.subgroup("t55xx", "T55xx raw commands") lf_generic = lf.subgroup("generic", "Generic commands") @@ -6008,6 +6009,173 @@ class LFVikingWriteT55xx(LFVikingIdArgsUnit, ReaderRequiredUnit): print(f" - Viking ID(8H): {id_hex} write done.") +@lf_t55xx.command("clone") +class LFT55xxClone(ReaderRequiredUnit): + """ + Clone a scanned or manually-specified LF card ID onto a blank T55xx tag. + + Supported types and their required arguments: + + em410x --id <10 hex> e.g. --id DEADBEEF88 + electra --id <26 hex> e.g. --id DEADBEEF880102030405060708 + hid -f --cn e.g. -f H10301 --fc 10 --cn 1234 + ioprox --ver --fc --cn OR --raw8 <16 hex> + viking --id <8 hex> e.g. --id DEADBEEF + + Only supported on Chameleon Ultra (Lite has no LF writer). + """ + + TYPES = ["em410x", "electra", "hid", "ioprox", "viking"] + + def args_parser(self) -> ArgumentParserNoExit: + parser = ArgumentParserNoExit() + parser.description = ( + "Clone a LF card ID onto a blank T55xx tag.\n" + "Supported types: em410x, electra, hid, ioprox, viking.\n" + "Only supported on Chameleon Ultra (Lite has no LF writer)." + ) + parser.add_argument( + "-t", "--type", + type=str, + required=True, + choices=self.TYPES, + metavar="TYPE", + help="Card type: " + ", ".join(self.TYPES), + ) + # EM410x / Electra / Viking + parser.add_argument( + "--id", + type=str, + required=False, + metavar="HEX", + help="Card ID in hex: 10 hex for em410x, 26 for electra, 8 for viking", + ) + # HID Prox + parser.add_argument( + "-f", "--format", + type=str, + required=False, + choices=[x.name for x in HIDFormat], + metavar="FORMAT", + help="HID Prox format, e.g. H10301 (required for hid type)", + ) + parser.add_argument( + "--fc", + type=int, + required=False, + metavar="INT", + help="Facility code (HID / ioProx)", + ) + parser.add_argument( + "--cn", + type=int, + required=False, + metavar="INT", + help="Card number (HID / ioProx)", + ) + parser.add_argument( + "--il", + type=int, + required=False, + metavar="INT", + help="Issue level (HID, optional)", + ) + parser.add_argument( + "--oem", + type=int, + required=False, + metavar="INT", + help="OEM code (HID, optional)", + ) + # ioProx + parser.add_argument( + "--ver", + type=int, + required=False, + metavar="INT", + help="Version byte (ioProx)", + ) + parser.add_argument( + "--raw8", + type=str, + required=False, + metavar="HEX", + help="ioProx raw 8 bytes in hex, e.g. 007854E03A5D65AB", + ) + return parser + + def on_exec(self, args: argparse.Namespace): + t = args.type + + if t in ("em410x", "electra"): + if args.id is None: + raise ArgsParserError("--id is required for em410x / electra") + expected = 10 if t == "em410x" else 26 + if not re.match(r"^[a-fA-F0-9]{" + str(expected) + r"}$", args.id): + raise ArgsParserError( + f"--id must be exactly {expected} hex characters for {t}" + ) + id_bytes = bytes.fromhex(args.id) + self.cmd.em410x_write_to_t55xx(id_bytes) + label = "EM410x Electra" if t == "electra" else "EM410x" + print(f" - {label} ID cloned to T55xx: {args.id.upper()}") + + elif t == "hid": + if args.format is None: + raise ArgsParserError("-f/--format is required for hid") + if args.cn is None: + raise ArgsParserError("--cn is required for hid") + fmt = HIDFormat[args.format] + fc = args.fc if args.fc is not None else 0 + il = args.il if args.il is not None else 0 + oem = args.oem if args.oem is not None else 0 + LFHIDIdArgsUnit.check_limits(fmt.value, fc, args.cn, il, oem) + cn = args.cn + id_bytes = struct.pack( + ">BIBIBH", + fmt.value, + fc, + (cn >> 32), + cn & 0xFFFFFFFF, + il, + oem, + ) + self.cmd.hidprox_write_to_t55xx(id_bytes) + print(f" - HID Prox cloned to T55xx") + print(f" Format : {fmt.name}") + if fc: print(f" FC : {fc}") + if il: print(f" IL : {il}") + if oem: print(f" OEM : {oem}") + print(f" CN : {cn}") + + elif t == "ioprox": + ver = args.ver if args.ver is not None else 1 + fc = int(args.fc, 0) if args.fc is not None else 0 + cn = args.cn if args.cn is not None else 0 + if args.raw8 is not None: + raw8 = LFIOProxIdArgsUnit.parse_raw8(args.raw8) + ver, fc, cn, raw8, *_ = self.cmd.ioprox_decode_raw(raw8) + else: + res = self.cmd.ioprox_compose_id(ver, fc, cn) + raw8 = res[3] + payload16 = struct.pack(">BBH8s4x", ver & 0xFF, fc & 0xFF, cn & 0xFFFF, raw8) + self.cmd.ioprox_write_to_t55xx(payload16) + print(f" - ioProx cloned to T55xx") + print(f" Ver : {ver}") + print(f" FC : {fc} [0x{fc:02X}]") + print(f" CN : {cn}") + print(f" Raw8 : {raw8.hex().upper()}") + + elif t == "viking": + if args.id is None: + raise ArgsParserError("--id is required for viking") + if not re.match(r"^[a-fA-F0-9]{8}$", args.id): + raise ArgsParserError("--id must be exactly 8 hex characters for viking") + id_bytes = bytes.fromhex(args.id) + self.cmd.viking_write_to_t55xx(id_bytes) + print(f" - Viking ID cloned to T55xx: {args.id.upper()}") + + @lf_generic.command("adcread") class LFADCGenericRead(ReaderRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: diff --git a/software/script/chameleon_cmd.py b/software/script/chameleon_cmd.py index 71ae813..a59bdea 100644 --- a/software/script/chameleon_cmd.py +++ b/software/script/chameleon_cmd.py @@ -668,6 +668,7 @@ class ChameleonCMD: + def lf_sniff(self, timeout_ms: int = 2000): """ Capture raw LF field ADC samples. diff --git a/software/script/chameleon_enum.py b/software/script/chameleon_enum.py index da9baa3..3e0cdb4 100644 --- a/software/script/chameleon_enum.py +++ b/software/script/chameleon_enum.py @@ -92,6 +92,7 @@ class Command(enum.IntEnum): IOPROX_WRITE_TO_T55XX = 3011 IOPROX_DECODE_RAW = 3012 IOPROX_COMPOSE_ID = 3013 + LF_T55XX_WRITE = 3014 MF1_WRITE_EMU_BLOCK_DATA = 4000 HF14A_SET_ANTI_COLL_DATA = 4001