diff --git a/CHANGELOG.md b/CHANGELOG.md index 27b559b..7dcacd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ All notable changes to this project will be documented in this file. This project uses the changelog in accordance with [keepchangelog](http://keepachangelog.com/). Please use this to write notable changes, which is not the same as git commit log... ## [unreleased][unreleased] + - Added PAC/Stanley LF protocol support: read, emulate and T55xx clone (@kevihiiin, @danieltwagner) + - Fix firmware application USB serial number (@taichunmin) - Added ioProx LF protocol support (read, emulate and T55xx clone) - Added `hf mfu nfcimport` to import Flipper Zero `.nfc` files into MFU/NTAG emulator slots, with `--amiibo` flag for automatic PWD/PACK derivation (@fmuk) - Added commands to dump and clone Mifare tags diff --git a/firmware/application/Makefile b/firmware/application/Makefile index 0e2b231..74531a5 100644 --- a/firmware/application/Makefile +++ b/firmware/application/Makefile @@ -35,8 +35,9 @@ 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/pac.c \ + $(PROJ_DIR)/rfid/nfctag/lf/protocols/ioprox.c \ $(PROJ_DIR)/rfid/nfctag/lf/protocols/viking.c \ $(PROJ_DIR)/rfid/nfctag/lf/protocols/wiegand.c \ $(PROJ_DIR)/utils/dataframe.c \ @@ -341,13 +342,16 @@ 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_generic.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_pac_data.c \ + $(PROJ_DIR)/rfid/reader/lf/lf_ioprox_data.c \ $(PROJ_DIR)/rfid/reader/lf/lf_viking_data.c \ - $(PROJ_DIR)/rfid/reader/lf/lf_reader_generic.c \ INC_FOLDERS +=\ ${PROJ_DIR}/rfid/reader/ \ diff --git a/firmware/application/src/app_cmd.c b/firmware/application/src/app_cmd.c index a8ada47..c020a3a 100644 --- a/firmware/application/src/app_cmd.c +++ b/firmware/application/src/app_cmd.c @@ -14,6 +14,12 @@ #include "settings.h" #include "delayed_reset.h" #include "netdata.h" +#if defined(PROJECT_CHAMELEON_ULTRA) +#include "bsp_wdt.h" +#include "lf_reader_generic.h" +#include "lf_em4x05_data.h" +#endif +#include "nfc_14a.h" #define NRF_LOG_MODULE_NAME app_cmd @@ -802,6 +808,15 @@ static data_frame_tx_t *cmd_processor_viking_scan(uint16_t cmd, uint16_t status, return data_frame_make(cmd, STATUS_LF_TAG_OK, sizeof(card_buffer), card_buffer); } +static data_frame_tx_t *cmd_processor_pac_scan(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { + uint8_t card_id[8] = {0x00}; + status = scan_pac(card_id); + 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_id), card_id); +} + static data_frame_tx_t *cmd_processor_viking_write_to_t55xx(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { typedef struct { uint8_t id[4]; @@ -817,6 +832,20 @@ 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) { + typedef struct { + uint8_t id[LF_PAC_TAG_ID_SIZE]; + uint8_t new_key[4]; + uint8_t old_keys[4]; + } 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) { + 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)); + return data_frame_make(cmd, status, 0, NULL); +} + #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) { @@ -1066,6 +1095,26 @@ static data_frame_tx_t *cmd_processor_viking_get_emu_id(uint16_t cmd, uint16_t s return data_frame_make(cmd, STATUS_SUCCESS, LF_VIKING_TAG_ID_SIZE, buffer->buffer); } +static data_frame_tx_t *cmd_processor_pac_set_emu_id(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { + if (length != LF_PAC_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_PAC); + memcpy(buffer->buffer, data, LF_PAC_TAG_ID_SIZE); + tag_emulation_load_by_buffer(TAG_TYPE_PAC, false); + return data_frame_make(cmd, STATUS_SUCCESS, 0, NULL); +} + +static data_frame_tx_t *cmd_processor_pac_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_PAC) { + return data_frame_make(cmd, STATUS_PAR_ERR, 0, data); + } + tag_data_buffer_t *buffer = get_buffer_by_tag_type(TAG_TYPE_PAC); + return data_frame_make(cmd, STATUS_SUCCESS, LF_PAC_TAG_ID_SIZE, buffer->buffer); +} + static nfc_tag_14a_coll_res_reference_t *get_coll_res_data(bool write) { nfc_tag_14a_coll_res_reference_t *info; tag_slot_specific_type_t tag_types; @@ -1729,6 +1778,117 @@ static data_frame_tx_t *cmd_processor_mf0_get_emulator_config(uint16_t cmd, uint * (cmd -> processor) function map, the map struct is: * cmd code before process cmd processor after process */ +#if defined(PROJECT_CHAMELEON_ULTRA) +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); + } + 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); +} + +static data_frame_tx_t *cmd_processor_lf_sniff(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { + /* Optional 2-byte big-endian timeout in ms from host (default 2000ms) */ + uint32_t timeout_ms = 2000; + if (length >= 2) { + timeout_ms = ((uint32_t)data[0] << 8) | data[1]; + if (timeout_ms == 0 || timeout_ms > 10000) timeout_ms = 2000; + } + + static uint8_t sniff_buf[LF_SNIFF_MAX_SAMPLES]; + size_t outlen = 0; + raw_read_to_buffer(sniff_buf, LF_SNIFF_MAX_SAMPLES, timeout_ms, &outlen); + + if (outlen == 0) { + return data_frame_make(cmd, STATUS_LF_TAG_NO_FOUND, 0, NULL); + } + return data_frame_make(cmd, STATUS_LF_TAG_OK, (uint16_t)outlen, sniff_buf); +} +#define HF_SNIFF_BUF_SIZE 3800 /* leave room for USB framing */ +#define HF_SNIFF_MAX_FRAMES 200 + +static uint8_t m_sniff_buf[HF_SNIFF_BUF_SIZE]; +static uint16_t m_sniff_buf_len = 0; +static bool m_sniff_active = false; +static uint16_t m_sniff_cb_count = 0; /* debug: total callback invocations */ + +static void hf14a_sniff_frame_cb(const uint8_t *data, uint16_t szBits) { + m_sniff_cb_count++; /* count even if buffer full or inactive */ + if (!m_sniff_active) return; + uint16_t szBytes = (szBits + 7) / 8; + /* Check space: 2 bytes header + data */ + if (m_sniff_buf_len + 2 + szBytes > HF_SNIFF_BUF_SIZE) return; + /* Write bit count big-endian */ + m_sniff_buf[m_sniff_buf_len++] = (szBits >> 8) & 0xFF; + m_sniff_buf[m_sniff_buf_len++] = szBits & 0xFF; + /* Write frame bytes */ + memcpy(&m_sniff_buf[m_sniff_buf_len], data, szBytes); + m_sniff_buf_len += szBytes; +} + +static data_frame_tx_t *cmd_processor_hf14a_sniff(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { + /* Optional 2-byte big-endian timeout in ms (default 5000ms) */ + uint32_t timeout_ms = 5000; + if (length >= 2) { + timeout_ms = ((uint32_t)data[0] << 8) | data[1]; + if (timeout_ms == 0 || timeout_ms > 30000) timeout_ms = 5000; + } + + /* Reload active slot data before sniffing. + * The NFCT anti-collision response is built from m_tag_information which + * points into the shared tag data buffer. After a slot switch the buffer + * may still contain the previous slot's UID if the FDS async load has not + * completed. A forced reload here ensures the correct UID is presented + * during the sniff session. + * A short settle delay follows to allow the reload to complete before + * the first field detection can trigger the anti-collision path. */ + tag_emulation_load_data(); + bsp_delay_ms(100); + + /* Install sniff callback into the already-running tag emulation stack. + * Do NOT call tag_mode_enter() or sense_switch() here — those reinit + * NFCT and wipe the anti-collision data, breaking the emulation. + * The device must already be in emulator mode (hw mode --emulator) + * with a slot active before running this command. */ + m_sniff_buf_len = 0; + m_sniff_cb_count = 0; + m_sniff_active = true; + nfc_tag_14a_set_sniff_cb(hf14a_sniff_frame_cb); + + /* Wait for duration, yielding each ms so USB stack stays alive. + * Feed watchdog every iteration — WDT timeout is 5000ms and the + * main loop cannot feed it while we are blocking here. */ + autotimer *p_at = bsp_obtain_timer(0); + while (NO_TIMEOUT_1MS(p_at, timeout_ms)) { + bsp_delay_ms(1); + bsp_wdt_feed(); + } + bsp_return_timer(p_at); + + /* Remove callback and restore normal sense state */ + m_sniff_active = false; + nfc_tag_14a_clear_sniff_cb(); + tag_emulation_sense_run(); /* restore slot-based sense state */ + + if (m_sniff_buf_len == 0) { + return data_frame_make(cmd, STATUS_HF_TAG_NO, 0, NULL); + } + return data_frame_make(cmd, STATUS_SUCCESS, m_sniff_buf_len, m_sniff_buf); +} + +#endif + static cmd_data_map_t m_data_cmd_map[] = { { DATA_CMD_GET_APP_VERSION, NULL, cmd_processor_get_app_version, NULL }, { DATA_CMD_CHANGE_DEVICE_MODE, NULL, cmd_processor_change_device_mode, NULL }, @@ -1798,6 +1958,8 @@ static cmd_data_map_t m_data_cmd_map[] = { { 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_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_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 }, @@ -1808,6 +1970,9 @@ static cmd_data_map_t m_data_cmd_map[] = { { DATA_CMD_IOPROX_DECODE_RAW, NULL, cmd_processor_ioprox_decode_raw, NULL }, { DATA_CMD_IOPROX_COMPOSE_ID, NULL, cmd_processor_ioprox_compose_id, NULL }, + { DATA_CMD_EM4X05_SCAN, before_reader_run, cmd_processor_em4x05_scan, NULL }, + { DATA_CMD_LF_SNIFF, before_reader_run, cmd_processor_lf_sniff, NULL }, + { DATA_CMD_HF14A_SNIFF, NULL, cmd_processor_hf14a_sniff, NULL }, #endif @@ -1860,6 +2025,8 @@ static cmd_data_map_t m_data_cmd_map[] = { { 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 }, + { DATA_CMD_PAC_SET_EMU_ID, NULL, cmd_processor_pac_set_emu_id, NULL }, + { DATA_CMD_PAC_GET_EMU_ID, NULL, cmd_processor_pac_get_emu_id, NULL }, }; data_frame_tx_t *cmd_processor_get_device_capabilities(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { diff --git a/firmware/application/src/app_status.h b/firmware/application/src/app_status.h index 653336d..71762b3 100644 --- a/firmware/application/src/app_status.h +++ b/firmware/application/src/app_status.h @@ -19,6 +19,7 @@ ///////////////////////////////////////////////////////////////////// #define STATUS_LF_TAG_OK (0x40) // Some of the low -frequency cards are successful! #define STATUS_LF_TAG_NO_FOUND (0x41) // Can't search for valid LF tags +#define STATUS_LF_TAG_LOGIN_REQUIRED (0x42) // Tag requires LOGIN before read ///////////////////////////////////////////////////////////////////// // other status diff --git a/firmware/application/src/data_cmd.h b/firmware/application/src/data_cmd.h index 51ac5fc..ec89711 100644 --- a/firmware/application/src/data_cmd.h +++ b/firmware/application/src/data_cmd.h @@ -78,6 +78,7 @@ #define DATA_CMD_HF14A_GET_CONFIG (2200) #define DATA_CMD_HF14A_SET_CONFIG (2201) +#define DATA_CMD_HF14A_SNIFF (2020) // // ****************************************************************** @@ -93,6 +94,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 (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) @@ -170,7 +173,13 @@ #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_PAC_SET_EMU_ID (5006) +#define DATA_CMD_PAC_GET_EMU_ID (5007) #define DATA_CMD_IOPROX_SET_EMU_ID (5008) #define DATA_CMD_IOPROX_GET_EMU_ID (5009) +#define DATA_CMD_EM4X05_SCAN (3030) +#define DATA_CMD_EM4X05_READSNIFF (3032) +#define DATA_CMD_LF_SNIFF (3031) + #endif diff --git a/firmware/application/src/rfid/nfctag/hf/nfc_14a.c b/firmware/application/src/rfid/nfctag/hf/nfc_14a.c index 7f208ac..d6fd0f6 100644 --- a/firmware/application/src/rfid/nfctag/hf/nfc_14a.c +++ b/firmware/application/src/rfid/nfctag/hf/nfc_14a.c @@ -59,6 +59,17 @@ const uint16_t ats_fsdi_table[] = { static volatile bool m_is_responded = false; // Receiving buffer static uint8_t m_nfc_rx_buffer[MAX_NFC_RX_BUFFER_SIZE] = { 0x00 }; + +/* Optional sniff callback — fires for every received frame */ +static nfc_tag_14a_sniff_cb_t m_sniff_cb = NULL; + +void nfc_tag_14a_set_sniff_cb(nfc_tag_14a_sniff_cb_t cb) { + m_sniff_cb = cb; +} + +void nfc_tag_14a_clear_sniff_cb(void) { + m_sniff_cb = NULL; +} static uint8_t m_nfc_tx_buffer[MAX_NFC_TX_BUFFER_SIZE] = { 0x00 }; // The N -secondary connection needs to use SAK, when the "third 'bit' in SAK is 1 is 1, the logo UID is incomplete static uint8_t m_uid_incomplete_sak[] = { 0x04, 0xda, 0x17 }; @@ -326,6 +337,11 @@ void nfc_tag_14a_data_process(uint8_t *p_data) { // Because of this error receiving event caused by this possible interference return; } + + /* Sniff hook — fire before any tag response logic */ + if (m_sniff_cb != NULL) { + m_sniff_cb(p_data, szDataBits); + } // Manually draw frame, separate data and strange school inspection #if !NFC_TAG_14A_RX_PARITY_AUTO_DEL_ENABLE if (szDataBits >= 9) { diff --git a/firmware/application/src/rfid/nfctag/hf/nfc_14a.h b/firmware/application/src/rfid/nfctag/hf/nfc_14a.h index e38b250..0d92022 100644 --- a/firmware/application/src/rfid/nfctag/hf/nfc_14a.h +++ b/firmware/application/src/rfid/nfctag/hf/nfc_14a.h @@ -82,6 +82,14 @@ typedef struct { // Communication reception function that needs to be implemented typedef void (*nfc_tag_14a_reset_handler_t)(void); + +/* Sniff callback — called for every received frame before the tag handler. + * data : raw frame bytes (after parity strip) + * szBits : number of bits received */ +typedef void (*nfc_tag_14a_sniff_cb_t)(const uint8_t *data, uint16_t szBits); + +void nfc_tag_14a_set_sniff_cb(nfc_tag_14a_sniff_cb_t cb); +void nfc_tag_14a_clear_sniff_cb(void); typedef void (*nfc_tag_14a_state_handler_t)(uint8_t *data, uint16_t szBits); typedef nfc_tag_14a_coll_res_reference_t *(*nfc_tag_14a_coll_handler_t)(void); diff --git a/firmware/application/src/rfid/nfctag/lf/lf_tag_em.c b/firmware/application/src/rfid/nfctag/lf/lf_tag_em.c index 6193ad1..2a83193 100644 --- a/firmware/application/src/rfid/nfctag/lf/lf_tag_em.c +++ b/firmware/application/src/rfid/nfctag/lf/lf_tag_em.c @@ -5,11 +5,13 @@ #include "bsp_delay.h" #include "fds_util.h" #include "nrf_gpio.h" +#include "nrf_soc.h" #include "nrfx_lpcomp.h" #include "nrfx_pwm.h" #include "protocols/em410x.h" #include "protocols/hidprox.h" #include "protocols/ioprox.h" +#include "protocols/pac.h" #include "protocols/viking.h" #include "syssleep.h" #include "tag_emulation.h" @@ -136,6 +138,23 @@ static void pwm_init(void) { } static void lf_sense_enable(void) { + // PWM bit timing divides HFCLK by a fixed ratio. On HFINT (64 MHz RC, + // ±1.5% at 25°C after factory trim, wider over temperature) this gives a + // chip-to-chip spread that NRZ readers — which see cumulative error across + // runs of same-polarity bits with no intra-run resync — reject even when + // Manchester/FSK readers don't. Holding HFXO brings the PWM clock to + // ±40 ppm. We can't lock to the reader's carrier (tag-mode antenna taps + // on this board are envelope-only), so this is as good as it gets. + // + // Paired release in lf_sense_disable(). SD reference-counts HFXO requests, + // so this coexists with BLE. Both functions run from thread context + // (tag_mode_enter/tag_emulation_sense_end) where SVCs are safe. + sd_clock_hfclk_request(); + uint32_t hfclk_running = 0; + while (!hfclk_running) { + sd_clock_hfclk_is_running(&hfclk_running); + } + lpcomp_init(); pwm_init(); // use precise hardware pwm to broadcast card id if (is_lf_field_exists()) { @@ -148,6 +167,7 @@ static void lf_sense_disable(void) { nrfx_lpcomp_uninit(); m_pwm_seq = NULL; m_is_lf_emulating = false; + sd_clock_hfclk_release(); } static enum { @@ -224,6 +244,15 @@ int lf_tag_data_loadcb(tag_specific_type_t type, tag_data_buffer_t *buffer) { return LF_VIKING_TAG_ID_SIZE; } + if (type == TAG_TYPE_PAC && buffer->length >= LF_PAC_TAG_ID_SIZE) { + m_tag_type = type; + void *codec = pac.alloc(); + m_pwm_seq = pac.modulator(codec, buffer->buffer); + pac.free(codec); + NRF_LOG_INFO("load lf pac data finish."); + return LF_PAC_TAG_ID_SIZE; + } + NRF_LOG_ERROR("no valid data exists in buffer for tag type: %d.", type); return 0; } @@ -346,3 +375,13 @@ bool lf_tag_viking_data_factory(uint8_t slot, tag_specific_type_t tag_type) { uint8_t tag_id[4] = {0xDE, 0xAD, 0xBE, 0xEF}; return lf_tag_data_factory(slot, tag_type, tag_id, sizeof(tag_id)); } + +int lf_tag_pac_data_savecb(tag_specific_type_t type, tag_data_buffer_t *buffer) { + return m_tag_type == TAG_TYPE_PAC ? LF_PAC_TAG_ID_SIZE : 0; +} + +bool lf_tag_pac_data_factory(uint8_t slot, tag_specific_type_t tag_type) { + // default id: 8 ASCII bytes + uint8_t tag_id[8] = {'C', 'A', 'R', 'D', '0', '0', '0', '1'}; + return lf_tag_data_factory(slot, tag_type, tag_id, sizeof(tag_id)); +} diff --git a/firmware/application/src/rfid/nfctag/lf/lf_tag_em.h b/firmware/application/src/rfid/nfctag/lf/lf_tag_em.h index 25163d3..d9fe8d9 100644 --- a/firmware/application/src/rfid/nfctag/lf/lf_tag_em.h +++ b/firmware/application/src/rfid/nfctag/lf/lf_tag_em.h @@ -10,6 +10,7 @@ #define LF_IOPROX_TAG_ID_SIZE 16 #define LF_HIDPROX_TAG_ID_SIZE 13 #define LF_VIKING_TAG_ID_SIZE 4 +#define LF_PAC_TAG_ID_SIZE 8 void lf_tag_125khz_sense_switch(bool enable); int lf_tag_data_loadcb(tag_specific_type_t type, tag_data_buffer_t *buffer); @@ -21,4 +22,6 @@ int lf_tag_ioprox_data_savecb(tag_specific_type_t type, tag_data_buffer_t *buffe bool lf_tag_ioprox_data_factory(uint8_t slot, tag_specific_type_t tag_type); int lf_tag_viking_data_savecb(tag_specific_type_t type, tag_data_buffer_t *buffer); bool lf_tag_viking_data_factory(uint8_t slot, tag_specific_type_t tag_type); +int lf_tag_pac_data_savecb(tag_specific_type_t type, tag_data_buffer_t *buffer); +bool lf_tag_pac_data_factory(uint8_t slot, tag_specific_type_t tag_type); bool is_lf_field_exists(void); diff --git a/firmware/application/src/rfid/nfctag/lf/protocols/pac.c b/firmware/application/src/rfid/nfctag/lf/protocols/pac.c new file mode 100644 index 0000000..629cc38 --- /dev/null +++ b/firmware/application/src/rfid/nfctag/lf/protocols/pac.c @@ -0,0 +1,400 @@ +#include "pac.h" + +#include +#include + +#include "nordic_common.h" +#include "nrf_pwm.h" +#include "protocols.h" +#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. +// With SAADC sampling at 1 sample per carrier cycle, 32 samples = 1 bit. +#define PAC_RF_PER_BIT 32 +#define PAC_HALF_BIT 16 // Half-bit for rounding interval → nbits +#define PAC_MAX_BITS_RUN 20 // Max consecutive same-polarity bits we accept + +// PAC frame is exactly 128 bits on T55xx (4 blocks × 32 bits): +// 8-bit sync marker (0xFF) + 12 × 10-bit UART frames = 128 bits +#define PAC_FRAME_BITS 128 +#define PAC_PREAMBLE_BITS 19 + +// Preamble: 1111111100100000010 (19 bits) = 0x7F902 +#define PAC_PREAMBLE 0x7F902UL +#define PAC_PREAMBLE_INV 0x006FDUL // Bitwise inverse masked to 19 bits + +#define PAC_UART_FRAME_BITS 10 +#define PAC_PAYLOAD_BYTES 12 // STX + '2' + '0' + 8 card ID + XOR checksum +#define PAC_STX 0x02 + +// ADC demodulation: spike-clipping + threshold with dead zone. +// +// Signal has 3 amplitude zones: +// NRZ low (~500-2000 ADC) — tag load modulation "0" state +// NRZ high (~4000-6000) — tag load modulation "1" state +// Spikes (~10k-14k) — LC ringing at NRZ transitions (8-20 cycles wide) +// +// Prescan (128 samples) finds the NRZ floor (raw_min), then spike_cap +// = max(raw_min * SPIKE_MULT, MIN_SPIKE_CAP) clips spikes while preserving +// the NRZ high level. MIN_SPIKE_CAP ensures spike_cap is never below the +// NRZ high level, even when raw_min correctly captures NRZ low. +#define PAC_PRESCAN_SAMPLES 128 // ~1ms: raw min detection +#define PAC_WARMUP_SAMPLES 600 // ~5ms: threshold calibration on clipped signal +#define PAC_SPIKE_MULT 3 // Clip at 3× floor +#define PAC_MIN_SPIKE_CAP 8000 // Floor: preserves NRZ high (~5000) always +#define PAC_THRESH_FUZZ 75 // Dead zone: 25%-75% of clipped range +// Auto-recalibrate if no frame found within this many Phase 3 samples. +// ~5 frame periods = 5 × 128 bits × 32 samples/bit = 20480 samples (~164ms). +// Gives ~3 calibration attempts in a 500ms scan window. +#define PAC_RECAL_SAMPLES 20480 + +typedef struct { + // NRZ shift register (128 bits) + uint64_t raw_hi; // upper 64 bits + uint64_t raw_lo; // lower 64 bits + bool polarity; // current NRZ level (toggled on each edge) + uint16_t bit_count; // total bits shifted in (capped at PAC_FRAME_BITS) + uint8_t card_id[PAC_DATA_SIZE]; + + // ADC → NRZ demodulation state (spike-clip + threshold with dead zone) + uint32_t total_samples; // total samples processed + int16_t raw_min; // minimum raw sample seen (for spike cap) + int32_t spike_cap; // clip level + int16_t clip_max; // max of clipped samples during warmup + int16_t clip_min; // min of clipped samples during warmup + int16_t thresh_high; // above this → bit=1 + int16_t thresh_low; // below this → bit=0, between → keep previous + bool adc_state; // current demodulated binary level + bool has_signal; // true after first threshold crossing + uint32_t sample_count; // samples since last transition + uint32_t decode_samples; // Phase 3 samples since last calibration +} pac_codec; + +// Shift one bit into the 128-bit register. +static void shift_bit(pac_codec *d, bool bit) { + d->raw_hi = (d->raw_hi << 1) | (d->raw_lo >> 63); + d->raw_lo = (d->raw_lo << 1) | (bit ? 1 : 0); +} + +// Extract a single bit from the 128-bit register. +// Position 0 = MSB of raw_hi (oldest), position 127 = LSB of raw_lo (newest). +static bool get_bit(pac_codec *d, uint16_t pos) { + if (pos < 64) { + return (d->raw_hi >> (63 - pos)) & 1; + } + return (d->raw_lo >> (127 - pos)) & 1; +} + +// Decode a 10-bit UART frame at bit position 'start'. +// Frame: start(0) + 7 data bits LSB-first + odd parity + stop(1). +static int decode_uart_byte(pac_codec *d, uint16_t start, bool inverted) { + #define RD(pos) (inverted ? !get_bit(d, (pos)) : get_bit(d, (pos))) + + if (RD(start)) { + return -1; + } + + uint8_t byte_val = 0; + uint8_t ones = 0; + for (int i = 0; i < 7; i++) { + if (RD(start + 1 + i)) { + byte_val |= (1 << i); + ones++; + } + } + + if (RD(start + 8)) { + ones++; + } + if ((ones & 1) == 0) { + return -1; + } + + if (!RD(start + 9)) { + return -1; + } + + #undef RD + return byte_val; +} + +// Check if the 128-bit register contains a valid PAC frame. +static bool try_decode_frame(pac_codec *d, bool inverted) { + uint32_t preamble = 0; + for (int i = 0; i < PAC_PREAMBLE_BITS; i++) { + preamble = (preamble << 1) | (get_bit(d, i) ? 1 : 0); + } + + uint32_t expected = inverted ? PAC_PREAMBLE_INV : PAC_PREAMBLE; + if (preamble != expected) { + return false; + } + + uint8_t decoded[PAC_PAYLOAD_BYTES]; + for (int i = 0; i < PAC_PAYLOAD_BYTES; i++) { + uint16_t frame_start = 8 + i * PAC_UART_FRAME_BITS; + int val = decode_uart_byte(d, frame_start, inverted); + if (val < 0) { + return false; + } + decoded[i] = (uint8_t)val; + } + + if (decoded[0] != PAC_STX) { + return false; + } + + uint8_t xor_check = 0; + for (int i = 3; i < 3 + PAC_DATA_SIZE; i++) { + xor_check ^= decoded[i]; + } + if (xor_check != decoded[11]) { + return false; + } + + memcpy(d->card_id, &decoded[3], PAC_DATA_SIZE); + return true; +} + +// Process a demodulated NRZ edge interval (in samples = carrier cycles). +static bool pac_process_interval(pac_codec *d, uint32_t interval) { + uint32_t nbits = (interval + PAC_HALF_BIT) / PAC_RF_PER_BIT; + + if (nbits < 1 || nbits > PAC_MAX_BITS_RUN) { + d->raw_hi = 0; + d->raw_lo = 0; + d->polarity = false; + d->bit_count = 0; + return false; + } + + for (uint32_t i = 0; i < nbits; i++) { + shift_bit(d, d->polarity); + if (d->bit_count < PAC_FRAME_BITS) { + d->bit_count++; + } + if (d->bit_count >= PAC_FRAME_BITS) { + if (try_decode_frame(d, false) || try_decode_frame(d, true)) { + return true; + } + } + } + + d->polarity = !d->polarity; + return false; +} + +static pac_codec *pac_alloc(void) { + pac_codec *codec = malloc(sizeof(pac_codec)); + return codec; +} + +static void pac_free(pac_codec *d) { + free(d); +} + +static uint8_t *pac_get_data(pac_codec *d) { + return d->card_id; +} + +static void pac_decoder_start(pac_codec *d, uint8_t format) { + memset(d, 0, sizeof(pac_codec)); + d->raw_min = 32767; // INT16_MAX: first sample updates it + d->spike_cap = 0x7FFFFFFF; // INT32_MAX: no capping until prescan completes + d->clip_max = -32768; // INT16_MIN: first clipped sample updates it + d->clip_min = 32767; // INT16_MAX: first clipped sample updates it +} + +// Feed a raw ADC sample (one per carrier cycle at 125kHz). +// Spike-clipping + threshold with dead zone (PM3 nrzRawDemod style). +static bool pac_decoder_feed(pac_codec *d, uint16_t raw_sample) { + int16_t sample = (int16_t)raw_sample; + d->total_samples++; + + // Phase 1: Prescan — track raw minimum to find the NRZ floor. + if (d->total_samples <= PAC_PRESCAN_SAMPLES) { + if (sample < d->raw_min && sample > 0) { + d->raw_min = sample; + } + if (d->total_samples == PAC_PRESCAN_SAMPLES) { + d->spike_cap = (int32_t)d->raw_min * PAC_SPIKE_MULT; + if (d->spike_cap < PAC_MIN_SPIKE_CAP) { + d->spike_cap = PAC_MIN_SPIKE_CAP; + } + } + return false; + } + + // Clip spikes: LC ringing transients are replaced with the cap level. + if (sample > d->spike_cap) { + sample = d->spike_cap; + } + + uint32_t warmup_samples = d->total_samples - PAC_PRESCAN_SAMPLES; + + // Phase 2: Warmup — track min/max of clipped samples to find NRZ levels. + if (warmup_samples <= PAC_WARMUP_SAMPLES) { + if (sample > d->clip_max) d->clip_max = sample; + if (sample < d->clip_min) d->clip_min = sample; + if (warmup_samples == PAC_WARMUP_SAMPLES) { + int16_t range = d->clip_max - d->clip_min; + d->thresh_high = d->clip_min + (range * PAC_THRESH_FUZZ) / 100; + d->thresh_low = d->clip_min + (range * (100 - PAC_THRESH_FUZZ)) / 100; + } + return false; + } + + // Phase 3: Per-sample threshold with dead zone. + // Auto-recalibrate if no frame found after enough decode samples — + // the one-shot calibration may have captured an unlucky NRZ segment. + d->decode_samples++; + if (d->decode_samples >= PAC_RECAL_SAMPLES) { + pac_decoder_start(d, 0); + return false; + } + d->sample_count++; + + bool new_state = d->adc_state; + if (sample >= d->thresh_high) { + new_state = true; + } else if (sample <= d->thresh_low) { + new_state = false; + } else { + return false; + } + + if (!d->has_signal) { + d->has_signal = true; + d->adc_state = new_state; + d->sample_count = 0; + return false; + } + + if (new_state == d->adc_state) { + return false; + } + + // Transition detected — process the interval + uint32_t interval = d->sample_count; + d->sample_count = 0; + d->adc_state = new_state; + + return pac_process_interval(d, interval); +} + +// --- Modulator (emulation) --- + +static nrf_pwm_values_wave_form_t m_pac_pwm_seq_vals[PAC_FRAME_BITS] = {}; + +static const nrf_pwm_sequence_t m_pac_pwm_seq = { + .values.p_wave_form = m_pac_pwm_seq_vals, + .length = NRF_PWM_VALUES_LENGTH(m_pac_pwm_seq_vals), + .repeats = 0, + .end_delay = 0, +}; + +// Build the 128-bit NRZ bitstream from 8-byte card ID. +// Frame: 0xFF sync (8 bits) + 12 × 10-bit UART frames = 128 bits. +// UART frame: start(0) + 7 data bits LSB-first + odd parity + stop(1). +// Payload bytes: STX(0x02), '2', '0', card_id[0..7], XOR checksum. +static void pac_build_bitstream(const uint8_t *card_id, uint8_t *bits_out) { + uint8_t payload[PAC_PAYLOAD_BYTES]; + payload[0] = PAC_STX; + payload[1] = '2'; + payload[2] = '0'; + memcpy(&payload[3], card_id, PAC_DATA_SIZE); + + // XOR checksum over card ID bytes (indices 3..10) + uint8_t xor_check = 0; + for (int i = 3; i < 3 + PAC_DATA_SIZE; i++) { + xor_check ^= payload[i]; + } + payload[11] = xor_check; + + int bit_pos = 0; + + // 8-bit sync marker: 0xFF (all ones) + for (int i = 0; i < 8; i++) { + bits_out[bit_pos++] = 1; + } + + // 12 UART frames + for (int f = 0; f < PAC_PAYLOAD_BYTES; f++) { + uint8_t byte_val = payload[f]; + + // Start bit (0) + bits_out[bit_pos++] = 0; + + // 7 data bits, LSB first + uint8_t ones = 0; + for (int i = 0; i < 7; i++) { + uint8_t bit = (byte_val >> i) & 1; + bits_out[bit_pos++] = bit; + ones += bit; + } + + // Odd parity: set so total ones (data + parity) is odd + uint8_t parity = (ones & 1) ? 0 : 1; + bits_out[bit_pos++] = parity; + + // Stop bit (1) + bits_out[bit_pos++] = 1; + } +} + +static const nrf_pwm_sequence_t *pac_modulator(pac_codec *d, uint8_t *buf) { + uint8_t bits[PAC_FRAME_BITS]; + pac_build_bitstream(buf, bits); + + // NRZ: output must be CONSTANT within each bit period (no mid-bit transition). + // Per nRF52840 PS: compare >= counter_top → pin held HIGH; compare = 0 → pin held LOW. + // Use compare = counter_top + 1 (not counter_top) to avoid the compare == counter_top + // boundary where a 1-tick output glitch may occur due to simultaneous compare-match + // and counter-wrap. Real PAC readers with hardware edge detection are sensitive to this; + // PM3's software NRZ demod is not (it averages over the bit period). + for (int i = 0; i < PAC_FRAME_BITS; i++) { + m_pac_pwm_seq_vals[i].channel_0 = bits[i] ? (PAC_RF_PER_BIT + 1) : 0; + m_pac_pwm_seq_vals[i].counter_top = PAC_RF_PER_BIT; + } + return &m_pac_pwm_seq; +} + +#define PAC_T55XX_BLOCK_COUNT 5 // 1 config + 4 data blocks + +uint8_t pac_t55xx_writer(uint8_t *data, uint32_t *blks) { + uint8_t bits[PAC_FRAME_BITS]; + pac_build_bitstream(data, bits); + + blks[0] = T5577_PAC_CONFIG; + for (int b = 0; b < 4; b++) { + uint32_t word = 0; + for (int i = 0; i < 32; i++) { + word = (word << 1) | bits[b * 32 + i]; + } + blks[b + 1] = word; + } + return PAC_T55XX_BLOCK_COUNT; +} + +const protocol pac = { + .tag_type = TAG_TYPE_PAC, + .data_size = PAC_DATA_SIZE, + .alloc = (codec_alloc)pac_alloc, + .free = (codec_free)pac_free, + .get_data = (codec_get_data)pac_get_data, + .modulator = (modulator)pac_modulator, + .decoder = + { + .start = (decoder_start)pac_decoder_start, + .feed = (decoder_feed)pac_decoder_feed, + }, +}; diff --git a/firmware/application/src/rfid/nfctag/lf/protocols/pac.h b/firmware/application/src/rfid/nfctag/lf/protocols/pac.h new file mode 100644 index 0000000..2dc7b69 --- /dev/null +++ b/firmware/application/src/rfid/nfctag/lf/protocols/pac.h @@ -0,0 +1,6 @@ +#pragma once + +#include "protocols.h" + +extern const protocol pac; +uint8_t pac_t55xx_writer(uint8_t *data, uint32_t *blks); diff --git a/firmware/application/src/rfid/nfctag/lf/protocols/t55xx.h b/firmware/application/src/rfid/nfctag/lf/protocols/t55xx.h index c46b561..c0383ba 100644 --- a/firmware/application/src/rfid/nfctag/lf/protocols/t55xx.h +++ b/firmware/application/src/rfid/nfctag/lf/protocols/t55xx.h @@ -75,6 +75,12 @@ extern "C" { T5577_PWD | \ (2 << T5577_MAXBLOCK_SHIFT)) +#define T5577_PAC_CONFIG ( \ + T5577_MODULATION_DIRECT | \ + T5577_BITRATE_RF_32 | \ + T5577_PWD | \ + (4 << T5577_MAXBLOCK_SHIFT)) + 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); diff --git a/firmware/application/src/rfid/nfctag/tag_base_type.h b/firmware/application/src/rfid/nfctag/tag_base_type.h index d88b0af..6a26cb0 100644 --- a/firmware/application/src/rfid/nfctag/tag_base_type.h +++ b/firmware/application/src/rfid/nfctag/tag_base_type.h @@ -43,6 +43,7 @@ typedef enum { // securakey // gallagher // PAC/Stanley + TAG_TYPE_PAC = 150, // Presco // Visa2000 // Viking @@ -107,7 +108,7 @@ typedef enum { } #define TAG_SPECIFIC_TYPE_LF_VALUES \ - TAG_TYPE_EM410X, TAG_TYPE_EM410X_ELECTRA, TAG_TYPE_HID_PROX, TAG_TYPE_IOPROX, TAG_TYPE_VIKING + TAG_TYPE_EM410X, TAG_TYPE_EM410X_ELECTRA, TAG_TYPE_PAC, TAG_TYPE_HID_PROX, TAG_TYPE_IOPROX, TAG_TYPE_VIKING #define TAG_SPECIFIC_TYPE_HF_VALUES \ TAG_TYPE_MIFARE_Mini, TAG_TYPE_MIFARE_1024, TAG_TYPE_MIFARE_2048, \ diff --git a/firmware/application/src/rfid/nfctag/tag_emulation.c b/firmware/application/src/rfid/nfctag/tag_emulation.c index 7773da6..66a23fd 100644 --- a/firmware/application/src/rfid/nfctag/tag_emulation.c +++ b/firmware/application/src/rfid/nfctag/tag_emulation.c @@ -94,6 +94,7 @@ static tag_base_handler_map_t tag_base_map[] = { {TAG_SENSE_LF, TAG_TYPE_HID_PROX, lf_tag_data_loadcb, lf_tag_hidprox_data_savecb, lf_tag_hidprox_data_factory, &m_tag_data_lf}, {TAG_SENSE_LF, TAG_TYPE_IOPROX, lf_tag_data_loadcb, lf_tag_ioprox_data_savecb, lf_tag_ioprox_data_factory, &m_tag_data_lf}, {TAG_SENSE_LF, TAG_TYPE_VIKING, lf_tag_data_loadcb, lf_tag_viking_data_savecb, lf_tag_viking_data_factory, &m_tag_data_lf}, + {TAG_SENSE_LF, TAG_TYPE_PAC, lf_tag_data_loadcb, lf_tag_pac_data_savecb, lf_tag_pac_data_factory, &m_tag_data_lf}, // MF1 tag emulation {TAG_SENSE_HF, TAG_TYPE_MIFARE_Mini, nfc_tag_mf1_data_loadcb, nfc_tag_mf1_data_savecb, nfc_tag_mf1_data_factory, &m_tag_data_hf}, {TAG_SENSE_HF, TAG_TYPE_MIFARE_1024, nfc_tag_mf1_data_loadcb, nfc_tag_mf1_data_savecb, nfc_tag_mf1_data_factory, &m_tag_data_hf}, diff --git a/firmware/application/src/rfid/nfctag/tag_emulation.h b/firmware/application/src/rfid/nfctag/tag_emulation.h index 9b3173a..f1e88c2 100644 --- a/firmware/application/src/rfid/nfctag/tag_emulation.h +++ b/firmware/application/src/rfid/nfctag/tag_emulation.h @@ -80,6 +80,7 @@ void tag_emulation_init(void); void tag_emulation_save(void); // Starting and ending of the emulation card +void tag_emulation_load_data(void); void tag_emulation_sense_run(void); void tag_emulation_sense_end(void); diff --git a/firmware/application/src/rfid/reader/lf/lf_125khz_radio.h b/firmware/application/src/rfid/reader/lf/lf_125khz_radio.h index f452ebf..f48f473 100644 --- a/firmware/application/src/rfid/reader/lf/lf_125khz_radio.h +++ b/firmware/application/src/rfid/reader/lf/lf_125khz_radio.h @@ -1,6 +1,11 @@ #pragma once #include "ble_main.h" +#include "nrfx_pwm.h" + +/* Exposed so lf_gap.c can stop the PWM and drive LF_ANT_DRIVER directly + * to create clean field gaps without relying on PWM pin release state. */ +extern nrfx_pwm_t m_pwm; void lf_125khz_radio_init(void); void lf_125khz_radio_uninit(void); 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 0000000..d948f38 --- /dev/null +++ b/firmware/application/src/rfid/reader/lf/lf_em4x05_data.c @@ -0,0 +1,346 @@ +#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(); + +#define EM4X05_CMD_BITS 9 +#define EM4X05_RESP_BITS 45 +#define EM4X05_ROWS 8 +#define EM4X05_COLS 4 +#define EM4X05_CB_SIZE 256 + +static inline uint8_t odd_parity4(uint8_t nibble) { + nibble ^= nibble >> 2; + nibble ^= nibble >> 1; + return (~nibble) & 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; + uint8_t p1 = (~(o1 ^ a1 ^ a0)) & 1; + uint8_t p0 = (~(o0 ^ a2 ^ a1)) & 1; + return (p2 << 2) | (p1 << 1) | p0; +} + +static uint16_t em4x05_build_cmd(uint8_t opcode, uint8_t addr) { + uint8_t parity = em4x05_cmd_parity(opcode, addr); + return (1u << 8) | ((opcode & 0x3) << 6) | ((addr & 0x7) << 3) | (parity & 0x7); +} + +static bool em4x05_decode_response(const uint8_t *bits, uint32_t *data) { + 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); + 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; + } + 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; + } + 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; + if (received_cp != ((~col_parity[col]) & 1)) { + NRF_LOG_DEBUG("em4x05: col %d parity fail", col); + return false; + } + } + *data = result; + return true; +} + +#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 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(); +} + +static uint8_t g_send_opcode; +static uint8_t g_send_addr; +static uint32_t g_send_password; +static volatile bool g_timeslot_done = false; + +/* + * Send one EM4305 command bit. + * Protocol: field ON for bit duration, then write gap (field OFF). + * The write gap delay is padded to compensate for antenna ringing (~200us). + * Field is left ON after the gap ready for the next bit or response window. + */ +static void send_em4305_bit(bool bit) { + if (bit) { + bsp_delay_us(256); /* bit 1: 32 Tc = 256us */ + } else { + bsp_delay_us(184); /* bit 0: 23 Tc = 184us */ + } + stop_lf_125khz_radio(); + bsp_delay_us(250); /* write gap: 128us target + ~122us ringing compensation */ + start_lf_125khz_radio(); +} + +static void em4x05_send_timeslot_cb(void) { + /* 1. Start gap: wake up tag */ + stop_lf_125khz_radio(); + bsp_delay_us(440); /* 55 Tc = 440us */ + + /* 2. Settle: allow tag clock recovery to lock onto carrier */ + start_lf_125khz_radio(); + bsp_delay_us(104); /* 13 carrier cycles = 104us */ + + /* 3. Send 9-bit command MSB first */ + uint16_t cmd = em4x05_build_cmd(g_send_opcode, g_send_addr); + for (int i = 8; i >= 0; i--) { + send_em4305_bit((cmd >> i) & 1); + } + + /* 4. Field stays ON (left by last start_lf in send_em4305_bit) + * Tag will respond ~3 Tc (~24us) after the last write gap */ + g_timeslot_done = true; +} + +static void em4x05_build_data_word(uint32_t data, uint8_t bits[45]) { + uint8_t col_par[4] = {0}; + int pos = 0; + bits[pos++] = 0; + for (int row = 0; row < 8; row++) { + uint8_t nibble = (data >> (28 - row * 4)) & 0xF; + uint8_t rp = 0; + for (int col = 0; col < 4; col++) { + uint8_t b = (nibble >> (3 - col)) & 1; + bits[pos++] = b; + col_par[col] ^= b; + rp ^= b; + } + bits[pos++] = (~rp) & 1; + } + for (int col = 0; col < 4; col++) { + bits[pos++] = (~col_par[col]) & 1; + } +} + +static void em4x05_login_timeslot_cb(void) { + /* Start gap */ + stop_lf_125khz_radio(); + bsp_delay_us(440); + start_lf_125khz_radio(); + bsp_delay_us(104); + + /* LOGIN command: opcode=0b00 (DSBL), addr=0b000 */ + uint16_t cmd = em4x05_build_cmd(EM4X05_OPCODE_DSBL, 0); + for (int i = 8; i >= 0; i--) { + send_em4305_bit((cmd >> i) & 1); + } + + /* Send 45-bit password word using same bit encoding */ + uint8_t pwd_bits[45]; + em4x05_build_data_word(g_send_password, pwd_bits); + for (int i = 0; i < 45; i++) { + send_em4305_bit(pwd_bits[i]); + } + + g_timeslot_done = true; +} + +static bool em4x05_login(uint32_t password, uint32_t timeout_ms) { + g_send_password = password; + g_timeslot_done = false; + + request_timeslot(15000, em4x05_login_timeslot_cb); + + autotimer *p_wait = bsp_obtain_timer(0); + while (!g_timeslot_done && NO_TIMEOUT_1MS(p_wait, 20)) {} + bsp_return_timer(p_wait); + + 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 ack = false; + autotimer *p_at = bsp_obtain_timer(0); + while (!ack && NO_TIMEOUT_1MS(p_at, timeout_ms)) { + uint16_t interval = 0; + if (!cb_pop_front(&g_cb, &interval)) { + continue; + } + uint8_t period = em4x05_rf64_period((uint8_t)interval); + if (period <= 2) { + ack = true; + } + } + bsp_return_timer(p_at); + lf_125khz_radio_gpiote_disable(); + unregister_rio_callback(); + cb_free(&g_cb); + return ack; +} + +static bool em4x05_read_block(uint8_t addr, uint32_t *data, uint32_t timeout_ms) { + g_send_opcode = EM4X05_OPCODE_READ; + g_send_addr = addr; + g_timeslot_done = false; + + /* + * Timeslot must cover full command transmission: + * start_gap(440) + settle(104) + 9 bits * (256+250) = 5098us + * Use 6000us for margin. + */ + request_timeslot(6000, em4x05_send_timeslot_cb); + + autotimer *p_wait = bsp_obtain_timer(0); + while (!g_timeslot_done && NO_TIMEOUT_1MS(p_wait, 10)) {} + bsp_return_timer(p_wait); + + 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(); + + manchester modem = { + .sync = true, + .rp = em4x05_rf64_period, + }; + uint8_t resp_bits[EM4X05_RESP_BITS] = {0}; + uint8_t bit_count = 0; + 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) { + 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) { + 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; +} + +bool em4x05_read(em4x05_data_t *out, uint32_t timeout_ms) { + memset(out, 0, sizeof(*out)); + + uint32_t block_timeout = timeout_ms / 4; + if (block_timeout < 100) block_timeout = 100; + + if (!em4x05_read_block(EM4X05_BLOCK_CONFIG, &out->config, block_timeout)) { + NRF_LOG_DEBUG("em4x05: block 0 read failed"); + return false; + } + + if (out->config == 0x00000000 || out->config == 0xFFFFFFFF) { + NRF_LOG_DEBUG("em4x05: invalid config word 0x%08X", out->config); + return false; + } + + bool rl = (out->config >> 6) & 1; + if (rl) { + NRF_LOG_DEBUG("em4x05: RL set, attempting login pwd=%08X", out->password); + if (!em4x05_login(out->password, block_timeout)) { + NRF_LOG_DEBUG("em4x05: login failed"); + out->login_required = true; + return false; + } + out->login_required = false; + NRF_LOG_DEBUG("em4x05: login OK"); + } + + uint8_t lwr = (out->config >> 16) & 0xF; + uint8_t uid_block = (lwr >= 1 && lwr < 14) ? lwr : EM4X05_BLOCK_UID; + + if (!em4x05_read_block(uid_block, &out->uid, block_timeout)) { + NRF_LOG_DEBUG("em4x05: UID block %d read failed", uid_block); + return false; + } + out->uid_block = uid_block; + + 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; + out->is_em4x69 = true; + } + + return true; +} + +uint8_t scan_em4x05(em4x05_data_t *out) { + start_lf_125khz_radio(); + bsp_delay_ms(5); + + bool found = em4x05_read(out, 1000); + + stop_lf_125khz_radio(); + + if (!found && out->login_required) { + return STATUS_LF_TAG_LOGIN_REQUIRED; + } + 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 0000000..230d645 --- /dev/null +++ b/firmware/application/src/rfid/reader/lf/lf_em4x05_data.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* ----------------------------------------------------------------------- + * Constants + * --------------------------------------------------------------------- */ + +#define EM4X05_OPCODE_READ 0x02 +#define EM4X05_OPCODE_WRITE 0x01 +#define EM4X05_OPCODE_PRCT 0x03 +#define EM4X05_OPCODE_DSBL 0x00 + +#define EM4X05_BLOCK_CONFIG 0 +#define EM4X05_BLOCK_PASSWD 1 +#define EM4X05_BLOCK_UID 15 +#define EM4X69_BLOCK_UID_LO 13 +#define EM4X69_BLOCK_UID_HI 14 + +#define EM4X05_RESPONSE_BITS 45 +#define EM4X05_RF_DIV 64 +#define EM4X05_RESPONSE_TIMEOUT_TC 300 + +/* ----------------------------------------------------------------------- + * Data structures + * --------------------------------------------------------------------- */ + +typedef struct { + uint32_t config; /* block 0: configuration word */ + uint32_t uid; /* UID (block determined by LWR or block 15) */ + uint32_t uid_hi; /* EM4x69 only: high word of 64-bit UID */ + bool is_em4x69; /* true if 64-bit UID was successfully read */ + uint8_t uid_block; /* block number where UID was actually read from */ + uint32_t password; /* password to use for LOGIN (default 0x00000000)*/ + bool login_required;/* true if tag has RL bit set and login failed */ +} em4x05_data_t; + +/* ----------------------------------------------------------------------- + * Public API + * --------------------------------------------------------------------- */ + +bool em4x05_read(em4x05_data_t *out, uint32_t timeout_ms); +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 0000000..3d15fb4 --- /dev/null +++ b/firmware/application/src/rfid/reader/lf/lf_gap.c @@ -0,0 +1,75 @@ +#include "lf_gap.h" + +#include "bsp_delay.h" +#include "hw_connect.h" +#include "lf_125khz_radio.h" +#include "lf_reader_data.h" +#include "nrf_gpio.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. + * + * Gap generation: we cannot rely on nrfx_pwm_stop() to cut the field + * because when the PWM stops it releases LF_ANT_DRIVER to GPIO state, + * which may leave the antenna driver enabled. Instead we: + * 1. Stop the PWM (releases pin to GPIO) + * 2. Explicitly drive LF_ANT_DRIVER low (field off) + * 3. Delay for the gap duration + * 4. Drive LF_ANT_DRIVER high then restart PWM (field on) + * --------------------------------------------------------------------- */ + +static inline void field_off(void) { + nrfx_pwm_stop(&m_pwm, true); /* stop PWM, releases pin */ + nrf_gpio_cfg_output(LF_ANT_DRIVER); + nrf_gpio_pin_clear(LF_ANT_DRIVER); /* drive low = field off */ +} + +static inline void field_on(void) { + nrf_gpio_pin_set(LF_ANT_DRIVER); /* drive high briefly */ + start_lf_125khz_radio(); /* restart PWM on pin */ +} + +void lf_gap_send_start(void) { + field_off(); + bsp_delay_us(GAP_START_US); + field_on(); +} + +void lf_gap_send_bit(uint8_t bit) { + if (bit & 1) { + bsp_delay_us(GAP_BIT1_US); + } else { + bsp_delay_us(GAP_BIT0_US); + } + field_off(); + bsp_delay_us(GAP_WRITE_US); + field_on(); +} + +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); + } +} + +bool lf_gap_detect(uint32_t last_count, uint32_t *gap_tc) { + uint32_t now = get_lf_counter_value(); + 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 0000000..919c135 --- /dev/null +++ b/firmware/application/src/rfid/reader/lf/lf_gap.h @@ -0,0 +1,93 @@ +#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 55 /* PM3 proven: 55*8=440us for EM4x05/4305 */ +#define GAP_START_US (GAP_START_TC * 8) + +/** Write gap: separates command bits during transmission. */ +#define GAP_WRITE_TC 16 /* PM3 proven: 16*8=128us */ +#define GAP_WRITE_US (GAP_WRITE_TC * 8) + +/** Field-on duration encoding bit '0' between write gaps. */ +#define GAP_BIT0_TC 23 /* PM3 proven: 23*8=184us */ +#define GAP_BIT0_US (GAP_BIT0_TC * 8) + +/** Field-on duration encoding bit '1' between write gaps. */ +#define GAP_BIT1_TC 32 /* PM3 proven: 32*8=256us */ +#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 + * --------------------------------------------------------------------- */ + +void lf_gap_send_start(void); +void lf_gap_send_bit(uint8_t bit); +void lf_gap_send_u32(uint32_t word); +void lf_gap_send_bits(uint32_t value, uint8_t nbits); +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_pac_data.c b/firmware/application/src/rfid/reader/lf/lf_pac_data.c new file mode 100644 index 0000000..fa3e040 --- /dev/null +++ b/firmware/application/src/rfid/reader/lf/lf_pac_data.c @@ -0,0 +1,75 @@ +#include + +#include "bsp_delay.h" +#include "bsp_time.h" +#include "circular_buffer.h" +#include "lf_125khz_radio.h" +#include "lf_reader_data.h" +#include "nrfx_saadc.h" +#include "protocols/pac.h" +#include "protocols/protocols.h" + +#define NRF_LOG_MODULE_NAME pac_reader +#include "nrf_log.h" +#include "nrf_log_ctrl.h" +#include "nrf_log_default_backends.h" +NRF_LOG_MODULE_REGISTER(); + +#define PAC_BUFFER_SIZE (6144) + +static circular_buffer cb; + +// SAADC callback — push raw ADC samples to circular buffer. +// NRZ/Direct modulation requires ADC sampling (not GPIOTE edge timing) +// because the comparator may not produce clean digital edges for NRZ signals. +static void pac_saadc_cb(nrf_saadc_value_t *vals, size_t size) { + for (size_t i = 0; i < size; i++) { + nrf_saadc_value_t val = vals[i]; + if (!cb_push_back(&cb, &val)) { + return; + } + } +} + +static void init_pac_hw(void) { + lf_125khz_radio_saadc_enable(pac_saadc_cb); +} + +static void uninit_pac_hw(void) { + lf_125khz_radio_saadc_disable(); +} + +bool pac_read(uint8_t *data, uint32_t timeout_ms) { + void *codec = pac.alloc(); + pac.decoder.start(codec, 0); + + // Start carrier first, then wait for T55XX POR (~5ms) before + // enabling SAADC. This ensures the prescan calibration phase + // sees real NRZ signal levels rather than power-up noise. + start_lf_125khz_radio(); + bsp_delay_ms(10); + + cb_init(&cb, PAC_BUFFER_SIZE, sizeof(uint16_t)); + init_pac_hw(); + + bool ok = false; + autotimer *p_at = bsp_obtain_timer(0); + while (!ok && NO_TIMEOUT_1MS(p_at, timeout_ms)) { + uint16_t val = 0; + while (!ok && NO_TIMEOUT_1MS(p_at, timeout_ms) && cb_pop_front(&cb, &val)) { + if (pac.decoder.feed(codec, val)) { + memcpy(data, pac.get_data(codec), pac.data_size); + ok = true; + break; + } + } + } + + bsp_return_timer(p_at); + stop_lf_125khz_radio(); + uninit_pac_hw(); + cb_free(&cb); + + pac.free(codec); + return ok; +} diff --git a/firmware/application/src/rfid/reader/lf/lf_reader_data.h b/firmware/application/src/rfid/reader/lf/lf_reader_data.h index 0d265ca..6775ae6 100644 --- a/firmware/application/src/rfid/reader/lf/lf_reader_data.h +++ b/firmware/application/src/rfid/reader/lf/lf_reader_data.h @@ -21,6 +21,7 @@ void clear_lf_counter_value(void); bool em410x_read(uint8_t *data, uint32_t timeout_ms); bool ioprox_read(uint8_t *data, uint8_t format_hint, uint32_t timeout_ms); bool hidprox_read(uint8_t *data, uint8_t format_hint, uint32_t timeout_ms); +bool pac_read(uint8_t *data, uint32_t timeout_ms); bool viking_read(uint8_t *data, uint32_t timeout_ms); bool raw_read_to_buffer(uint8_t *data, size_t maxlen, uint32_t timeout_ms, size_t *outlen); diff --git a/firmware/application/src/rfid/reader/lf/lf_reader_generic.c b/firmware/application/src/rfid/reader/lf/lf_reader_generic.c index 4fdbc42..d6da018 100644 --- a/firmware/application/src/rfid/reader/lf/lf_reader_generic.c +++ b/firmware/application/src/rfid/reader/lf/lf_reader_generic.c @@ -1,6 +1,8 @@ +#include "lf_reader_generic.h" #include "lf_reader_data.h" #include "bsp_delay.h" +#include "bsp_wdt.h" #include "bsp_time.h" #include "circular_buffer.h" #include "lf_125khz_radio.h" @@ -13,15 +15,19 @@ #include "nrf_log_default_backends.h" NRF_LOG_MODULE_REGISTER(); -#define CIRCULAR_BUFFER_SIZE (128) +/* + * Circular buffer for SAADC samples. + * Increased from 128 to 512 to reduce overrun risk during USB transfer. + * The main loop drains it as fast as possible into the output buffer. + */ +#define CIRCULAR_BUFFER_SIZE (512) static circular_buffer cb; -// saadc irq is used to sample ANT GPIO. static void saadc_cb(nrf_saadc_value_t *vals, size_t size) { for (int i = 0; i < size; i++) { nrf_saadc_value_t val = vals[i]; if (!cb_push_back(&cb, &val)) { - return; + return; /* buffer full — oldest samples dropped */ } } } @@ -41,14 +47,20 @@ bool raw_read_to_buffer(uint8_t *data, size_t maxlen, uint32_t timeout_ms, size_ init_saadc_hw(); start_lf_125khz_radio(); + /* Wait for antenna to settle before capturing. + * The LC circuit rings for ~400µs on field startup, then takes + * another ~800µs to reach steady state. Skip 2ms to be safe. */ + bsp_delay_ms(2); + autotimer *p_at = bsp_obtain_timer(0); while (NO_TIMEOUT_1MS(p_at, timeout_ms) && *outlen < maxlen) { uint16_t val = 0; while (cb_pop_front(&cb, &val) && *outlen < maxlen) { - val = val >> 5; // 14 bit ADC to 8 bit value and /2 range - data[*outlen] = val > 0xff ? 0xff : val; + val = val >> 5; /* 14-bit ADC → 9-bit, then >>5 gives 8-bit */ + data[*outlen] = val > 0xff ? 0xff : (uint8_t)val; ++(*outlen); } + bsp_wdt_feed(); /* prevent watchdog reset during long captures */ } bsp_return_timer(p_at); diff --git a/firmware/application/src/rfid/reader/lf/lf_reader_generic.h b/firmware/application/src/rfid/reader/lf/lf_reader_generic.h new file mode 100644 index 0000000..f7c3340 --- /dev/null +++ b/firmware/application/src/rfid/reader/lf/lf_reader_generic.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include + +/* + * Capture raw ADC samples from the LF antenna field. + * + * The SAADC samples at the PWM period rate (125kHz = 8µs/sample). + * Each sample is an 8-bit value (14-bit ADC >> 5, clamped to 0xFF). + * A steady carrier reads ~0x80-0x82; a gap reads noticeably lower. + * + * @param data Output buffer for raw samples + * @param maxlen Max bytes to capture (max 4000 for USB frame limit) + * @param timeout_ms Stop after this many ms even if buffer not full + * @param outlen Actual number of bytes written + * @return true on success + */ +/** Maximum bytes a single raw capture can return (USB frame limit). */ +#define LF_SNIFF_MAX_SAMPLES 4000 + +bool raw_read_to_buffer(uint8_t *data, size_t maxlen, uint32_t timeout_ms, size_t *outlen); 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 556bef4..b96fa0d 100644 --- a/firmware/application/src/rfid/reader/lf/lf_reader_main.c +++ b/firmware/application/src/rfid/reader/lf/lf_reader_main.c @@ -9,6 +9,7 @@ #include "protocols/ioprox.h" #include "protocols/hidprox.h" #include "protocols/t55xx.h" +#include "protocols/pac.h" #include "protocols/viking.h" #define NRF_LOG_MODULE_NAME lf_main @@ -80,6 +81,16 @@ uint8_t encode_ioprox_params(uint8_t ver, uint8_t fc, uint16_t cn, uint8_t *out) return STATUS_CMD_ERR; } +/** + * Search PAC/Stanley tag + */ +uint8_t scan_pac(uint8_t *card_id) { + if (pac_read(card_id, g_timeout_readem_ms)) { + return STATUS_LF_TAG_OK; + } + return STATUS_LF_TAG_NO_FOUND; +} + /** * Search Viking tag */ @@ -187,6 +198,13 @@ uint8_t write_viking_to_t55xx(uint8_t *uid, uint8_t *new_passwd, uint8_t *old_pa return write_t55xx(blks, blk_count, new_passwd, old_passwds, old_passwd_count); } +uint8_t write_pac_to_t55xx(uint8_t *data, uint8_t *new_passwd, uint8_t *old_passwds, uint8_t old_passwd_count) { + uint32_t blks[7] = {0x00}; + uint8_t blk_count = pac_t55xx_writer(data, blks); + if (blk_count == 0) return STATUS_PAR_ERR; + return write_t55xx(blks, blk_count, new_passwd, old_passwds, old_passwd_count); +} + /** * Set the LF card scanning timeout value (in milliseconds). */ 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 c975d13..629f015 100644 --- a/firmware/application/src/rfid/reader/lf/lf_reader_main.h +++ b/firmware/application/src/rfid/reader/lf/lf_reader_main.h @@ -5,6 +5,9 @@ #include "app_status.h" #include "lf_125khz_radio.h" +#if defined(PROJECT_CHAMELEON_ULTRA) +#include "lf_em4x05_data.h" +#endif #include "lf_reader_data.h" void set_scan_tag_timeout(uint32_t ms); @@ -13,9 +16,11 @@ 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_pac(uint8_t *card_id); uint8_t scan_viking(uint8_t *uid); 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); +uint8_t write_pac_to_t55xx(uint8_t *data, uint8_t *new_passwd, uint8_t *old_passwds, uint8_t old_passwd_count); diff --git a/firmware/application/src/sdk_config.h b/firmware/application/src/sdk_config.h index 5bddb15..943af40 100644 --- a/firmware/application/src/sdk_config.h +++ b/firmware/application/src/sdk_config.h @@ -6614,7 +6614,7 @@ #ifndef APP_USBD_STRING_SERIAL_EXTERN -#define APP_USBD_STRING_SERIAL_EXTERN 0 +#define APP_USBD_STRING_SERIAL_EXTERN 1 #endif // APP_USBD_STRING_SERIAL - String descriptor for the serial number. @@ -6622,7 +6622,7 @@ // Note: This value is not editable in Configuration Wizard. // Serial number that is defined the same way like in @ref APP_USBD_STRINGS_MANUFACTURER. #ifndef APP_USBD_STRING_SERIAL -#define APP_USBD_STRING_SERIAL APP_USBD_STRING_DESC("000000000000") +#define APP_USBD_STRING_SERIAL g_extern_serial_number #endif // diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index 48370ba..a167f74 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -753,10 +753,15 @@ hf_mfu = hf.subgroup("mfu", "MIFARE Ultralight / NTAG commands") lf = root.subgroup("lf", "Low Frequency commands") lf_em = lf.subgroup("em", "EM commands") +lf_em_4x05 = lf_em.subgroup("4x05", "EM4x05/EM4x69 commands") +data = root.subgroup('data', 'Data analysis and visualization commands') + + 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") @@ -2440,9 +2445,43 @@ class HFMFAutopwn(ReaderRequiredUnit): print(f" {CG}[+]{C0} Some keys found, recovering remaining..") if nt_level == 2: - print( - f" {CY}[+]{C0} Hardened card — use 'hf mf hardnested' to recover remaining keys" - ) + missing_keys = self.find_missing_keys(current_keys_found, total) + hardnested = HFMFHardNested.__new__(HFMFHardNested) + BaseCLIUnit.__init__(hardnested) + hardnested._device_cmd = self.cmd + for missing_key_num, key_type_target in missing_keys.items(): + if current_keys_found.get(missing_key_num) is not None: + print(f" {CG}[+]{C0} Key {missing_key_num} found by reuse") + continue + block_known, key_known_bytes, type_known = self.choose_random_known_key( + current_keys_found + ) + block_target = (missing_key_num // 2) * 4 + hn_key = hardnested.recover_key( + False, + block_known, + type_known, + key_known_bytes, + block_target, + key_type_target, + False, + 200, + 3, + ) + if hn_key is None: + continue + print(f" {CG}[+]{C0} Found key {missing_key_num}: {hn_key.upper()}") + current_keys_found[missing_key_num] = bytes.fromhex(hn_key) + current_keys_found = dict(sorted(current_keys_found.items())) + _, mask_bytes = self.mask_from_keys(missing_keys) + current_keys_found = self.merge_found_sector_keys( + current_keys_found, + self.try_key(bytes.fromhex(hn_key), self.neg_bytes(mask_bytes)), + ) + if len(current_keys_found) < total: + current_keys_found = self.run_senested( + current_keys_found, max_sectors_num + ) elif nt_level == 0: current_keys_found = self.run_senested(current_keys_found, max_sectors_num) else: @@ -3622,7 +3661,7 @@ class HFMFESave(SlotIndexArgsAndGoUnit, DeviceRequiredUnit): data = bytearray(0) max_blocks = self.device_com.data_max_length // 16 while block_count > 0: - chunk_count = min(block_count, max_blocks) + chunk_count = min(block_count, max_blocks, 32) data.extend(self.cmd.mf1_read_emu_block_data(index, chunk_count)) index += chunk_count block_count -= chunk_count @@ -5896,12 +5935,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): @@ -5921,9 +5960,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, @@ -5931,14 +5970,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") @@ -5971,9 +6010,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, @@ -5983,21 +6022,200 @@ 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): + 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) + 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): + 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.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: + raise NotImplementedError("Please implement this") + + def on_exec(self, args: argparse.Namespace): + raise NotImplementedError("Please implement this") + +@lf_pac.command('write') +class LFPacWriteT55xx(LFPacIdArgsUnit, ReaderRequiredUnit): + def args_parser(self) -> ArgumentParserNoExit: + parser = ArgumentParserNoExit() + parser.description = 'Write PAC/Stanley id to T55xx' + return self.add_card_arg(parser, required=True) + + def on_exec(self, args: argparse.Namespace): + 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) + 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): + def args_parser(self) -> ArgumentParserNoExit: + parser = ArgumentParserNoExit() + parser.description = 'Set emulated PAC/Stanley card ID' + self.add_slot_args(parser) + self.add_card_arg(parser) + return parser + + def on_exec(self, args: argparse.Namespace): + if args.id is not None: + 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.PAC: + print(f"{color_string((CR, 'WARNING'))}: Slot type not set to PAC.") + self.cmd.pac_set_emu_id(bytes.fromhex(args.id)) + print(' - Set PAC/Stanley tag id success.') + 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'CN: {card_id_ascii} | Raw: {raw.hex().upper()}') @lf_viking.command("read") class LFVikingRead(ReaderRequiredUnit): @@ -6109,8 +6327,20 @@ class HWSlotList(DeviceRequiredUnit): for slot in SlotNumber: fwslot = SlotNumber.to_fw(slot) status = f"({color_string((CG, 'active'))})" if slot == selected else "" - hf_tag_type = TagSpecificType(slotinfo[fwslot]["hf"]) - lf_tag_type = TagSpecificType(slotinfo[fwslot]["lf"]) + try: + hf_tag_type = TagSpecificType(slotinfo[fwslot]["hf"]) + except ValueError: + hf_tag_type = TagSpecificType.UNDEFINED + hf_unknown = slotinfo[fwslot]["hf"] + else: + hf_unknown = None + try: + lf_tag_type = TagSpecificType(slotinfo[fwslot]["lf"]) + except ValueError: + lf_tag_type = TagSpecificType.UNDEFINED + lf_unknown = slotinfo[fwslot]["lf"] + else: + lf_unknown = None print(f' - {f"Slot {slot}:":{4+maxnamelength+1}} {status}') # HF @@ -6124,7 +6354,9 @@ class HWSlotList(DeviceRequiredUnit): f" HF: " f'{slotnames[fwslot]["hf"]["name"]:{field_length}}', end="" ) print(status, end="") - if hf_tag_type != TagSpecificType.UNDEFINED: + if hf_unknown is not None: + print(color_string((CR, f"Unknown ({hf_unknown})"))) + elif hf_tag_type != TagSpecificType.UNDEFINED: color = CY if enabled[fwslot]["hf"] else C0 print(color_string((color, hf_tag_type))) else: @@ -6133,6 +6365,7 @@ class HWSlotList(DeviceRequiredUnit): (not args.short) and enabled[fwslot]["hf"] and hf_tag_type != TagSpecificType.UNDEFINED + and hf_unknown is None ): if current != slot: self.cmd.set_active_slot(slot) @@ -6198,7 +6431,9 @@ class HWSlotList(DeviceRequiredUnit): f" LF: " f'{slotnames[fwslot]["lf"]["name"]:{field_length}}', end="" ) print(status, end="") - if lf_tag_type != TagSpecificType.UNDEFINED: + if lf_unknown is not None: + print(color_string((CR, f"Unknown ({lf_unknown})"))) + elif lf_tag_type != TagSpecificType.UNDEFINED: color = CY if enabled[fwslot]["lf"] else C0 print(color_string((color, lf_tag_type))) else: @@ -6207,6 +6442,7 @@ class HWSlotList(DeviceRequiredUnit): (not args.short) and enabled[fwslot]["lf"] and lf_tag_type != TagSpecificType.UNDEFINED + and lf_unknown is None ): if current != slot: self.cmd.set_active_slot(slot) @@ -6236,6 +6472,12 @@ class HWSlotList(DeviceRequiredUnit): if lf_tag_type == TagSpecificType.Viking: id = self.cmd.viking_get_emu_id() print(f" {'ID:':40}{color_string((CY, id.hex().upper()))}") + 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) + 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) @@ -6269,6 +6511,7 @@ class HWSlotType(TagTypeArgsUnit, SlotIndexArgsUnit): else: slot_num = SlotNumber.from_fw(self.cmd.get_active_slot()) self.cmd.set_slot_tag_type(slot_num, tag_type) + self.cmd.set_slot_data_default(slot_num, tag_type) print(f" - Set slot {slot_num} tag type success.") @@ -6970,3 +7213,965 @@ examples/notes: ) else: print(f" [*] {color_string((CY, 'No response'))}") + + +@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): + try: + pwd = int(args.pwd, 16) if hasattr(args, 'pwd') and args.pwd else 0 + except ValueError: + print(f"{CR}Invalid password, expected hex{C0}") + return + (config, uid, uid_hi, is_em4x69, uid_block) = self.cmd.em4x05_scan(pwd=pwd) + tag_label = "EM4x69" if is_em4x69 else "EM4x05" + rl = bool((config >> 6) & 1) + print(f" Tag type : {CG}{tag_label}{C0}") + print(f" Config : {CG}{config:#010x}{C0}") + print(f" UID block: {CG}{uid_block}{C0}") + if rl: + print(f" Auth : {CG}LOGIN used (pwd={args.pwd.upper() if hasattr(args, 'pwd') and args.pwd else '00000000'}){C0}") + if is_em4x69: + uid64 = (uid_hi << 32) | uid + print(f" UID (64) : {CG}{uid64:016x}{C0}") + else: + print(f" UID : {CG}{uid:08x}{C0}") + + +@lf.command('sniff') +class LFSniff(ReaderRequiredUnit): + def args_parser(self) -> ArgumentParserNoExit: + parser = ArgumentParserNoExit() + parser.description = ( + "Capture raw LF field ADC samples (125kHz, 8µs/sample). " + "~0x80 = field on, lower values = gap or no field." + ) + parser.add_argument( + '--timeout', type=int, default=2000, metavar='MS', + help='Capture duration in milliseconds (default: 2000, max: 10000, firmware blocks for full duration)' + ) + parser.add_argument( + '--out', type=str, default=None, metavar='FILE', + help='Save raw samples to binary file (for offline analysis)' + ) + parser.add_argument( + '--hex', action='store_true', + help='Print hex dump of samples to screen' + ) + return parser + + def on_exec(self, args: argparse.Namespace): + timeout = max(1, min(10000, args.timeout)) + print(f" Capturing LF field for {timeout}ms at 125kHz (8µs/sample)...") + resp = self.cmd.lf_sniff(timeout_ms=timeout) + + if resp.status != Status.LF_TAG_OK or not resp.data: + print(f"{CR}No samples captured{C0}") + return + + import chameleon_cli_unit as _self_mod + data = bytes(resp.data) + _self_mod._last_capture = data + + n = len(data) + duration_ms = n * 8 / 1000 + print(f" Captured : {CG}{n}{C0} bytes ({duration_ms:.1f}ms)") + + mn = min(data) + mx = max(data) + mean = sum(data) // len(data) + print(f" Range : {CG}0x{mn:02x}{C0} – {CG}0x{mx:02x}{C0} mean: {CG}0x{mean:02x}{C0}") + + # Detect real field gaps — they drop to near zero (0x00-0x40), + # well below the steady carrier (~0xb0). Use half of mean as threshold + # to avoid false positives from the antenna startup transient. + gap_threshold = mean // 2 + # Skip first 200 samples (1.6ms) to ignore startup ringing + steady_data = data[200:] + gap_count = sum(1 for b in steady_data if b < gap_threshold) + if gap_count > 0: + print(f" Gaps : {CG}{gap_count}{C0} samples below 0x{gap_threshold:02x} (real field drops)") + else: + print(f" Gaps : {CR}none detected — flat carrier (no gap commands sent){C0}") + + if args.hex: + print() + print(f" addr {'hex bytes':47s} level") + print(f" ---- {'-'*47} ----------------") + for i in range(0, min(n, 256), 16): + row = data[i:i+16] + hex_part = ' '.join(f'{b:02x}' for b in row) + bar = '' + for b in row: + if b < 0x10: + bar += '_' # gap / field off + elif b < 0x40: + bar += '.' # ringing decay + elif b < 0x80: + bar += '-' # low + elif b < 0xa0: + bar += '+' # mid + elif b < 0xc0: + bar += 'o' # steady carrier + elif b < 0xe0: + bar += 'O' # high + else: + bar += '#' # clipped 0xff + print(f" {i:04x} {hex_part:<47s} {bar}") + if n > 256: + print(f" ... ({n - 256} more bytes, use --out to save all)") + print() + print(" _ gap . ringing - low + mid o carrier O high # clipped") + + if args.out: + try: + with open(args.out, 'wb') as f: + f.write(data) + print(f" Saved : {CG}{args.out}{C0} ({n} bytes)") + except Exception as e: + print(f"{CR}Failed to save: {e}{C0}") + + +@hf_14a.command("info") +@hf_14a.command('sniff') +class HF14ASniff(BaseCLIUnit): + def args_parser(self) -> ArgumentParserNoExit: + parser = ArgumentParserNoExit() + parser.description = ( + "Capture ISO14443A reader frames while CU acts as a tag. " + "Place CU near a reader — all commands the reader sends are logged. " + "Useful for understanding what a reader expects before configuring emulation." + ) + parser.add_argument( + '--timeout', type=int, default=5000, metavar='MS', + help='Listen duration in milliseconds (default: 5000, max: 30000, firmware blocks for full duration)' + ) + return parser + + def on_exec(self, args: argparse.Namespace): + timeout = max(1, min(30000, args.timeout)) + print(f" Listening for reader frames for {timeout}ms...") + print(" Place CU near a reader now.") + print() + + try: + resp = self.cmd.hf14a_sniff(timeout_ms=timeout) + except Exception as e: + if 'CMDInvalid' in type(e).__name__ or '2020' in str(e): + print(f"{CR}Command not supported — reflash firmware to enable hf 14a sniff{C0}") + else: + print(f"{CR}{e}{C0}") + return + + if resp.status not in (Status.HF_TAG_OK, Status.SUCCESS): + cb_count = 0 + if resp.data and len(resp.data) >= 2: + cb_count = (resp.data[0] << 8) | resp.data[1] + if cb_count > 0: + print(f"{CY} Callback fired {cb_count}x but no valid frames buffered{C0}") + else: + print(" No frames captured — no reader detected") + return + + # Parse packed frame buffer: [2 bytes bits BE][N bytes data] ... + buf = bytes(resp.data) + frames = [] + i = 0 + while i + 2 <= len(buf): + szBits = (buf[i] << 8) | buf[i+1] + i += 2 + if szBits == 0: + break + szBytes = (szBits + 7) // 8 + if i + szBytes > len(buf): + break + raw = buf[i:i+szBytes] + i += szBytes + + # ISO14443-A frames include one parity bit per byte. + # Short frames (< 8 bits, e.g. REQA=7 bits) have no parity. + # All other frames: szBits = data_bytes * 9, strip every 9th bit. + if szBits >= 8 and szBits % 9 == 0: + n_bytes = szBits // 9 + all_bits = [] + for byte in raw: + for b in range(8): + all_bits.append((byte >> b) & 1) + stripped = [] + for nb in range(n_bytes): + val = 0 + for b in range(8): + val |= all_bits[nb * 9 + b] << b + stripped.append(val) + data = bytes(stripped) + szBits = n_bytes * 8 + else: + data = raw + + frames.append((szBits, data)) + + if not frames: + print(f"{CR}No frames decoded{C0}") + return + + print(f" Captured : {CG}{len(frames)}{C0} frame(s)") + print() + print(f" {'#':>3} {'bits':>4} {'hex data':<42} decoded") + print(f" {'---':>3} {'----':>4} {'-'*42} {'-'*35}") + + for n, (szBits, data) in enumerate(frames): + hex_str = ' '.join(f'{b:02x}' for b in data) + decoded, col = _decode_14a_frame_col(data, szBits) + print(f" {CY}{n+1:>3}{C0} {szBits:>4} {hex_str:<42} {col}{decoded}{C0}") + + # Summary block + print() + _print_14a_sniff_summary(frames) + + + + +def _decode_14a_frame_col(data: bytes, szBits: int): + """Return (description, colour) for a 14A frame.""" + if not data: + return '', C0 + b0 = data[0] + + # Short frames (7-bit) + if szBits == 7: + if b0 == 0x26: + return 'REQA', CG + if b0 == 0x52: + return 'WUPA', CG + return f'short(0x{b0:02x})', CC + + # Anti-collision / Select + if b0 == 0x93: + if len(data) > 1 and data[1] == 0x70: + uid = ' '.join(f'{b:02x}' for b in data[2:6]) if len(data) >= 6 else '' + return f'SELECT CL1 UID={uid}', CB + nvb = f'NVB={data[1]:02x}' if len(data) > 1 else '' + return f'ANTICOLL CL1 {nvb}', CB + if b0 == 0x95: + if len(data) > 1 and data[1] == 0x70: + uid = ' '.join(f'{b:02x}' for b in data[2:6]) if len(data) >= 6 else '' + return f'SELECT CL2 UID={uid}', CB + nvb = f'NVB={data[1]:02x}' if len(data) > 1 else '' + return f'ANTICOLL CL2 {nvb}', CB + if b0 == 0x97: + if len(data) > 1 and data[1] == 0x70: + uid = ' '.join(f'{b:02x}' for b in data[2:6]) if len(data) >= 6 else '' + return f'SELECT CL3 UID={uid}', CB + nvb = f'NVB={data[1]:02x}' if len(data) > 1 else '' + return f'ANTICOLL CL3 {nvb}', CB + + # HALT (0x50 0x00 + CRC — b1 may vary after parity strip) + if b0 == 0x50: + return 'HALT', CC + + # S-DESELECT (ISO14443-4 block) + if b0 == 0xc2: + return 'S-DESELECT', CC + + # PPS + if b0 == 0xd0: + return f'PPS PPS1={data[1]:02x}' if len(data) > 1 else 'PPS', CC + + # RATS + if b0 == 0xe0: + fsdi = (data[1] >> 4) if len(data) > 1 else 0 + cid = (data[1] & 0xf) if len(data) > 1 else 0 + return f'RATS FSDI={fsdi} CID={cid}', CC + + # MIFARE Classic commands + if b0 == 0x60: + return f'AUTH KeyA block={data[1]}' if len(data) > 1 else 'AUTH KeyA', CR + if b0 == 0x61: + return f'AUTH KeyB block={data[1]}' if len(data) > 1 else 'AUTH KeyB', CR + # Encrypted nonce / auth response (follows AUTH, first byte varies) + if szBits == 72: + return '(encrypted nonce — auth challenge/response)', CC + + if b0 == 0x30: + return f'READ block={data[1]}' if len(data) > 1 else 'READ', CC + if b0 == 0xa0: + return f'WRITE block={data[1]}' if len(data) > 1 else 'WRITE', CY + if b0 == 0x40: + return 'MAGIC WUPC1', CY + if b0 == 0x43: + return 'MAGIC WUPC2', CY + if b0 == 0x41: + return 'MAGIC WIPE', CR + + # ISO 7816-4 APDUs + if len(data) >= 2 and b0 in (0x00, 0x80, 0x90, 0xa0): + cla, ins = data[0], data[1] + p1 = data[2] if len(data) > 2 else 0 + p2 = data[3] if len(data) > 3 else 0 + # SELECT FILE / AID + if cla == 0x00 and ins == 0xa4: + if len(data) > 5: + aid = ' '.join(f'{b:02x}' for b in data[5:5+data[4]]) + # Identify known AIDs + aid_raw = bytes(data[5:5+data[4]]) + name = _known_aid(aid_raw) + label = f'SELECT AID {aid.upper()}' + if name: + label += f' ({name})' + return label, CY + return 'SELECT', CY + # READ BINARY + if cla == 0x00 and ins == 0xb0: + return f'READ BINARY off={p1<<8|p2} len={data[4] if len(data)>4 else 0}', CC + # READ RECORD + if cla == 0x00 and ins == 0xb2: + sfi = p2 >> 3 + return f'READ RECORD SFI={sfi} rec={p1}', CC + # GET DATA + if cla == 0x80 and ins == 0xca: + tag = (p1 << 8) | p2 + name = _known_bertag(tag) + return f'GET DATA {p1:02x}{p2:02x}' + (f' ({name})' if name else ''), CC + # GET PROCESSING OPTIONS + if cla == 0x80 and ins == 0xa8: + return 'GPO (Get Processing Options)', CY + # GENERATE AC + if cla == 0x80 and ins == 0xae: + actype = {0x00:'AAC', 0x40:'TC', 0x80:'ARQC'}.get(p1 & 0xc0, f'AC/{p1:02x}') + return f'GENERATE AC requesting {actype}', CR + # VERIFY + if cla == 0x00 and ins == 0x20: + return 'VERIFY PIN', CY + # INTERNAL AUTHENTICATE + if cla == 0x00 and ins == 0x88: + return 'INTERNAL AUTH', CR + # EXTERNAL AUTHENTICATE + if cla == 0x00 and ins == 0x82: + return 'EXTERNAL AUTH', CR + # MANAGE CHANNEL + if cla == 0x00 and ins == 0x70: + return 'MANAGE CHANNEL', CC + return f'APDU CLA={cla:02x} INS={ins:02x} P1={p1:02x} P2={p2:02x}', CY + + # Unknown — show first byte + return f'unknown (0x{b0:02x})', CC + + + + +def _known_aid(aid: bytes) -> str: + table = { + bytes.fromhex('a0000000031010'): 'Visa Credit/Debit', + bytes.fromhex('a0000000032010'): 'Visa Electron', + bytes.fromhex('a0000000033010'): 'Visa Classic', + bytes.fromhex('a0000000038010'): 'Visa Plus', + bytes.fromhex('a0000000041010'): 'Mastercard', + bytes.fromhex('a0000000043060'): 'Maestro', + bytes.fromhex('a000000025010801'): 'AmEx', + bytes.fromhex('a0000000181002'): 'Mastercard Debit', + bytes.fromhex('d2760000850101'): 'NDEF (NFC Forum)', + bytes.fromhex('d27600002545'): 'NDEF Type 4', + bytes.fromhex('315041592e5359532e4444463031'): 'PPSE (2PAY.SYS.DDF01)', + } + return table.get(aid, '') + + + + +def _known_bertag(tag: int) -> str: + table = { + 0x9f36: 'ATC', + 0x9f13: 'Last Online ATC', + 0x9f17: 'PIN Try Counter', + 0x9f4f: 'Log Format', + 0x9f4e: 'Merchant Name', + } + return table.get(tag, '') + + + + +def _print_14a_sniff_summary(frames): + """Print a decoded summary of the sniff session.""" + uid_cl1 = None + uid_cl2 = None + uid_cl3 = None + aids = [] + auth_blocks = [] # (key_type, block) + auth_seen = False + arqc_seen = False + tc_seen = False + halted = False + rats_seen = False + atc_tag = None + amount = None + + for szBits, data in frames: + if not data: + continue + b0 = data[0] + + # Extract UID from anticoll frames (NVB != 70 = anticoll, NVB = 70 = select) + # Anticoll frame with NVB=41 means we're requesting UID bytes + # The *response* from the tag contains the UID — but we only see reader frames + # So extract from SELECT (NVB=70) which contains the full UID + if b0 in (0x93, 0x95, 0x97) and len(data) >= 5 and data[1] == 0x70: + uid_bytes = bytes(data[2:6]) + if b0 == 0x93: + if data[2] == 0x88: + uid_cl1 = None # cascade tag, UID continues in CL2 + else: + uid_cl1 = uid_bytes + elif b0 == 0x95: + uid_cl2 = uid_bytes + elif b0 == 0x97: + uid_cl3 = uid_bytes + + # Extract partial UID from anticoll frames — the tag sends back UID bytes + # We capture the reader's anticoll command which may contain partial UID + # NVB high nibble = number of full bytes sent, low nibble = bits + # NVB=41 means reader sent 4 bits, so tag should respond with rest + # NVB=e1 (225) is unusual — may be tag response captured by NFCT + + # RATS + if b0 == 0xe0: + rats_seen = True + + # SELECT AID + if b0 == 0x00 and len(data) > 5 and data[1] == 0xa4: + aid = bytes(data[5:5+data[4]]) + name = _known_aid(aid) + entry = aid.hex().upper() + if name: + entry += f' ({name})' + if entry not in aids: + aids.append(entry) + + # MIFARE Classic auth + if b0 in (0x60, 0x61) and len(data) > 1: + auth_seen = True + key_type = 'KeyA' if b0 == 0x60 else 'KeyB' + block = data[1] + if (key_type, block) not in auth_blocks: + auth_blocks.append((key_type, block)) + + # GENERATE AC — check AC type + if b0 == 0x80 and len(data) > 2 and data[1] == 0xae: + if (data[2] & 0xc0) == 0x80: + arqc_seen = True + if (data[2] & 0xc0) == 0x40: + tc_seen = True + + # GET DATA — ATC + if b0 == 0x80 and len(data) > 2 and data[1] == 0xca: + tag = (data[2] << 8) | data[3] + atc_tag = _known_bertag(tag) or f'{data[2]:02x}{data[3]:02x}' + + # GPO — extract amount if PDOL present + if b0 == 0x80 and len(data) > 4 and data[1] == 0xa8: + # Amount is usually first 6 bytes of PDOL data at offset 4+ + if len(data) >= 11: + amt_bytes = data[5:11] + amt = int.from_bytes(amt_bytes, 'big') + if amt > 0: + amount = amt + + # HALT / DESELECT + if b0 == 0x50 or b0 == 0xc2: + halted = True + + # Build UID from cascade levels + uid_bytes = None + if uid_cl1 and uid_cl2: + uid_bytes = uid_cl1 + uid_cl2 + if uid_cl3: + uid_bytes = uid_bytes + uid_cl3 + elif uid_cl1: + uid_bytes = uid_cl1 + + # Do NOT attempt to extract UID from anticoll frames (81-bit NVB=e1): + # those frames are the CU's own emulated tag responding, so the UID + # would always be the active slot's UID — not useful information. + # Only report UID when we see a completed SELECT (NVB=70). + + print(f" {'─'*55}") + if uid_bytes: + uid_str = ' '.join(f'{b:02X}' for b in uid_bytes) + cascade = f' ({len(uid_bytes)}-byte UID)' if uid_bytes else '' + print(f" {CC}UID :{C0} {CG}{uid_str}{cascade}{C0}") + if rats_seen: + print(f" {CC}Protocol :{C0} ISO14443-4 (RATS seen)") + for aid in aids: + print(f" {CC}AID :{C0} {CY}{aid}{C0}") + if amount is not None: + major = amount // 100 + minor = amount % 100 + print(f" {CC}Amount :{C0} {CG}{major}.{minor:02d}{C0} (raw={amount})") + if auth_blocks: + for key_type, block in auth_blocks: + print(f" {CC}Auth :{C0} {CR}MIFARE Classic {key_type} block={block}{C0}") + elif auth_seen: + print(f" {CC}Auth :{C0} {CR}MIFARE Classic auth detected{C0}") + if arqc_seen: + print(f" {CC}Auth type:{C0} {CR}ARQC — online authorisation requested{C0}") + if tc_seen: + print(f" {CC}Auth type:{C0} {CG}TC — approved offline{C0}") + if atc_tag: + print(f" {CC}ATC :{C0} tag {atc_tag} (transaction counter)") + if halted: + print(f" {CC}End :{C0} HALT / DESELECT") + if not uid_bytes and not aids and not auth_seen and not rats_seen: + print(f" {CC}Note :{C0} anti-collision incomplete — no SELECT seen (reader could not complete exchange)") + + +def _get_capture(): + """Return last capture buffer or print error.""" + import chameleon_cli_unit as _m + if not _m._last_capture: + return None + return _m._last_capture + + + + +@data.command('hexsamples') +class DataHexsamples(BaseCLIUnit): + def args_parser(self) -> ArgumentParserNoExit: + parser = ArgumentParserNoExit() + parser.description = 'Dump last LF sniff capture as hex bytes (PM3 style)' + parser.add_argument('-n', '--num', type=int, default=512, metavar='N', + help='Number of bytes to display (default: 512)') + return parser + + def on_exec(self, args: argparse.Namespace): + buf = _get_capture() + if buf is None: + print(f"{CR}No capture in buffer — run lf sniff first{C0}") + return + n = min(args.num, len(buf)) + print(f" Buffer: {CG}{len(buf)}{C0} bytes total, showing {n}") + print() + for row in range(0, n, 16): + chunk = buf[row:row+16] + hex_part = ' '.join(f'{b:02x}' for b in chunk) + bar = '' + for b in chunk: + if b < 0x10: + bar += '_' + elif b < 0x40: + bar += '.' + elif b < 0x80: + bar += '-' + elif b < 0xa0: + bar += '+' + elif b < 0xc0: + bar += 'o' + elif b < 0xe0: + bar += 'O' + else: + bar += '#' + print(f" {row // 16 :02d} | {hex_part:<47s} | {bar}") + print() + print(" _ gap . ringing - low + mid o carrier O high # clipped") + + + +@data.command('plot') +class DataPlot(BaseCLIUnit): + def args_parser(self) -> ArgumentParserNoExit: + parser = ArgumentParserNoExit() + parser.description = 'Graphical waveform plot of last LF sniff capture (PyQt5 or matplotlib)' + parser.add_argument('--start', type=int, default=0, metavar='N', + help='Start sample (default: 0)') + parser.add_argument('--len', type=int, default=4000, metavar='N', + help='Number of samples to plot (default: all)') + parser.add_argument('--ascii', action='store_true', + help='Force ASCII plot even if GUI is available') + return parser + + def on_exec(self, args: argparse.Namespace): + buf = _get_capture() + if buf is None: + print(f"{CR}No capture in buffer — run lf sniff first{C0}") + return + + start = max(0, args.start) + end = min(len(buf), start + args.len) + view = list(buf[start:end]) + n = len(view) + + # X axis: time in µs (1 sample = 8µs) + xs = [((start + i) * 8) for i in range(n)] + + mean = sum(view) // n + threshold = mean // 2 + + if not args.ascii: + # Try PyQt5 first, then matplotlib + try: + from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget + from PyQt5.QtCore import Qt + import pyqtgraph as pg + _plot_pyqtgraph(xs, view, mean, threshold, start, end) + return + except ImportError: + pass + try: + import matplotlib + matplotlib.use('Qt5Agg') + import matplotlib.pyplot as plt + _plot_matplotlib(xs, view, mean, threshold, start, end) + return + except ImportError: + pass + try: + import matplotlib.pyplot as plt + _plot_matplotlib(xs, view, mean, threshold, start, end) + return + except ImportError: + print(" No GUI library found (install PyQt5+pyqtgraph or matplotlib)") + print(" Falling back to ASCII plot...") + + # ASCII fallback + w = 64 + bsize = max(1, n // w) + buckets = [] + for i in range(0, n, bsize): + chunk = view[i:i+bsize] + buckets.append(sum(chunk) // len(chunk)) + buckets = buckets[:w] + mn, mx = min(view), max(view) + print(f" Samples {start}–{end} range 0x{mn:02x}–0x{mx:02x} mean 0x{mean:02x}") + print() + levels = [0xe0, 0xc0, 0xa0, 0x80, 0x60, 0x40, 0x20, 0x00] + labels = ['0xff','0xc0','0xa0','0x80','0x60','0x40','0x20','0x00'] + for thresh, lbl in zip(levels, labels): + row = ''.join('#' if v >= thresh else ' ' for v in buckets) + print(f" {lbl} |{row}|") + print(f" +{'-'*len(buckets)}+") + + + + +def _plot_matplotlib(xs, ys, mean, threshold, start, end): + import matplotlib.pyplot as plt + import matplotlib.patches as mpatches + + fig, ax = plt.subplots(figsize=(14, 5)) + fig.patch.set_facecolor('#1a1a2e') + ax.set_facecolor('#0d1117') + + # Main waveform + ax.plot(xs, ys, color='#00e5ff', linewidth=0.8, label='LF field') + + # Mean and gap threshold lines + ax.axhline(mean, color='#ffb020', linewidth=0.8, linestyle='--', label=f'mean 0x{mean:02x}') + ax.axhline(threshold, color='#ff3d57', linewidth=0.8, linestyle=':', label=f'gap threshold 0x{threshold:02x}') + + # Shade gap regions + in_gap = False + gap_start = 0 + for i, v in enumerate(ys): + if not in_gap and v < threshold: + in_gap = True + gap_start = xs[i] + elif in_gap and v >= threshold: + ax.axvspan(gap_start, xs[i], alpha=0.25, color='#ff3d57', linewidth=0) + in_gap = False + if in_gap: + ax.axvspan(gap_start, xs[-1], alpha=0.25, color='#ff3d57', linewidth=0) + + ax.set_xlabel('Time (µs)', color='#8899b4') + ax.set_ylabel('ADC value', color='#8899b4') + ax.set_title(f'LF Sniff — samples {start}–{end} ({(end-start)*8}µs)', + color='#dde8f5', fontsize=11) + ax.set_ylim(0, 270) + ax.set_xlim(xs[0], xs[-1]) + ax.tick_params(colors='#8899b4') + for spine in ax.spines.values(): + spine.set_edgecolor('#21262d') + ax.legend(facecolor='#161b22', edgecolor='#30363d', labelcolor='#c9d1d9', + fontsize=8, loc='upper right') + ax.grid(True, color='#21262d', linewidth=0.5) + + gap_patch = mpatches.Patch(color='#ff3d57', alpha=0.4, label='field gap') + handles, labels = ax.get_legend_handles_labels() + ax.legend(handles + [gap_patch], labels + ['field gap'], + facecolor='#161b22', edgecolor='#30363d', + labelcolor='#c9d1d9', fontsize=8, loc='upper right') + + plt.tight_layout() + plt.show() + + + + +def _plot_pyqtgraph(xs, ys, mean, threshold, start, end): + import sys + from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel + from PyQt5.QtCore import Qt + from PyQt5.QtGui import QFont + import pyqtgraph as pg + + pg.setConfigOption('background', '#0d1117') + pg.setConfigOption('foreground', '#8899b4') + + app = QApplication.instance() or QApplication(sys.argv) + + win = pg.GraphicsLayoutWidget(title='ChameleonUltra — LF Sniff') + win.resize(1200, 400) + win.setWindowTitle(f'LF Sniff — samples {start}–{end} ({(end-start)*8}µs)') + + plot = win.addPlot() + plot.setLabel('bottom', 'Time (µs)') + plot.setLabel('left', 'ADC value') + plot.showGrid(x=True, y=True, alpha=0.2) + plot.setYRange(0, 270) + + # Waveform + plot.plot(xs, ys, pen=pg.mkPen('#00e5ff', width=1)) + + # Mean line + plot.addLine(y=mean, pen=pg.mkPen('#ffb020', width=1, style=pg.QtCore.Qt.DashLine)) + # Gap threshold line + plot.addLine(y=threshold, pen=pg.mkPen('#ff3d57', width=1, style=pg.QtCore.Qt.DotLine)) + + # Shade gaps + for i in range(len(ys)-1): + if ys[i] < threshold: + r = pg.LinearRegionItem([xs[i], xs[i+1]], + brush=pg.mkBrush(255, 61, 87, 40), + pen=pg.mkPen(None), movable=False) + plot.addItem(r) + + # Legend / info panel + legend_text = ( + '' + ' LF field (ADC)  ' + '- - Mean  ' + '··· Gap threshold (mean÷2)  ' + '   ' + ' Field gap (below threshold)  ' + 'Ringing = exponential rise on field restore' + '' + ) + legend = pg.LabelItem(legend_text, justify='left') + win.addItem(legend, row=1, col=0) + + win.show() + app.exec_() + + + + +@data.command('manrawdecode') +class DataManrawdecode(BaseCLIUnit): + def args_parser(self) -> ArgumentParserNoExit: + parser = ArgumentParserNoExit() + parser.description = 'Manchester decode the last LF sniff capture' + parser.add_argument('--clock', type=int, default=64, metavar='N', + help='Clock divisor in Tc (default: 64 = RF/64)') + parser.add_argument('--invert', action='store_true', + help='Invert logic (high=0, low=1)') + return parser + + def on_exec(self, args: argparse.Namespace): + buf = _get_capture() + if buf is None: + print(f"{CR}No capture in buffer — run lf sniff first{C0}") + return + + # Binarise: above mean = 1 (carrier), below = 0 (gap) + mean = sum(buf) // len(buf) + threshold = mean // 2 + bits_raw = [1 if b > threshold else 0 for b in buf] + if args.invert: + bits_raw = [1 - b for b in bits_raw] + + # Find transitions and measure run lengths + runs = [] + cur = bits_raw[0] + count = 1 + for b in bits_raw[1:]: + if b == cur: + count += 1 + else: + runs.append((cur, count)) + cur = b + count = 1 + runs.append((cur, count)) + + # Clock period in samples (1 sample = 8µs) + half_clk = args.clock // 2 # samples per half-bit + + # Decode Manchester: half-bit transitions + # Low->High = 0, High->Low = 1 (standard Manchester) + decoded_bits = [] + tol = max(2, half_clk // 3) + + i = 0 + while i < len(runs): + val, cnt = runs[i] + # Short run = half period, long run = full period + half = abs(cnt - half_clk) <= tol + full = abs(cnt - args.clock) <= tol + if half: + # need next run to complete bit + if i + 1 < len(runs): + nval, ncnt = runs[i+1] + nhalf = abs(ncnt - half_clk) <= tol + if nhalf: + # two halves: transition val->nval + if val == 0 and nval == 1: + decoded_bits.append(0) + elif val == 1 and nval == 0: + decoded_bits.append(1) + i += 2 + continue + elif full: + # biphase / stay same level for full period = repeated bit + decoded_bits.append(val) + i += 1 + + if not decoded_bits: + print(f"{CR}No bits decoded — check clock rate or signal quality{C0}") + print(f" Mean threshold: 0x{threshold:02x} Clock: RF/{args.clock}") + return + + bits_str = ''.join(str(b) for b in decoded_bits) + hex_str = hex(int(bits_str, 2))[2:] if decoded_bits else '' + + print(f" Clock : RF/{args.clock} ({args.clock} Tc = {args.clock*8}µs/bit)") + print(f" Threshold: 0x{threshold:02x} Inverted: {args.invert}") + print(f" Bits : {CG}{len(decoded_bits)}{C0}") + print() + # Print in rows of 64 + for i in range(0, len(bits_str), 64): + print(f" {bits_str[i:i+64]}") + if hex_str: + print() + print(f" Hex: {CG}{hex_str[:64]}{C0}{'...' if len(hex_str) > 64 else ''}") + + + + +@data.command('modulation') +class DataModulation(BaseCLIUnit): + def args_parser(self) -> ArgumentParserNoExit: + parser = ArgumentParserNoExit() + parser.description = 'Detect clock rate and modulation type in last LF capture' + return parser + + def on_exec(self, args: argparse.Namespace): + buf = _get_capture() + if buf is None: + print(f"{CR}No capture in buffer — run lf sniff first{C0}") + return + + n = len(buf) + mean = sum(buf) // n + mn = min(buf) + mx = max(buf) + threshold = mean // 2 + + print(f" Samples : {CG}{n}{C0} ({n*8}µs)") + print(f" Range : 0x{mn:02x} – 0x{mx:02x} mean: 0x{mean:02x}") + print() + + # Check if there is any modulation at all + dynamic_range = mx - mn + if dynamic_range < 0x20: + print(f" Modulation: {CR}none — flat carrier (no signal){C0}") + return + + # Binarise + bits = [1 if b > threshold else 0 for b in buf] + + # Measure run lengths (periods between transitions) + runs = [] + cur = bits[0] + count = 1 + for b in bits[1:]: + if b == cur: + count += 1 + else: + runs.append(count) + cur = b + count = 1 + + runs.append(count) + + if len(runs) < 4: + print(f" Modulation: {CR}insufficient transitions{C0}") + return + + runs_sorted = sorted(runs) + # Remove outliers (top/bottom 10%) + trim = max(1, len(runs) // 10) + + # Estimate clock: most common run length = half-period + from collections import Counter + run_counts = Counter(runs) + most_common_run = run_counts.most_common(1)[0][0] + + # Map to nearest standard RF divider + half_samples = most_common_run + full_period_us = half_samples * 2 * 8 # us + + rf_dividers = [8, 16, 32, 40, 50, 64, 100, 128] + tc_us = 8 # 1 Tc = 8µs at 125kHz + best_div = min(rf_dividers, key=lambda d: abs(d*tc_us - full_period_us)) + + print(f" Half-period : ~{most_common_run} samples = {most_common_run*8}µs") + print(f" Full period : ~{full_period_us}µs") + print(f" Nearest RF : {CG}RF/{best_div}{C0} ({best_div*tc_us}µs/bit)") + print() + + # Modulation type heuristic + # Manchester: runs cluster around 1 value (half period) and 2x that (full period) + # ASK/NRZ: long runs of same value + # FSK: two distinct run lengths alternating + + unique_runs = set(runs) + long_runs = [r for r in runs if r > most_common_run * 3] + + # Manchester has runs clustering at N and 2N (half and full period) + # Check if second most common run is ~2x the most common + tol = max(2, most_common_run // 3) + top2 = run_counts.most_common(2) + is_manchester = (len(top2) >= 2 and + abs(top2[1][0] - most_common_run * 2) <= tol) + + if len(long_runs) > len(runs) * 0.3: + mod = "ASK / NRZ (long steady periods)" + col = CG + elif is_manchester: + mod = f"Manchester (RF/{best_div})" + col = CG + elif len(unique_runs) <= 4: + mod = f"Biphase (RF/{best_div})" + col = CG + else: + mod = "FSK or mixed (multiple run lengths)" + col = CG + + print(f" Modulation : {col}{mod}{C0}") + + # Gap detection + gap_threshold = mean // 2 + gaps = [i for i, b in enumerate(buf[200:]) if b < gap_threshold] + + + if gaps: + print(f" RTF gaps : {CG}{len(gaps)}{C0} samples below 0x{gap_threshold:02x}" + f" ^`^t gap commands present") + else: + print(f" RTF gaps : {CR}none ^`^t no gap commands detected{C0}") + diff --git a/software/script/chameleon_cmd.py b/software/script/chameleon_cmd.py index f541715..932f9eb 100644 --- a/software/script/chameleon_cmd.py +++ b/software/script/chameleon_cmd.py @@ -425,6 +425,22 @@ class ChameleonCMD: i += 14 return resp + def hf14a_sniff(self, timeout_ms: int = 5000): + """ + Capture ISO14443A reader frames while CU acts as a tag emulator. + + The firmware installs a sniff callback into the HF14A stack for the + requested duration, then returns all captured frames packed as: + [2 bytes: bit count, big-endian] [N bytes: frame data, ceil(bits/8)] ... + + :param timeout_ms: Listen duration in ms (1-30000, default 5000) + :return: Raw response — check .status and .data + """ + timeout_ms = max(1, min(30000, timeout_ms)) + payload = bytes([(timeout_ms >> 8) & 0xFF, timeout_ms & 0xFF]) + timeout_s = (timeout_ms // 1000) + 5 + return self.device.send_cmd_sync(Command.HF14A_SNIFF, payload, timeout=timeout_s) + @expect_response(Status.SUCCESS) def hf14a_get_config(self): """ @@ -555,6 +571,48 @@ class ChameleonCMD: resp.parsed = struct.unpack(">BBH8sBBBB", resp.data[:16]) return resp + + + def lf_sniff(self, timeout_ms: int = 2000): + """ + Capture raw LF field ADC samples. + + The ChameleonUltra samples the LF antenna at 125kHz (8µs/sample). + Each byte is an 8-bit ADC value: ~0x80 = field on, lower = gap/no field. + + :param timeout_ms: Capture duration in ms (1-10000, default 2000) + :return: Raw response object — check .status and .data + """ + timeout_ms = max(1, min(10000, timeout_ms)) + payload = bytes([(timeout_ms >> 8) & 0xFF, timeout_ms & 0xFF]) + timeout_s = (timeout_ms // 1000) + 2 + return self.device.send_cmd_sync(Command.LF_SNIFF, payload, timeout=timeout_s) + + + + @expect_response(Status.LF_TAG_OK) + def em4x05_scan(self, pwd: int = 0): + """ + Read an EM4x05 or EM4x69 tag (reader-talk-first). + + Response payload (14 bytes, big-endian): + config 4 bytes — block 0 configuration word + uid 4 bytes — EM4x05 UID + uid_hi 4 bytes — EM4x69 uid_hi (zero for plain EM4x05) + is_em4x69 1 byte — 1 if a 64-bit EM4x69 UID was read + uid_block 1 byte — block number UID was read from + + :param pwd: 32-bit password for LOGIN (default 0x00000000) + :return: parsed tuple (config, uid, uid_hi, is_em4x69, uid_block) + """ + pwd_bytes = struct.pack('!I', pwd & 0xFFFFFFFF) + resp = self.device.send_cmd_sync(Command.EM4X05_SCAN, pwd_bytes) + if resp.status == Status.LF_TAG_OK: + resp.parsed = struct.unpack('!IIIBB', resp.data[:14]) + return resp + + + @expect_response(Status.LF_TAG_OK) def viking_scan(self): """ @@ -580,6 +638,31 @@ 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 pac_write_to_t55xx(self, id_bytes: bytes): + """ + Write PAC/Stanley card data to a T55XX tag. + + :param id_bytes: 8-byte ASCII card ID + :return: + """ + if len(id_bytes) != 8: + raise ValueError("The id bytes length must equal 8") + data = struct.pack(f'!8s4s{4*len(old_keys)}s', id_bytes, new_key, b''.join(old_keys)) + return self.device.send_cmd_sync(Command.PAC_WRITE_TO_T55XX, data) + @expect_response(Status.LF_TAG_OK) def adc_generic_read(self): """ @@ -813,6 +896,28 @@ class ChameleonCMD: resp.parsed = resp.data return resp + @expect_response(Status.SUCCESS) + def pac_set_emu_id(self, id: bytes): + """ + Set the card ID emulated by PAC/Stanley. + + :param id: 8-byte ASCII card ID + :return: + """ + if len(id) != 8: + raise ValueError("The id bytes length must equal 8") + data = struct.pack('8s', id) + return self.device.send_cmd_sync(Command.PAC_SET_EMU_ID, data) + + @expect_response(Status.SUCCESS) + def pac_get_emu_id(self): + """ + Get the emulated PAC/Stanley card ID + """ + resp = self.device.send_cmd_sync(Command.PAC_GET_EMU_ID) + resp.parsed = resp.data[:8] + return resp + @expect_response(Status.SUCCESS) def mf1_set_detection_enable(self, enabled: bool): """ diff --git a/software/script/chameleon_enum.py b/software/script/chameleon_enum.py index c159762..4b4baef 100644 --- a/software/script/chameleon_enum.py +++ b/software/script/chameleon_enum.py @@ -75,6 +75,7 @@ class Command(enum.IntEnum): MF1_CHECK_KEYS_ON_BLOCK = 2015 HF14A_GET_CONFIG = 2200 HF14A_SET_CONFIG = 2201 + HF14A_SNIFF = 2020 EM410X_SCAN = 3000 EM410X_WRITE_TO_T55XX = 3001 @@ -83,6 +84,8 @@ class Command(enum.IntEnum): HIDPROX_WRITE_TO_T55XX = 3003 VIKING_SCAN = 3004 VIKING_WRITE_TO_T55XX = 3005 + PAC_SCAN = 3014 + PAC_WRITE_TO_T55XX = 3015 ADC_GENERIC_READ = 3009 IOPROX_SCAN = 3010 IOPROX_WRITE_TO_T55XX = 3011 @@ -141,8 +144,13 @@ class Command(enum.IntEnum): HIDPROX_GET_EMU_ID = 5003 VIKING_SET_EMU_ID = 5004 VIKING_GET_EMU_ID = 5005 + PAC_SET_EMU_ID = 5006 + PAC_GET_EMU_ID = 5007 IOPROX_SET_EMU_ID = 5008 IOPROX_GET_EMU_ID = 5009 + EM4X05_SCAN = 3030 + EM4X05_READSNIFF = 3032 + LF_SNIFF = 3031 @enum.unique @@ -276,6 +284,7 @@ class TagSpecificType(enum.IntEnum): # securakey # gallagher # PAC/Stanley + PAC = 150 # Presco # Visa2000 Viking = 170 @@ -369,6 +378,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: