diff --git a/firmware/application/Makefile b/firmware/application/Makefile index 0e2b2317..41f3e66f 100644 --- a/firmware/application/Makefile +++ b/firmware/application/Makefile @@ -35,7 +35,6 @@ SRC_FILES += \ $(PROJ_DIR)/rfid/nfctag/lf/utils/circular_buffer.c \ $(PROJ_DIR)/rfid/nfctag/lf/utils/manchester.c \ $(PROJ_DIR)/rfid/nfctag/lf/protocols/em410x.c \ - $(PROJ_DIR)/rfid/nfctag/lf/protocols/ioprox.c \ $(PROJ_DIR)/rfid/nfctag/lf/protocols/hidprox.c \ $(PROJ_DIR)/rfid/nfctag/lf/protocols/viking.c \ $(PROJ_DIR)/rfid/nfctag/lf/protocols/wiegand.c \ @@ -341,10 +340,11 @@ ifeq (${CURRENT_DEVICE_TYPE}, ${CHAMELEON_ULTRA}) $(PROJ_DIR)/rfid/reader/hf/rc522.c \ $(PROJ_DIR)/rfid/reader/lf/lf_125khz_radio.c \ $(PROJ_DIR)/rfid/reader/lf/lf_em410x_data.c \ + $(PROJ_DIR)/rfid/reader/lf/lf_em4x05_data.c \ + $(PROJ_DIR)/rfid/reader/lf/lf_gap.c \ $(PROJ_DIR)/rfid/reader/lf/lf_reader_data.c \ $(PROJ_DIR)/rfid/reader/lf/lf_reader_main.c \ $(PROJ_DIR)/rfid/reader/lf/lf_t55xx_data.c \ - $(PROJ_DIR)/rfid/reader/lf/lf_ioprox_data.c \ $(PROJ_DIR)/rfid/reader/lf/lf_hidprox_data.c \ $(PROJ_DIR)/rfid/reader/lf/lf_viking_data.c \ $(PROJ_DIR)/rfid/reader/lf/lf_reader_generic.c \ diff --git a/firmware/application/src/app_cmd.c b/firmware/application/src/app_cmd.c index a8ada471..451e478e 100644 --- a/firmware/application/src/app_cmd.c +++ b/firmware/application/src/app_cmd.c @@ -714,85 +714,6 @@ static data_frame_tx_t *cmd_processor_hidprox_scan(uint16_t cmd, uint16_t status return data_frame_make(cmd, STATUS_LF_TAG_OK, sizeof(card_data), card_data); } -static data_frame_tx_t *cmd_processor_ioprox_scan(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { - uint8_t card_data[16] = {0}; - uint8_t hint = (data != NULL) ? data[0] : 0; - status = scan_ioprox(card_data, hint); - if (status != STATUS_LF_TAG_OK) { - return data_frame_make(cmd, status, 0, NULL); - } - - return data_frame_make(cmd, STATUS_LF_TAG_OK, sizeof(card_data), card_data); -} - -static data_frame_tx_t *cmd_processor_ioprox_write_to_t55xx(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { - typedef struct { - uint8_t card_data[16]; // ioprox_codec_t->data layout: version, facility code, card number, raw8 - uint8_t new_key[4]; - uint8_t old_keys[4]; // we can have more than one... struct just to compute offsets with min 1 key - } PACKED payload_t; - - payload_t *payload = (payload_t *)data; - - // Validate packet length - if (length < sizeof(payload_t) || - (length - offsetof(payload_t, old_keys)) % sizeof(payload->old_keys) != 0) { - return data_frame_make(cmd, STATUS_PAR_ERR, 0, NULL); - } - - uint8_t old_cnt = (length - offsetof(payload_t, old_keys)) / sizeof(payload->old_keys); - - // Pass card_data (including raw8 at index 4-11) directly to the T55xx writer. - status = write_ioprox_to_t55xx( - payload->card_data, - payload->new_key, - payload->old_keys, - old_cnt - ); - - return data_frame_make(cmd, status, 0, NULL); -} - -/** - * @brief Decode raw8 data to structured ioProx format - * @param raw8 Input 8 bytes - * @param output 16 bytes ioprox_codec_t->data layout: version, facility code, card number, raw8 - * @return STATUS_SUCCESS on success - */ -static data_frame_tx_t *cmd_processor_ioprox_decode_raw(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { - if (length != 8) return data_frame_make(cmd, STATUS_PAR_ERR, 0, NULL); - - uint8_t output[16]; - uint8_t result = decode_ioprox_raw(data, output); - - if (result != STATUS_SUCCESS) { - return data_frame_make(cmd, STATUS_CMD_ERR, 0, NULL); - } - - return data_frame_make(cmd, STATUS_SUCCESS, 16, output); -} - -/** - * @brief Encode ioProx parameters to structured ioProx format - * @param ver Version byte - * @param fc Facility code byte - * @param cn Card number (16-bit) - * @param out 16 bytes ioprox_codec_t->data layout: version, facility code, card number, raw8 - * @return STATUS_SUCCESS on success - */ -static data_frame_tx_t *cmd_processor_ioprox_compose_id(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { - if (length != 4) return data_frame_make(cmd, STATUS_PAR_ERR, 0, NULL); - - uint8_t output[16]; - memset(output, 0, sizeof(output)); - uint8_t result = encode_ioprox_params(data[0], data[1], (data[2] << 8) | data[3], output); - - if (result != STATUS_SUCCESS) { - return data_frame_make(cmd, STATUS_CMD_ERR, 0, NULL); - } - return data_frame_make(cmd, STATUS_SUCCESS, 16, output); -} - static data_frame_tx_t *cmd_processor_viking_scan(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { uint8_t card_buffer[4] = {0x00}; status = scan_viking(card_buffer); @@ -817,6 +738,32 @@ 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_em4x05_scan(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { + em4x05_data_t tag = {0}; + status = scan_em4x05(&tag); + if (status != STATUS_LF_TAG_OK) { + return data_frame_make(cmd, status, 0, NULL); + } + /* + * Response payload layout: + * config [4 bytes] — block 0 configuration word + * uid [4 bytes] — EM4x05 block 15 UID (or EM4x69 uid_lo) + * uid_hi [4 bytes] — EM4x69 uid_hi (zero for EM4x05) + * is_em4x69[1 byte] — 1 if 64-bit UID was read, 0 otherwise + */ + struct { + uint32_t config; + uint32_t uid; + uint32_t uid_hi; + uint8_t is_em4x69; + } PACKED payload; + payload.config = U32HTONL(tag.config); + payload.uid = U32HTONL(tag.uid); + payload.uid_hi = U32HTONL(tag.uid_hi); + payload.is_em4x69 = tag.is_em4x69 ? 1 : 0; + return data_frame_make(cmd, STATUS_LF_TAG_OK, sizeof(payload), (uint8_t *)&payload); +} + #define GENERIC_READ_LEN 800 #define GENERIC_READ_TIMEOUT_MS 500 static data_frame_tx_t *cmd_processor_generic_read(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { @@ -1026,26 +973,6 @@ static data_frame_tx_t *cmd_processor_hidprox_get_emu_id(uint16_t cmd, uint16_t return data_frame_make(cmd, STATUS_SUCCESS, LF_HIDPROX_TAG_ID_SIZE, buffer->buffer); } -static data_frame_tx_t *cmd_processor_ioprox_set_emu_id(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { - if (length != LF_IOPROX_TAG_ID_SIZE) { - return data_frame_make(cmd, STATUS_PAR_ERR, 0, NULL); - } - tag_data_buffer_t *buffer = get_buffer_by_tag_type(TAG_TYPE_IOPROX); - memcpy(buffer->buffer, data, LF_IOPROX_TAG_ID_SIZE); - tag_emulation_load_by_buffer(TAG_TYPE_IOPROX, false); - return data_frame_make(cmd, STATUS_SUCCESS, 0, NULL); -} - -static data_frame_tx_t *cmd_processor_ioprox_get_emu_id(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { - tag_slot_specific_type_t tag_types; - tag_emulation_get_specific_types_by_slot(tag_emulation_get_slot(), &tag_types); - if (tag_types.tag_lf != TAG_TYPE_IOPROX) { - return data_frame_make(cmd, STATUS_PAR_ERR, 0, data); // no data in slot, don't send garbage - } - tag_data_buffer_t *buffer = get_buffer_by_tag_type(TAG_TYPE_IOPROX); - return data_frame_make(cmd, STATUS_SUCCESS, LF_IOPROX_TAG_ID_SIZE, buffer->buffer); -} - static data_frame_tx_t *cmd_processor_viking_set_emu_id(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { if (length != LF_VIKING_TAG_ID_SIZE) { return data_frame_make(cmd, STATUS_PAR_ERR, 0, NULL); @@ -1796,8 +1723,7 @@ static cmd_data_map_t m_data_cmd_map[] = { { DATA_CMD_HIDPROX_WRITE_TO_T55XX, before_reader_run, cmd_processor_hidprox_write_to_t55xx, NULL }, { DATA_CMD_VIKING_SCAN, before_reader_run, cmd_processor_viking_scan, NULL }, { DATA_CMD_VIKING_WRITE_TO_T55XX, before_reader_run, cmd_processor_viking_write_to_t55xx, NULL }, - { DATA_CMD_IOPROX_SCAN, before_reader_run, cmd_processor_ioprox_scan, NULL }, - { DATA_CMD_IOPROX_WRITE_TO_T55XX, before_reader_run, cmd_processor_ioprox_write_to_t55xx, NULL }, + { DATA_CMD_EM4X05_SCAN, before_reader_run, cmd_processor_em4x05_scan, 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 }, @@ -1806,9 +1732,6 @@ static cmd_data_map_t m_data_cmd_map[] = { { DATA_CMD_HF14A_GET_CONFIG, NULL, cmd_processor_hf14a_get_config, NULL }, { DATA_CMD_HF14A_SET_CONFIG, NULL, cmd_processor_hf14a_set_config, NULL }, - { DATA_CMD_IOPROX_DECODE_RAW, NULL, cmd_processor_ioprox_decode_raw, NULL }, - { DATA_CMD_IOPROX_COMPOSE_ID, NULL, cmd_processor_ioprox_compose_id, NULL }, - #endif { DATA_CMD_HF14A_GET_ANTI_COLL_DATA, NULL, cmd_processor_hf14a_get_anti_coll_data, NULL }, @@ -1856,8 +1779,6 @@ static cmd_data_map_t m_data_cmd_map[] = { { DATA_CMD_EM410X_GET_EMU_ID, NULL, cmd_processor_em410x_get_emu_id, NULL }, { DATA_CMD_HIDPROX_SET_EMU_ID, NULL, cmd_processor_hidprox_set_emu_id, NULL }, { DATA_CMD_HIDPROX_GET_EMU_ID, NULL, cmd_processor_hidprox_get_emu_id, NULL }, - { DATA_CMD_IOPROX_SET_EMU_ID, NULL, cmd_processor_ioprox_set_emu_id, NULL }, - { DATA_CMD_IOPROX_GET_EMU_ID, NULL, cmd_processor_ioprox_get_emu_id, NULL }, { DATA_CMD_VIKING_SET_EMU_ID, NULL, cmd_processor_viking_set_emu_id, NULL }, { DATA_CMD_VIKING_GET_EMU_ID, NULL, cmd_processor_viking_get_emu_id, NULL }, }; diff --git a/firmware/application/src/data_cmd.h b/firmware/application/src/data_cmd.h index 51ac5fcb..b4a5e4ae 100644 --- a/firmware/application/src/data_cmd.h +++ b/firmware/application/src/data_cmd.h @@ -98,10 +98,7 @@ #define DATA_CMD_ADC_GENERIC_READ (3009) #define DATA_CMD_GENERIC_READ (3007) #define DATA_CMD_CORR_GENERIC_READ (3008) -#define DATA_CMD_IOPROX_SCAN (3010) -#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_EM4X05_SCAN (3010) // // ****************************************************************** @@ -170,7 +167,5 @@ #define DATA_CMD_HIDPROX_GET_EMU_ID (5003) #define DATA_CMD_VIKING_SET_EMU_ID (5004) #define DATA_CMD_VIKING_GET_EMU_ID (5005) -#define DATA_CMD_IOPROX_SET_EMU_ID (5008) -#define DATA_CMD_IOPROX_GET_EMU_ID (5009) #endif diff --git a/firmware/application/src/rfid/reader/lf/lf_em4x05_data.c b/firmware/application/src/rfid/reader/lf/lf_em4x05_data.c new file mode 100644 index 00000000..be187bee --- /dev/null +++ b/firmware/application/src/rfid/reader/lf/lf_em4x05_data.c @@ -0,0 +1,353 @@ +#include "lf_em4x05_data.h" + +#include +#include + +#include "app_status.h" +#include "bsp_delay.h" +#include "bsp_time.h" +#include "circular_buffer.h" +#include "lf_125khz_radio.h" +#include "lf_gap.h" +#include "lf_reader_data.h" +#include "timeslot.h" + +#include "utils/manchester.h" + +#define NRF_LOG_MODULE_NAME lf_em4x05 +#include "nrf_log.h" +#include "nrf_log_ctrl.h" +#include "nrf_log_default_backends.h" +NRF_LOG_MODULE_REGISTER(); + +/* ----------------------------------------------------------------------- + * Internal constants + * --------------------------------------------------------------------- */ + +/* + * Command word structure (9 bits, sent MSB-first after start gap): + * bit 8: start bit (always 1) + * bits 7-6: opcode (2 bits) + * bits 5-3: block address (3 bits) + * bits 2-0: odd parity of the 6 data bits (3 bits, one per pair) + * + * The parity scheme: three parity bits p[2:0] where + * p[2] = parity(opcode[1], opcode[0], addr[2]) + * p[1] = parity(opcode[1], addr[1], addr[0]) -- see datasheet §6.1 + * p[0] = parity(opcode[0], addr[2], addr[1]) + * Each is odd parity over the three bits. + * + * Response word (45 bits): + * bit 44: header (always 0 per datasheet — marks start) + * bits 43-12: data (32 bits, MSB first) + * bits 11-8: column parity (4 bits) + * bit 7: stop bit (always 0) + * bits 6-3: row parity (4 bits) [actually these are col parity bits 3-0] + * bit 2-1: stop bits + * bit 0: trailer + * + * Simplified: we read 45 bits, extract bits [43:12] as data, validate the + * 4-bit column parity over the 32 data bits (8 nibbles × 4 bits each), and + * validate the row parity bits. + * + * Parity (EM4x05 datasheet §5, same layout as EM4100): + * The 32 data bits are arranged as 8 rows × 4 columns. + * Row parity: one odd parity bit per row (appended after each row). + * Column parity: one odd parity bit per column (4 bits total, at the end). + * + * In the 45-bit response the layout is: + * [0] header + * [1..4] row 0 data nibble + * [5] row 0 parity + * [6..9] row 1 data nibble + * [10] row 1 parity + * ... + * [36..39] row 7 data nibble + * [40] row 7 parity + * [41..44] column parity nibble + */ + +#define EM4X05_CMD_BITS 9 +#define EM4X05_RESP_BITS 45 +#define EM4X05_ROWS 8 +#define EM4X05_COLS 4 + +#define EM4X05_CB_SIZE 256 /* circular buffer capacity (intervals) */ + +/* ----------------------------------------------------------------------- + * Parity helpers + * --------------------------------------------------------------------- */ + +/* Odd parity of a byte (1 if odd number of set bits, 0 if even) */ +static inline uint8_t odd_parity4(uint8_t nibble) { + nibble ^= nibble >> 2; + nibble ^= nibble >> 1; + return (~nibble) & 1; +} + +/* Build the 3-bit command parity field per EM4x05 datasheet §6.1 */ +static uint8_t em4x05_cmd_parity(uint8_t opcode, uint8_t addr) { + uint8_t o1 = (opcode >> 1) & 1; + uint8_t o0 = (opcode) & 1; + uint8_t a2 = (addr >> 2) & 1; + uint8_t a1 = (addr >> 1) & 1; + uint8_t a0 = (addr) & 1; + + uint8_t p2 = (~(o1 ^ o0 ^ a2)) & 1; /* odd parity of o1,o0,a2 */ + uint8_t p1 = (~(o1 ^ a1 ^ a0)) & 1; + uint8_t p0 = (~(o0 ^ a2 ^ a1)) & 1; + + return (p2 << 2) | (p1 << 1) | p0; +} + +/* Build the full 9-bit command word */ +static uint16_t em4x05_build_cmd(uint8_t opcode, uint8_t addr) { + uint8_t parity = em4x05_cmd_parity(opcode, addr); + /* [start=1][opcode 2b][addr 3b][parity 3b] */ + return (1u << 8) | ((opcode & 0x3) << 6) | ((addr & 0x7) << 3) | (parity & 0x7); +} + +/* ----------------------------------------------------------------------- + * Response decoding + * --------------------------------------------------------------------- */ + +/* + * Decode a 45-bit response word. + * + * bits[] must contain exactly EM4X05_RESP_BITS bits in order of reception + * (bit 0 = first received = header bit). + * + * Returns true and writes *data if parity checks pass. + */ +static bool em4x05_decode_response(const uint8_t *bits, uint32_t *data) { + /* bit[0] = header, should be 0 (tag sets it to 0 before data) */ + /* Not strictly required for decoding but sanity-check it */ + if (bits[0] != 0) { + return false; + } + + uint32_t result = 0; + uint8_t col_parity[EM4X05_COLS] = {0}; + + for (int row = 0; row < EM4X05_ROWS; row++) { + int base = 1 + row * (EM4X05_COLS + 1); /* +1 for parity bit */ + uint8_t nibble = 0; + for (int col = 0; col < EM4X05_COLS; col++) { + uint8_t b = bits[base + col] & 1; + nibble = (nibble << 1) | b; + col_parity[col] ^= b; + } + /* Row parity check */ + uint8_t rp = bits[base + EM4X05_COLS] & 1; + if (rp != odd_parity4(nibble)) { + NRF_LOG_DEBUG("em4x05: row %d parity fail", row); + return false; + } + result = (result << EM4X05_COLS) | nibble; + } + + /* Column parity check: bits [41..44] */ + int cp_base = 1 + EM4X05_ROWS * (EM4X05_COLS + 1); + for (int col = 0; col < EM4X05_COLS; col++) { + uint8_t received_cp = bits[cp_base + col] & 1; + /* col_parity[col] is XOR of all data bits in that column; + * odd parity means it should equal 1 when col_parity is even */ + if (received_cp != ((~col_parity[col]) & 1)) { + NRF_LOG_DEBUG("em4x05: col %d parity fail", col); + return false; + } + } + + *data = result; + return true; +} + +/* ----------------------------------------------------------------------- + * Timeslot command send + * --------------------------------------------------------------------- */ + +static uint8_t g_send_opcode; +static uint8_t g_send_addr; + +static void em4x05_send_timeslot_cb(void) { + lf_gap_send_start(); + /* Allow tag to power up fully after start gap */ + bsp_delay_us(GAP_LISTEN_US); + /* Send 9-bit command MSB-first */ + uint16_t cmd = em4x05_build_cmd(g_send_opcode, g_send_addr); + lf_gap_send_bits(cmd, EM4X05_CMD_BITS); + /* + * Leave carrier on — tag will begin responding after ~3Tc. + * The receive loop in em4x05_read_block() takes over from here. + */ +} + +/* ----------------------------------------------------------------------- + * Edge-capture receive loop + * --------------------------------------------------------------------- */ + +static circular_buffer g_cb; + +static void em4x05_edge_cb(void) { + uint32_t cnt = get_lf_counter_value(); + uint16_t val = (cnt > 0xff) ? 0xff : (uint16_t)(cnt & 0xff); + cb_push_back(&g_cb, &val); + clear_lf_counter_value(); +} + +/* + * Read one block from the tag. + * + * Sends the READ command via a timeslot, then switches to edge-capture + * receive mode and runs the Manchester decoder until 45 bits are collected + * or the timeout expires. + * + * @param addr Block address (0–15). + * @param data Output: decoded 32-bit block value. + * @param timeout_ms Overall timeout for the receive phase. + * @return true on success. + */ +/* + * RF/64 Manchester period classifier for EM4x05. + * 1T=64Tc, 1.5T=96Tc, 2T=128Tc, jitter=±16Tc. + */ +#define EM4X05_T1 0x40u +#define EM4X05_T15 0x60u +#define EM4X05_T2 0x80u +#define EM4X05_JIT 0x10u + +static uint8_t em4x05_rf64_period(uint8_t interval) { + if (interval >= (EM4X05_T1 - EM4X05_JIT) && interval <= (EM4X05_T1 + EM4X05_JIT)) return 0; + if (interval >= (EM4X05_T15 - EM4X05_JIT) && interval <= (EM4X05_T15 + EM4X05_JIT)) return 1; + if (interval >= (EM4X05_T2 - EM4X05_JIT) && interval <= (EM4X05_T2 + EM4X05_JIT)) return 2; + return 3; +} + +static bool em4x05_read_block(uint8_t addr, uint32_t *data, uint32_t timeout_ms) { + manchester modem = { + .sync = true, + .rp = em4x05_rf64_period, + }; + + uint8_t resp_bits[EM4X05_RESP_BITS] = {0}; + uint8_t bit_count = 0; + + /* Stage 1: send command in timeslot */ + g_send_opcode = EM4X05_OPCODE_READ; + g_send_addr = addr; + + /* + * The timeslot duration needs to cover the command transmission: + * start_gap(50) + listen(50) + 9 bits × (56+10)µs ≈ 1294µs. + * Request 2ms to be safe. + */ + request_timeslot(2000, em4x05_send_timeslot_cb); + + /* Give the timeslot time to complete */ + bsp_delay_ms(3); + + /* Stage 2: receive — switch to edge-capture mode */ + cb_init(&g_cb, EM4X05_CB_SIZE, sizeof(uint16_t)); + register_rio_callback(em4x05_edge_cb); + lf_125khz_radio_gpiote_enable(); + clear_lf_counter_value(); + + bool ok = false; + autotimer *p_at = bsp_obtain_timer(0); + + while (!ok && NO_TIMEOUT_1MS(p_at, timeout_ms)) { + uint16_t interval = 0; + if (!cb_pop_front(&g_cb, &interval)) { + continue; + } + + bool mbits[2] = {false, false}; + int8_t mbitlen = 0; + manchester_feed(&modem, (uint8_t)interval, mbits, &mbitlen); + + if (mbitlen == -1) { + /* Desync — reset and keep trying */ + manchester_reset(&modem); + bit_count = 0; + continue; + } + + for (int8_t i = 0; i < mbitlen && bit_count < EM4X05_RESP_BITS; i++) { + resp_bits[bit_count++] = mbits[i] ? 1 : 0; + } + + if (bit_count >= EM4X05_RESP_BITS) { + ok = em4x05_decode_response(resp_bits, data); + if (!ok) { + /* + * Parity failed — could be a framing alignment issue. + * Slide the window by discarding the oldest bit and + * continuing accumulation. + */ + memmove(resp_bits, resp_bits + 1, EM4X05_RESP_BITS - 1); + bit_count = EM4X05_RESP_BITS - 1; + } + } + } + + bsp_return_timer(p_at); + lf_125khz_radio_gpiote_disable(); + unregister_rio_callback(); + cb_free(&g_cb); + + return ok; +} + +/* ----------------------------------------------------------------------- + * Public API + * --------------------------------------------------------------------- */ + +bool em4x05_read(em4x05_data_t *out, uint32_t timeout_ms) { + memset(out, 0, sizeof(*out)); + + /* + * Read block 0 (config) and block 15 (UID). + * For EM4x69 also attempt blocks 13 and 14 (64-bit UID). + * + * We split the timeout evenly across up to 4 block reads. + */ + uint32_t block_timeout = timeout_ms / 4; + if (block_timeout < 50) block_timeout = 50; + + /* Block 0: configuration */ + if (!em4x05_read_block(EM4X05_BLOCK_CONFIG, &out->config, block_timeout)) { + NRF_LOG_DEBUG("em4x05: block 0 read failed"); + return false; + } + + /* Block 15: UID (EM4x05) */ + if (!em4x05_read_block(EM4X05_BLOCK_UID, &out->uid, block_timeout)) { + NRF_LOG_DEBUG("em4x05: block 15 read failed"); + return false; + } + + /* + * Attempt EM4x69 64-bit UID blocks (13 and 14). + * Failure here is non-fatal — tag may simply be an EM4x05. + */ + uint32_t uid_lo = 0, uid_hi = 0; + if (em4x05_read_block(EM4X69_BLOCK_UID_LO, &uid_lo, block_timeout) && + em4x05_read_block(EM4X69_BLOCK_UID_HI, &uid_hi, block_timeout)) { + out->uid_hi = uid_hi; + out->uid = uid_lo; /* overwrite with EM4x69 UID lo */ + out->is_em4x69 = true; + } + + return true; +} + +uint8_t scan_em4x05(em4x05_data_t *out) { + start_lf_125khz_radio(); + bsp_delay_ms(5); /* allow tag to power up */ + + bool found = em4x05_read(out, 500); + + stop_lf_125khz_radio(); + + return found ? STATUS_LF_TAG_OK : STATUS_LF_TAG_NO_FOUND; +} diff --git a/firmware/application/src/rfid/reader/lf/lf_em4x05_data.h b/firmware/application/src/rfid/reader/lf/lf_em4x05_data.h new file mode 100644 index 00000000..4896b103 --- /dev/null +++ b/firmware/application/src/rfid/reader/lf/lf_em4x05_data.h @@ -0,0 +1,126 @@ +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * EM4x05 / EM4x69 reader (read-only). + * + * EM4x05 and EM4x69 are reader-talk-first (RTF) 125kHz LF tags made by + * EM Microelectronic. EM4x69 is identical to EM4x05 with an added + * password-protected login command; the read protocol is the same. + * + * Protocol summary (EM4x05 datasheet rev 1.0, §6): + * + * 1. Reader asserts a start gap (≥50 Tc field off). + * 2. Reader sends a 9-bit command word: + * [start_bit=1] [opcode 2 bits] [address 3 bits] [parity 3 bits] + * using gap encoding: field on for 56Tc = '1', 24Tc = '0', separated + * by 10Tc write gaps. + * 3. Tag waits ~3Tc then transmits a 45-bit response word: + * [header 1 bit] [data 32 bits] [col_parity 4 bits] [stop 1 bit] + * [row_parity 4 bits] [stop 1 bit] [trailer 2 bits] + * encoded as Manchester at RF/64 (one bit = 64 carrier cycles). + * + * Opcodes: + * EM4X05_OPCODE_READ (0b10 = 2) — read one block + * EM4X05_OPCODE_WRITE (0b01 = 1) — write one block (not implemented here) + * EM4X05_OPCODE_PRCT (0b11 = 3) — protect (not implemented here) + * EM4X05_OPCODE_DSBL (0b00 = 0) — disable (not implemented here) + * + * Block map (EM4x05, 16 blocks × 32 bits): + * Block 0: configuration word + * Block 1: password (write-only; reads as 0) + * Block 2: user data + * ... + * Block 15: UID (read-only) + * + * EM4x69 adds blocks 13–14 for a 64-bit UID and a LOGIN command that must + * be issued before protected blocks are accessible. The read protocol for + * unprotected blocks is identical. + * + * This implementation reads the minimum set needed to identify a tag: + * - Block 0 (config — tells us encoding, data rate, tag type) + * - Block 1 (UID low word for EM4x05; block 13 for EM4x69 64-bit UID) + * - Block 15 (UID for EM4x05; per-chip serial for EM4x69) + * + * Data returned: + * em4x05_data_t packs the raw block words read from the tag. + */ + +/* ----------------------------------------------------------------------- + * Constants + * --------------------------------------------------------------------- */ + +#define EM4X05_OPCODE_READ 0x02 /* 0b10 */ +#define EM4X05_OPCODE_WRITE 0x01 /* 0b01 — not used here */ +#define EM4X05_OPCODE_PRCT 0x03 /* 0b11 — not used here */ +#define EM4X05_OPCODE_DSBL 0x00 /* 0b00 — not used here */ + +#define EM4X05_BLOCK_CONFIG 0 +#define EM4X05_BLOCK_PASSWD 1 +#define EM4X05_BLOCK_UID_LO 2 /* EM4x05: first user data block */ +#define EM4X05_BLOCK_UID 15 /* EM4x05: factory UID */ +#define EM4X69_BLOCK_UID_LO 13 /* EM4x69: 64-bit UID low word */ +#define EM4X69_BLOCK_UID_HI 14 /* EM4x69: 64-bit UID high word */ + +/** Number of bits in one tag response word (header+data+parity+stop+trailer) */ +#define EM4X05_RESPONSE_BITS 45 + +/** Manchester clock: one bit = RF/64 = 64 carrier cycles */ +#define EM4X05_RF_DIV 64 + +/** + * Timeout waiting for the tag response after command, in carrier cycles. + * The tag begins responding within ~3Tc; we wait up to 300Tc to cover + * slow wakeup and Manchester sync acquisition. + */ +#define EM4X05_RESPONSE_TIMEOUT_TC 300 + +/* ----------------------------------------------------------------------- + * Data structures + * --------------------------------------------------------------------- */ + +/** Raw data read back from an EM4x05/4x69 tag. */ +typedef struct { + uint32_t config; /* block 0: configuration word */ + uint32_t uid; /* block 15 (EM4x05) or blocks 13+14 (EM4x69) */ + uint32_t uid_hi; /* EM4x69 only: high word of 64-bit UID */ + bool is_em4x69; /* true if 64-bit UID was successfully read */ +} em4x05_data_t; + +/* ----------------------------------------------------------------------- + * Public API + * --------------------------------------------------------------------- */ + +/** + * Read an EM4x05 or EM4x69 tag. + * + * Sends a READ command for each required block, decodes the Manchester + * response, validates parity, and fills *out on success. + * + * The 125kHz carrier must already be running (call start_lf_125khz_radio() + * and enable the GPIOTE edge counter before calling this). + * + * @param out Output structure; valid only when returns true. + * @param timeout_ms Maximum time to wait for the first response. + * @return true on success (at least block 15 / UID read OK). + */ +bool em4x05_read(em4x05_data_t *out, uint32_t timeout_ms); + +/** + * High-level scan entry point matching the pattern of em410x_read() in + * lf_reader_main.c. Starts the radio, reads the tag, stops the radio. + * + * @param out Output structure. + * @return STATUS_LF_TAG_OK or STATUS_LF_TAG_NO_FOUND. + */ +uint8_t scan_em4x05(em4x05_data_t *out); + +#ifdef __cplusplus +} +#endif diff --git a/firmware/application/src/rfid/reader/lf/lf_gap.c b/firmware/application/src/rfid/reader/lf/lf_gap.c new file mode 100644 index 00000000..3acadeac --- /dev/null +++ b/firmware/application/src/rfid/reader/lf/lf_gap.c @@ -0,0 +1,79 @@ +#include "lf_gap.h" + +#include "bsp_delay.h" +#include "lf_125khz_radio.h" +#include "lf_reader_data.h" + +#define NRF_LOG_MODULE_NAME lf_gap +#include "nrf_log.h" +#include "nrf_log_ctrl.h" +#include "nrf_log_default_backends.h" +NRF_LOG_MODULE_REGISTER(); + +/* ----------------------------------------------------------------------- + * Transmit side + * + * All functions must be called from within a timeslot callback, exactly + * as t55xx_timeslot_callback() does in lf_t55xx_data.c. The timeslot + * gives us uninterrupted CPU time so the µs-precision delays are accurate. + * --------------------------------------------------------------------- */ + +void lf_gap_send_start(void) { + stop_lf_125khz_radio(); + bsp_delay_us(GAP_START_US); + start_lf_125khz_radio(); +} + +void lf_gap_send_bit(uint8_t bit) { + /* Field on for the bit duration, then a write gap */ + if (bit & 1) { + bsp_delay_us(GAP_BIT1_US); + } else { + bsp_delay_us(GAP_BIT0_US); + } + stop_lf_125khz_radio(); + bsp_delay_us(GAP_WRITE_US); + start_lf_125khz_radio(); +} + +void lf_gap_send_u32(uint32_t word) { + lf_gap_send_bits(word, 32); +} + +void lf_gap_send_bits(uint32_t value, uint8_t nbits) { + for (int8_t i = (int8_t)(nbits - 1); i >= 0; i--) { + lf_gap_send_bit((value >> i) & 1); + } +} + +/* ----------------------------------------------------------------------- + * Receive side + * + * The GPIOTE edge counter (m_pwm_timer_counter via get_lf_counter_value) + * increments once per carrier cycle while the field is present and edges + * arrive. During a gap, no edges arrive so the counter freezes. + * + * We detect a gap by comparing the current counter value against the value + * at the last known edge. If the difference exceeds GAP_DETECT_TIMEOUT_TC + * we declare a gap. + * + * Note: get_lf_counter_value() returns the captured counter (snapshot), + * not a live read. The counter is captured by the PPI on each PWM period + * end. At 125kHz this gives 8µs granularity which is sufficient. + * --------------------------------------------------------------------- */ + +bool lf_gap_detect(uint32_t last_count, uint32_t *gap_tc) { + uint32_t now = get_lf_counter_value(); + + /* + * Handle counter wrap (32-bit, wraps at 2^32 carrier cycles ≈ 9.5 hours + * of continuous field — effectively never, but handle it correctly). + */ + uint32_t elapsed = now - last_count; + + if (elapsed >= GAP_DETECT_TIMEOUT_TC) { + *gap_tc = elapsed; + return true; + } + return false; +} diff --git a/firmware/application/src/rfid/reader/lf/lf_gap.h b/firmware/application/src/rfid/reader/lf/lf_gap.h new file mode 100644 index 00000000..9412251b --- /dev/null +++ b/firmware/application/src/rfid/reader/lf/lf_gap.h @@ -0,0 +1,134 @@ +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * LF reader-talk-first gap detection and transmission. + * + * Reader-talk-first (RTF) protocols like EM4x05/4x69 and EM4x50/4x70 + * communicate with the tag by briefly cutting the 125kHz carrier field. + * A "gap" — carrier off for a calibrated number of carrier cycles — encodes + * one bit. After the command sequence, the reader restores the field and + * listens for the tag's Manchester- or Biphase-encoded response. + * + * Gap timing (EM4x05 / EM4x69, per datasheet): + * Start gap: ~50 Tc (powers up and resets the tag) + * Write gap: ~10 Tc (separates command bits during transmission) + * Bit '0': ~24 Tc field on between gaps + * Bit '1': ~56 Tc field on between gaps + * + * The existing T5577 writer in lf_t55xx_data.c uses the same physical + * mechanism (stop_lf_125khz_radio / bsp_delay_us / start_lf_125khz_radio) + * inside a timeslot callback. This module follows the same pattern. + * + * Gap detection on the receive side: + * The GPIOTE edge-capture counter fires on each carrier envelope edge. + * During a gap the carrier is absent, so no edges arrive. We detect a + * gap by polling the counter and declaring a gap when no edge has arrived + * within GAP_DETECT_TIMEOUT_TC carrier cycles. The gap duration is then + * the elapsed counter value. + * + * Units: all timing constants are in carrier cycles (Tc = 1/125000 s = 8 µs). + * bsp_delay_us() is used for gap transmission; the counter captures elapsed + * carrier cycles on the receive side. + */ + +/* ----------------------------------------------------------------------- + * Transmit timing constants (in microseconds = Tc × 8) + * --------------------------------------------------------------------- */ + +/** Start gap: resets the tag and signals start of a command sequence. */ +#define GAP_START_TC 50 +#define GAP_START_US (GAP_START_TC * 8) + +/** Write gap: separates command bits during transmission. */ +#define GAP_WRITE_TC 10 +#define GAP_WRITE_US (GAP_WRITE_TC * 8) + +/** Field-on duration encoding bit '0' between write gaps. */ +#define GAP_BIT0_TC 24 +#define GAP_BIT0_US (GAP_BIT0_TC * 8) + +/** Field-on duration encoding bit '1' between write gaps. */ +#define GAP_BIT1_TC 56 +#define GAP_BIT1_US (GAP_BIT1_TC * 8) + +/** + * Listen window after command: time the tag needs before it begins + * transmitting its response (EM4x05 datasheet: ~3 Tc after last gap). + * We wait a generous 50 Tc to be safe with slow tags. + */ +#define GAP_LISTEN_TC 50 +#define GAP_LISTEN_US (GAP_LISTEN_TC * 8) + +/* ----------------------------------------------------------------------- + * Receive timing constants (in carrier cycles) + * --------------------------------------------------------------------- */ + +/** + * Gap detection timeout: if no edge arrives within this many carrier + * cycles, the current interval is treated as a gap. + * Set conservatively above the longest expected normal interval (≈ 2×RF/64 + * = 128 Tc for EM4x05 Manchester at RF/64) but below any deliberate gap. + */ +#define GAP_DETECT_TIMEOUT_TC 200 + +/* ----------------------------------------------------------------------- + * API + * --------------------------------------------------------------------- */ + +/** + * Send a start gap (carrier off for GAP_START_US, then carrier on). + * Must be called while the carrier is running. + * Must be called from within a timeslot callback. + */ +void lf_gap_send_start(void); + +/** + * Send a single command bit. + * Leaves the carrier on after the write gap. + * Must be called from within a timeslot callback. + * + * @param bit 0 or 1. + */ +void lf_gap_send_bit(uint8_t bit); + +/** + * Send a 32-bit word MSB-first as gap-encoded bits. + * Must be called from within a timeslot callback. + */ +void lf_gap_send_u32(uint32_t word); + +/** + * Send an N-bit value MSB-first. + * Must be called from within a timeslot callback. + * + * @param value Data to send. + * @param nbits Number of bits (1–32). + */ +void lf_gap_send_bits(uint32_t value, uint8_t nbits); + +/** + * Poll for a gap on the receive side. + * + * Reads the current GPIOTE edge counter. If the counter has not advanced + * since the last call (i.e. no edge has arrived), and the elapsed time + * exceeds GAP_DETECT_TIMEOUT_TC carrier cycles, returns true and writes + * the gap duration into *gap_tc. + * + * Caller is responsible for resetting the counter before calling. + * + * @param last_count Counter value at the previous edge (or last poll). + * @param gap_tc Output: duration of the detected gap in carrier cycles. + * @return true if a gap was detected. + */ +bool lf_gap_detect(uint32_t last_count, uint32_t *gap_tc); + +#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 556bef45..94709bc2 100644 --- a/firmware/application/src/rfid/reader/lf/lf_reader_main.c +++ b/firmware/application/src/rfid/reader/lf/lf_reader_main.c @@ -4,9 +4,9 @@ #include "bsp_time.h" #include "hex_utils.h" #include "lf_125khz_radio.h" +#include "lf_em4x05_data.h" #include "lf_reader_data.h" #include "protocols/em410x.h" -#include "protocols/ioprox.h" #include "protocols/hidprox.h" #include "protocols/t55xx.h" #include "protocols/viking.h" @@ -41,53 +41,20 @@ uint8_t scan_hidprox(uint8_t *data, uint8_t format_hint) { } /** - * @brief Search ioProx tag - * @param output 16 bytes ioprox_codec_t->data layout: version, facility code, card number, raw8 - * @return STATUS_LF_TAG_OK on success + * Search Viking tag */ -uint8_t scan_ioprox(uint8_t *data, uint8_t format_hint) { - if (ioprox_read(data, format_hint, g_timeout_readem_ms)) { +uint8_t scan_viking(uint8_t *uid) { + if (viking_read(uid, g_timeout_readem_ms)) { return STATUS_LF_TAG_OK; } return STATUS_LF_TAG_NO_FOUND; } /** - * @brief Decode raw8 data to structured ioProx format - * @param raw8 Input 8 bytes - * @param output 16 bytes ioprox_codec_t->data layout: version, facility code, card number, raw8 - * @return STATUS_SUCCESS on success - */ -uint8_t decode_ioprox_raw(uint8_t *raw8, uint8_t *output) { - if (ioprox_decode_raw_to_data(raw8, output)) { - return STATUS_SUCCESS; - } - return STATUS_CMD_ERR; -} - -/** - * @brief Encode ioProx parameters to structured ioProx format - * @param ver Version byte - * @param fc Facility code byte - * @param cn Card number (16-bit) - * @param out 16 bytes ioprox_codec_t->data layout: version, facility code, card number, raw8 - * @return STATUS_SUCCESS on success + * Search EM4x05 / EM4x69 tag (reader-talk-first) */ -uint8_t encode_ioprox_params(uint8_t ver, uint8_t fc, uint16_t cn, uint8_t *out) { - if (ioprox_encode_params_to_data(ver, fc, cn, out)) { - return STATUS_SUCCESS; - } - return STATUS_CMD_ERR; -} - -/** - * Search Viking tag - */ -uint8_t scan_viking(uint8_t *uid) { - if (viking_read(uid, g_timeout_readem_ms)) { - return STATUS_LF_TAG_OK; - } - return STATUS_LF_TAG_NO_FOUND; +uint8_t scan_em4x05(em4x05_data_t *out) { + return lf_em4x05_scan(out); } /** @@ -159,22 +126,6 @@ uint8_t write_hidprox_to_t55xx(uint8_t format, uint32_t fc, uint64_t cn, uint32_ return write_t55xx(blks, blk_count, new_passwd, old_passwds, old_passwd_count); } -/** - * Write ioprox card data to t55xx - */ -uint8_t write_ioprox_to_t55xx(uint8_t *card_data, uint8_t *new_passwd, uint8_t *old_passwds, uint8_t old_passwd_count) { - // Prepare T5577 block array: index 0 = config word, 1-2 = data blocks - uint32_t blks[3] = {0x00}; - - uint8_t blk_count = ioprox_t55xx_writer(card_data, blks); - - if (blk_count == 0) { - return STATUS_PAR_ERR; - } - - return write_t55xx(blks, blk_count, new_passwd, old_passwds, old_passwd_count); -} - /** * Write viking card data to t55xx */ 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 c975d139..52286795 100644 --- a/firmware/application/src/rfid/reader/lf/lf_reader_main.h +++ b/firmware/application/src/rfid/reader/lf/lf_reader_main.h @@ -5,17 +5,15 @@ #include "app_status.h" #include "lf_125khz_radio.h" +#include "lf_em4x05_data.h" #include "lf_reader_data.h" void set_scan_tag_timeout(uint32_t ms); uint8_t scan_em410x(uint8_t *uid); -uint8_t scan_ioprox(uint8_t *uid, uint8_t format_hint); -uint8_t decode_ioprox_raw(uint8_t *raw8, uint8_t *output); -uint8_t encode_ioprox_params(uint8_t ver, uint8_t fc, uint16_t cn, uint8_t *out); uint8_t scan_hidprox(uint8_t *uid, uint8_t format_hint); uint8_t scan_viking(uint8_t *uid); +uint8_t scan_em4x05(em4x05_data_t *out); uint8_t write_em410x_to_t55xx(uint8_t *uid, uint8_t *newkey, uint8_t *old_keys, uint8_t old_key_count); uint8_t write_em410x_electra_to_t55xx(uint8_t *uid, uint8_t *newkey, uint8_t *old_keys, uint8_t old_key_count); uint8_t write_hidprox_to_t55xx(uint8_t format, uint32_t fc, uint64_t cn, uint32_t il, uint32_t oem, uint8_t *new_passwd, uint8_t *old_passwds, uint8_t old_passwd_count); -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); diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index c9818a56..9c13697d 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -631,70 +631,6 @@ def args_parser(self) -> ArgumentParserNoExit: def on_exec(self, args: argparse.Namespace): raise NotImplementedError() -class LFIOProxIdArgsUnit(DeviceRequiredUnit): - """ - IOProx identity arguments: - --ver version (0-255) - --fc facility (0-255) - --cn card number (0-65535) - --raw8 raw 8 bytes hex, e.g. 007854E03A5D65AB - """ - @staticmethod - def add_card_arg(parser: ArgumentParserNoExit, required=False): - parser.add_argument("--ver", type=int, required=False, help="ioProx version", metavar="") - parser.add_argument("--fc", type=str, required=False, help="ioProx facility code, e.g., 83 or 0x53", metavar="") - parser.add_argument("--cn", type=int, required=required, help="ioProx card number", metavar="") - parser.add_argument("--raw8", type=str, required=False, help="ioProx raw 8 bytes hex (e.g. 00AABBCCDDEEFF55)", metavar="") - return parser - - @staticmethod - def _check_u8(name: str, v: int): - if v < 0 or v > 0xFF: - raise ArgsParserError(f"{name} must be 0..255") - - @staticmethod - def _check_u16(name: str, v: int): - if v < 0 or v > 0xFFFF: - raise ArgsParserError(f"{name} must be 0..65535") - - @staticmethod - def parse_raw8(raw8: str) -> bytes: - s = raw8.replace(" ", "").replace("0x", "").strip() - b = bytes.fromhex(s) - if len(b) != 8: - raise ArgsParserError("ioProx --raw must be exactly 8 bytes (16 hex chars), e.g. 007854E03A5D65AB") - return b - - @staticmethod - def checksum5(b1, b2, b3, b4, b5) -> int: - return (0xFF - ((b1 + b2 + b3 + b4 + b5) & 0xFF)) & 0xFF - - def before_exec(self, args: argparse.Namespace): - if not super().before_exec(args): - return False - - # validate if provided - if args.ver is not None: - self._check_u8("version", args.ver) - if args.fc is not None: - val = int(args.fc, 0) - self._check_u8("facility", val) - args.fc = val - if args.cn is not None: - self._check_u16("card number", args.cn) - - # if raw is present, validate it - if args.raw8 is not None: - self.parse_raw8(args.raw8) - - return True - - -class LFIOProxReadArgsUnit(DeviceRequiredUnit): - @staticmethod - def add_card_arg(parser: ArgumentParserNoExit, required=False): - parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output") - return parser class LFVikingIdArgsUnit(DeviceRequiredUnit): @staticmethod @@ -754,9 +690,9 @@ def on_exec(self, args: argparse.Namespace): lf = root.subgroup("lf", "Low Frequency commands") lf_em = lf.subgroup("em", "EM commands") lf_em_410x = lf_em.subgroup("410x", "EM410x commands") +lf_em_4x05 = lf_em.subgroup("4x05", "EM4x05/EM4x69 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_viking = lf.subgroup("viking", "Viking commands") lf_generic = lf.subgroup("generic", "Generic commands") @@ -5567,6 +5503,7 @@ def on_exec(self, args: argparse.Namespace): print(f" OEM: {color_string((CG, oem))}") print(f" CN: {color_string((CG, cn))}") + @lf_hid_prox.command("write") class LFHIDProxWriteT55xx(LFHIDIdArgsUnit, ReaderRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: @@ -5652,116 +5589,6 @@ def on_exec(self, args: argparse.Namespace): print(f" OEM: {color_string((CG, oem))}") print(f" CN: {color_string((CG, cn))}") -@lf_ioprox.command("read") -class LFIOProxRead(LFIOProxReadArgsUnit, ReaderRequiredUnit): - def args_parser(self) -> ArgumentParserNoExit: - parser = ArgumentParserNoExit() - parser.description = "Scan ioProx tag and print version, facility, card number and raw" - 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") - 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()))}") - -@lf_ioprox.command("write") -class LFIOProxWriteT55xx(LFIOProxIdArgsUnit, ReaderRequiredUnit): - def args_parser(self) -> ArgumentParserNoExit: - parser = ArgumentParserNoExit() - parser.description = "Write ioProx card data to t55xx" - return self.add_card_arg(parser, required=False) - - def on_exec(self, args: argparse.Namespace): - # defaults - ver = args.ver if args.ver is not None else 1 - fc = args.fc if args.fc is not None else 0 - cn = args.cn if args.cn is not None else 0 - - # raw8 priority - if args.raw8 is not None: - 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) - raw8 = res[3] - - payload16 = struct.pack( - ">BBH8s4x", - ver & 0xFF, - fc & 0xFF, - cn & 0xFFFF, - raw8 - ) - - result = self.cmd.ioprox_write_to_t55xx(payload16) - - 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("Write done.") - -@lf_ioprox.command("econfig") -class LFIOProxEconfig(SlotIndexArgsAndGoUnit, LFIOProxIdArgsUnit): - def args_parser(self) -> ArgumentParserNoExit: - parser = ArgumentParserNoExit() - parser.description = "Set/Get emulated ioProx card id (stored in slot)" - self.add_slot_args(parser) - self.add_card_arg(parser, required=False) # SET when --cn or --raw present; GET otherwise - return parser - - def on_exec(self, args: argparse.Namespace): - do_set = (args.cn is not None) or (args.raw8 is not None) or (args.fc is not None) or (args.ver is not None) - - if do_set: - # warn if slot isn't ioProx - slotinfo = self.cmd.get_slot_info() - selected = SlotNumber.from_fw(self.cmd.get_active_slot()) - lf_tag_type = TagSpecificType(slotinfo[selected - 1]["lf"]) - if lf_tag_type != TagSpecificType.ioProx: - print(f"{color_string((CR, 'WARNING'))}: Slot type not set to IOProx.") - - # defaults - ver = args.ver if args.ver is not None else 1 - fc = args.fc if args.fc is not None else 0 - cn = args.cn if args.cn is not None else 0 - - # raw8 priority - if args.raw8 is not None: - 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) - raw8 = res[3] - - payload16 = struct.pack( - ">BBH8s4x", - ver & 0xFF, - fc & 0xFF, - cn & 0xFFFF, - raw8 - ) - - result = self.cmd.ioprox_set_emu_id(payload16) - - 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()))}") - - else: - # GET - ver, fc, cn, raw8, *futureuse = self.cmd.ioprox_get_emu_id() - 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()))}") @lf_viking.command("read") class LFVikingRead(ReaderRequiredUnit): @@ -5789,6 +5616,27 @@ def on_exec(self, args: argparse.Namespace): print(f" - Viking ID(8H): {id_hex} write done.") +@lf_em_4x05.command("read") +class LFEm4x05Read(ReaderRequiredUnit): + def args_parser(self) -> ArgumentParserNoExit: + parser = ArgumentParserNoExit() + parser.description = ( + "Scan EM4x05 or EM4x69 tag (reader-talk-first) and print config, UID" + ) + return parser + + def on_exec(self, args: argparse.Namespace): + (config, uid, uid_hi, is_em4x69) = self.cmd.em4x05_scan() + tag_label = "EM4x69" if is_em4x69 else "EM4x05" + print(f" Tag type : {color_string((CG, tag_label))}") + print(f" Config : {color_string((CG, f'{config:#010x}'))}") + if is_em4x69: + uid64 = (uid_hi << 32) | uid + print(f" UID (64) : {color_string((CG, f'{uid64:016x}'))}") + else: + print(f" UID : {color_string((CG, f'{uid:08x}'))}") + + @lf_generic.command("adcread") class LFADCGenericRead(ReaderRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: @@ -5991,12 +5839,6 @@ def on_exec(self, args: argparse.Namespace): if oem > 0: print(f" {'OEM:':40}{color_string((CG, oem))}") print(f" {'CN:':40}{color_string((CG, cn))}") - if lf_tag_type == TagSpecificType.ioProx: - ver, fc, cn, raw8, *futureuse = self.cmd.ioprox_get_emu_id() - print(f" {'Version:':40}{color_string((CG, ver))}") - print(f" {'Facility:':40}{color_string((CG, f'{fc} [0x{fc:02X}]'))}") - print(f" {'ID:':40}{color_string((CY, cn))}") - print(f" {'Raw:':40}{color_string((CY, raw8.hex().upper()))}") if lf_tag_type == TagSpecificType.Viking: id = self.cmd.viking_get_emu_id() print(f" {'ID:':40}{color_string((CY, id.hex().upper()))}") diff --git a/software/script/chameleon_cmd.py b/software/script/chameleon_cmd.py index f5417155..6bc1ac36 100644 --- a/software/script/chameleon_cmd.py +++ b/software/script/chameleon_cmd.py @@ -509,52 +509,6 @@ def hidprox_write_to_t55xx(self, id_bytes: bytes): data = struct.pack(f'!13s4s{4*len(old_keys)}s', id_bytes, new_key, b''.join(old_keys)) return self.device.send_cmd_sync(Command.HIDPROX_WRITE_TO_T55XX, data) - @expect_response(Status.LF_TAG_OK) - def ioprox_scan(self): - """ - Read ioProx (XSF): version, facility, number, raw. - """ - resp = self.device.send_cmd_sync(Command.IOPROX_SCAN) - if resp.status == Status.LF_TAG_OK: - resp.parsed = struct.unpack(">BBH8sBBBB", resp.data[:16]) - return resp - - @expect_response(Status.LF_TAG_OK) - def ioprox_write_to_t55xx(self, id_bytes: bytes): - """ - Write ioProx card data to a T55XX tag. - """ - if len(id_bytes) != 16: - raise ValueError("The ioProx id bytes length must equal 16") - - # Pack id_bytes (16), new_key (4), and all old_keys (4 each) into one buffer - fmt = f'!16s4s{4 * len(old_keys)}s' - data = struct.pack(fmt, id_bytes, new_key, b''.join(old_keys)) - return self.device.send_cmd_sync(Command.IOPROX_WRITE_TO_T55XX, data) - - @expect_response(Status.SUCCESS) - def ioprox_decode_raw(self, raw8_bytes): - """ - Send 8 raw card bytes to firmware and return 16-byte card data structure. - Response layout: [0]=ver, [1]=fc, [2..3]=cn, [4..11]=raw8, [12..15]=padding. - """ - resp = self.device.send_cmd_sync(Command.IOPROX_DECODE_RAW, data=raw8_bytes) - if resp.status == Status.SUCCESS: - resp.parsed = struct.unpack(">BBH8sBBBB", resp.data[:16]) - return resp - - @expect_response(Status.SUCCESS) - def ioprox_compose_id(self, ver, fc, cn): - """ - Encode ioProx parameters into a 16-byte card data structure via firmware. - Response layout: [0]=ver, [1]=fc, [2..3]=cn, [4..11]=raw8, [12..15]=padding. - """ - payload = struct.pack(">BBH", ver, fc, cn) - resp = self.device.send_cmd_sync(Command.IOPROX_COMPOSE_ID, data=payload) - if resp.status == Status.SUCCESS: - resp.parsed = struct.unpack(">BBH8sBBBB", resp.data[:16]) - return resp - @expect_response(Status.LF_TAG_OK) def viking_scan(self): """ @@ -580,6 +534,24 @@ def viking_write_to_t55xx(self, id_bytes: bytes): 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 em4x05_scan(self): + """ + Read an EM4x05 or EM4x69 tag (reader-talk-first). + + Response payload (13 bytes, big-endian): + config 4 bytes — block 0 configuration word + uid 4 bytes — EM4x05 block-15 UID (or EM4x69 uid_lo) + uid_hi 4 bytes — EM4x69 uid_hi (zero for plain EM4x05) + is_em4x69 1 byte — 1 if a 64-bit EM4x69 UID was read + + :return: parsed tuple (config, uid, uid_hi, is_em4x69) + """ + resp = self.device.send_cmd_sync(Command.EM4X05_SCAN) + if resp.status == Status.LF_TAG_OK: + resp.parsed = struct.unpack('!IIIB', resp.data[:13]) + return resp + @expect_response(Status.LF_TAG_OK) def adc_generic_read(self): """ @@ -768,28 +740,6 @@ def hidprox_get_emu_id(self): if resp.status == Status.SUCCESS: resp.parsed = struct.unpack('>BIBIBH', resp.data[:13]) return resp - - @expect_response(Status.SUCCESS) - def ioprox_set_emu_id(self, id: bytes): - """ - Set the card number emulated by ioProx. - - :param id_bytes: byte of the card number - :return: - """ - if len(id) != 16: - raise ValueError("The id bytes length must equal 16") - return self.device.send_cmd_sync(Command.IOPROX_SET_EMU_ID, id) - - @expect_response(Status.SUCCESS) - def ioprox_get_emu_id(self): - """ - Get the emulated ioProx card id - """ - resp = self.device.send_cmd_sync(Command.IOPROX_GET_EMU_ID) - if resp.status == Status.SUCCESS: - resp.parsed = struct.unpack(">BBH8sBBBB", resp.data[:16]) - return resp @expect_response(Status.SUCCESS) def viking_set_emu_id(self, id: bytes): diff --git a/software/script/chameleon_enum.py b/software/script/chameleon_enum.py index c159762d..813f63b8 100644 --- a/software/script/chameleon_enum.py +++ b/software/script/chameleon_enum.py @@ -84,10 +84,7 @@ class Command(enum.IntEnum): VIKING_SCAN = 3004 VIKING_WRITE_TO_T55XX = 3005 ADC_GENERIC_READ = 3009 - IOPROX_SCAN = 3010 - IOPROX_WRITE_TO_T55XX = 3011 - IOPROX_DECODE_RAW = 3012 - IOPROX_COMPOSE_ID = 3013 + EM4X05_SCAN = 3010 MF1_WRITE_EMU_BLOCK_DATA = 4000 HF14A_SET_ANTI_COLL_DATA = 4001 @@ -141,8 +138,6 @@ class Command(enum.IntEnum): HIDPROX_GET_EMU_ID = 5003 VIKING_SET_EMU_ID = 5004 VIKING_GET_EMU_ID = 5005 - IOPROX_SET_EMU_ID = 5008 - IOPROX_GET_EMU_ID = 5009 @enum.unique @@ -284,7 +279,7 @@ class TagSpecificType(enum.IntEnum): # FSK Tag-Talk-First 200 HIDProx = 200 - ioProx = 201 + # ioProx # AWID # Paradox @@ -367,8 +362,6 @@ def __str__(self): return "EM410X Electra" elif self == TagSpecificType.HIDProx: return "HIDProx" - elif self == TagSpecificType.ioProx: - return "ioProx" elif self == TagSpecificType.Viking: return "Viking" elif self == TagSpecificType.MIFARE_Mini: