From c494a2cc81cce1e162c14d91c662a32df91c810b Mon Sep 17 00:00:00 2001 From: Kevin Yuan Date: Wed, 11 Feb 2026 00:06:09 +0000 Subject: [PATCH] Add PAC/Stanley LF tag reading support Implements NRZ/Direct modulation decoder for PAC/Stanley 125kHz cards using SAADC ADC sampling with spike-aware threshold calibration. The LC antenna produces brief high-amplitude transients at NRZ transitions which are clipped before the moving-average filter to isolate the actual data levels. --- firmware/application/Makefile | 2 + firmware/application/src/app_cmd.c | 10 + firmware/application/src/data_cmd.h | 1 + .../src/rfid/nfctag/lf/protocols/pac.c | 297 ++++++++++++++++++ .../src/rfid/nfctag/lf/protocols/pac.h | 5 + .../src/rfid/nfctag/tag_base_type.h | 3 +- .../src/rfid/reader/lf/lf_pac_data.c | 70 +++++ .../src/rfid/reader/lf/lf_reader_data.h | 1 + .../src/rfid/reader/lf/lf_reader_main.c | 11 + .../src/rfid/reader/lf/lf_reader_main.h | 1 + software/script/chameleon_cli_unit.py | 105 +++++++ software/script/chameleon_cmd.py | 12 + software/script/chameleon_enum.py | 4 + 13 files changed, 521 insertions(+), 1 deletion(-) create mode 100644 firmware/application/src/rfid/nfctag/lf/protocols/pac.c create mode 100644 firmware/application/src/rfid/nfctag/lf/protocols/pac.h create mode 100644 firmware/application/src/rfid/reader/lf/lf_pac_data.c diff --git a/firmware/application/Makefile b/firmware/application/Makefile index 0e2b231..1d011df 100644 --- a/firmware/application/Makefile +++ b/firmware/application/Makefile @@ -37,6 +37,7 @@ SRC_FILES += \ $(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/viking.c \ $(PROJ_DIR)/rfid/nfctag/lf/protocols/wiegand.c \ $(PROJ_DIR)/utils/dataframe.c \ @@ -346,6 +347,7 @@ ifeq (${CURRENT_DEVICE_TYPE}, ${CHAMELEON_ULTRA}) $(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_viking_data.c \ $(PROJ_DIR)/rfid/reader/lf/lf_reader_generic.c \ diff --git a/firmware/application/src/app_cmd.c b/firmware/application/src/app_cmd.c index a8ada47..6fb0f58 100644 --- a/firmware/application/src/app_cmd.c +++ b/firmware/application/src/app_cmd.c @@ -802,6 +802,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]; @@ -1798,6 +1807,7 @@ 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_ADC_GENERIC_READ, before_reader_run, cmd_processor_generic_read, NULL }, { DATA_CMD_HF14A_SET_FIELD_ON, before_reader_run, cmd_processor_hf14a_set_field_on, NULL }, diff --git a/firmware/application/src/data_cmd.h b/firmware/application/src/data_cmd.h index 51ac5fc..c7c054a 100644 --- a/firmware/application/src/data_cmd.h +++ b/firmware/application/src/data_cmd.h @@ -93,6 +93,7 @@ #define DATA_CMD_EM410X_ELECTRA_WRITE_TO_T55XX (3006) #define DATA_CMD_HIDPROX_SCAN (3002) #define DATA_CMD_HIDPROX_WRITE_TO_T55XX (3003) +#define DATA_CMD_PAC_SCAN (3010) #define DATA_CMD_VIKING_SCAN (3004) #define DATA_CMD_VIKING_WRITE_TO_T55XX (3005) #define DATA_CMD_ADC_GENERIC_READ (3009) diff --git a/firmware/application/src/rfid/nfctag/lf/protocols/pac.c b/firmware/application/src/rfid/nfctag/lf/protocols/pac.c new file mode 100644 index 0000000..9b11985 --- /dev/null +++ b/firmware/application/src/rfid/nfctag/lf/protocols/pac.c @@ -0,0 +1,297 @@ +#include "pac.h" + +#include +#include + +#include "protocols.h" +#include "tag_base_type.h" + +#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 parameters +#define PAC_AVG_WINDOW 32 // Moving average window = 1 NRZ bit period +#define PAC_PRESCAN_SAMPLES 128 // Samples for raw min/max detection (spike cap) +#define PAC_WARMUP_SAMPLES 600 // Samples for threshold calibration (~5ms) +#define PAC_MIN_HYST_SUM (10 * PAC_AVG_WINDOW) // Min hysteresis in sum units +#define PAC_SPIKE_MULT 3 // Samples > raw_min * SPIKE_MULT are spike transients + +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 (moving-average based) + int16_t avg_buf[PAC_AVG_WINDOW]; // circular buffer for moving average + int32_t avg_sum; // running sum of last PAC_AVG_WINDOW samples + uint8_t avg_idx; // circular index into avg_buf + uint16_t total_samples; // total samples processed + int16_t raw_min; // minimum raw sample seen (for spike detection) + int16_t spike_cap; // clip level: raw values above this are transients + int32_t avg_max; // max of avg_sum during warmup (spike-free) + int32_t avg_min; // min of avg_sum during warmup (spike-free) + int32_t threshold; // center threshold (in sum units, not divided) + int32_t hysteresis; // hysteresis margin (in sum units) + bool adc_state; // current demodulated binary level + bool has_signal; // true after first threshold crossing + uint16_t sample_count; // samples since last transition +} 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, uint16_t interval) { + uint16_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 (uint16_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)); + // Initialize min/max to extremes so first comparison updates them + d->raw_min = 32767; // INT16_MAX for raw min tracking + d->spike_cap = 32767; // No capping until prescan completes + d->avg_max = -1048576; + d->avg_min = 1048576; +} + +// Feed a raw ADC sample (one per carrier cycle at 125kHz). +// The antenna signal has brief 0xFF transient spikes at NRZ transitions due to +// LC circuit ringing. The actual NRZ data is encoded as two lower amplitude +// levels. We clip spikes before averaging, then threshold the clean signal. +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 characterize data levels. + // The NRZ data levels are the lowest values; spikes are 5-15x higher. + 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) { + // Set spike cap: anything above 3x the minimum is a transient. + // This keeps both NRZ data levels but removes the LC ringing spikes. + d->spike_cap = d->raw_min * PAC_SPIKE_MULT; + } + return false; + } + + // Clip spikes: replace transient values with the cap level. + // This prevents the LC ringing spikes from dominating the moving average. + if (sample > d->spike_cap) { + sample = d->spike_cap; + } + + // Update 32-sample moving average (window = 1 NRZ bit period at RF/32). + d->avg_sum -= d->avg_buf[d->avg_idx]; + d->avg_buf[d->avg_idx] = sample; + d->avg_sum += sample; + d->avg_idx = (d->avg_idx + 1) % PAC_AVG_WINDOW; + + uint16_t avg_samples = d->total_samples - PAC_PRESCAN_SAMPLES; + + // Need a full window before the average is valid + if (avg_samples < PAC_AVG_WINDOW) { + return false; + } + + // Warmup: track min/max of the spike-free averaged signal. + if (avg_samples < PAC_WARMUP_SAMPLES) { + if (d->avg_sum > d->avg_max) d->avg_max = d->avg_sum; + if (d->avg_sum < d->avg_min) d->avg_min = d->avg_sum; + return false; + } + + // Compute threshold once at end of warmup + if (avg_samples == PAC_WARMUP_SAMPLES) { + d->threshold = (d->avg_max + d->avg_min) / 2; + d->hysteresis = (d->avg_max - d->avg_min) / 4; + if (d->hysteresis < PAC_MIN_HYST_SUM) { + d->hysteresis = PAC_MIN_HYST_SUM; + } + } + + // Detection: threshold the averaged signal with hysteresis + d->sample_count++; + + bool new_state; + if (d->avg_sum > d->threshold + d->hysteresis) { + new_state = true; + } else if (d->avg_sum < d->threshold - d->hysteresis) { + 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 + uint16_t interval = d->sample_count; + d->sample_count = 0; + d->adc_state = new_state; + + return pac_process_interval(d, interval); +} + +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 = NULL, + .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..e9a6d46 --- /dev/null +++ b/firmware/application/src/rfid/nfctag/lf/protocols/pac.h @@ -0,0 +1,5 @@ +#pragma once + +#include "protocols.h" + +extern const protocol pac; 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/reader/lf/lf_pac_data.c b/firmware/application/src/rfid/reader/lf/lf_pac_data.c new file mode 100644 index 0000000..1c5278d --- /dev/null +++ b/firmware/application/src/rfid/reader/lf/lf_pac_data.c @@ -0,0 +1,70 @@ +#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); + + cb_init(&cb, PAC_BUFFER_SIZE, sizeof(uint16_t)); + init_pac_hw(); + start_lf_125khz_radio(); + + 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_main.c b/firmware/application/src/rfid/reader/lf/lf_reader_main.c index 556bef4..458637a 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 */ 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..96f65b3 100644 --- a/firmware/application/src/rfid/reader/lf/lf_reader_main.h +++ b/firmware/application/src/rfid/reader/lf/lf_reader_main.h @@ -13,6 +13,7 @@ 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); diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index c9818a5..245ddaa 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -757,6 +757,7 @@ 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") @@ -5763,6 +5764,110 @@ class LFIOProxEconfig(SlotIndexArgsAndGoUnit, LFIOProxIdArgsUnit): print(f" ID: {color_string((CY, cn))}") print(f" Raw: {color_string((CY, raw8.hex().upper()))}") +@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) + print(f" PAC/Stanley Card ID: {color_string((CG, card_id_ascii))} ({card_id.hex().upper()})") + + +@lf_pac.command('debug') +class LFPacDebug(ReaderRequiredUnit): + def args_parser(self) -> ArgumentParserNoExit: + parser = ArgumentParserNoExit() + parser.description = 'Capture raw ADC data and analyze NRZ signal for PAC debugging' + return parser + + def on_exec(self, args: argparse.Namespace): + resp = self.cmd.adc_generic_read() + if resp is None: + print("ADC read failed") + return + + data = list(resp) + min_val = min(data) + max_val = max(data) + + print(f" Samples: {len(data)}") + print(f" ADC range: {min_val} - {max_val} (spread: {max_val - min_val})") + + # Spike removal: clip values above 3x the minimum (LC ringing transients) + spike_cap = min_val * 3 if min_val > 0 else max_val + clipped = [min(v, spike_cap) for v in data] + c_min = min(clipped) + c_max = max(clipped) + c_thresh = (c_min + c_max) / 2 + + print(f" Spike cap: {spike_cap} (3x raw min {min_val})") + print(f" Clipped range: {c_min} - {c_max} (spread: {c_max - c_min})") + + # Moving-average of clipped signal (32-sample window = 1 NRZ bit) + window = 32 + if len(clipped) >= window: + filtered = [] + s = sum(clipped[:window]) + for i in range(window, len(clipped)): + filtered.append(s / window) + s += clipped[i] - clipped[i - window] + filtered.append(s / window) + f_min = min(filtered) + f_max = max(filtered) + f_thresh = (f_min + f_max) / 2 + f_hyst = (f_max - f_min) / 4 + print(f"\n Spike-free moving average (window={window}):") + print(f" Range: {f_min:.1f} - {f_max:.1f} (spread: {f_max - f_min:.1f})") + print(f" Threshold: {f_thresh:.1f}, Hysteresis: {f_hyst:.1f}") + + # Binary decode with hysteresis + f_binary = [] + state = filtered[0] > f_thresh + for v in filtered: + if v > f_thresh + f_hyst: + state = True + elif v < f_thresh - f_hyst: + state = False + f_binary.append(1 if state else 0) + + f_trans = sum(1 for i in range(1, len(f_binary)) if f_binary[i] != f_binary[i - 1]) + f_runs = [] + cur = f_binary[0] + cnt = 1 + for i in range(1, len(f_binary)): + if f_binary[i] == cur: + cnt += 1 + else: + f_runs.append(cnt) + cur = f_binary[i] + cnt = 1 + f_runs.append(cnt) + print(f" Transitions: {f_trans}") + print(f" Run lengths: {f_runs[:40]}{'...' if len(f_runs) > 40 else ''}") + run_bits = [f"{r / 32:.1f}" for r in f_runs[:40]] + print(f" Run bits: {run_bits}") + + # Show bit-period averages (each = avg of 32 clipped samples) + print(f"\n Bit-period averages (32 samples each):") + bit_avgs = [] + for i in range(0, len(clipped) - window + 1, window): + chunk = clipped[i:i + window] + bit_avgs.append(sum(chunk) / len(chunk)) + bit_str = " ".join(f"{a:.0f}" for a in bit_avgs) + print(f" {bit_str}") + + # Raw hex dump (first 512 bytes) + print(f"\n Raw ADC (8-bit, min={min_val} max={max_val}):") + for i in range(0, min(len(data), 512), 50): + chunk = data[i:i + 50] + hexpart = " ".join(f"{b:02x}" for b in chunk) + print(f" {i:04x} {hexpart}") + + @lf_viking.command("read") class LFVikingRead(ReaderRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: diff --git a/software/script/chameleon_cmd.py b/software/script/chameleon_cmd.py index f541715..72b2985 100644 --- a/software/script/chameleon_cmd.py +++ b/software/script/chameleon_cmd.py @@ -580,6 +580,18 @@ 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 adc_generic_read(self): """ diff --git a/software/script/chameleon_enum.py b/software/script/chameleon_enum.py index c159762..810542d 100644 --- a/software/script/chameleon_enum.py +++ b/software/script/chameleon_enum.py @@ -83,6 +83,7 @@ class Command(enum.IntEnum): HIDPROX_WRITE_TO_T55XX = 3003 VIKING_SCAN = 3004 VIKING_WRITE_TO_T55XX = 3005 + PAC_SCAN = 3010 ADC_GENERIC_READ = 3009 IOPROX_SCAN = 3010 IOPROX_WRITE_TO_T55XX = 3011 @@ -276,6 +277,7 @@ class TagSpecificType(enum.IntEnum): # securakey # gallagher # PAC/Stanley + PAC = 150 # Presco # Visa2000 Viking = 170 @@ -369,6 +371,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: