LupusE requested in the PR #407 review (2026-05-13) to move IDTECK
command IDs since PR #404 (Jablotron) also uses 3017 / 5010 / 5011.
Both PRs are open with the feature-freeze label; Jablotron retains
the original slots and IDTECK shifts up:
DATA_CMD_IDTECK_WRITE_TO_T55XX: 3017 -> 3018
DATA_CMD_IDTECK_SET_EMU_ID: 5010 -> 5012
DATA_CMD_IDTECK_GET_EMU_ID: 5011 -> 5013
Files changed:
- firmware/application/src/data_cmd.h
- software/script/chameleon_enum.py
app_cmd.c references the constants by name only, no edit needed there.
Exposes IDTECK to the host command protocol:
- DATA_CMD_IDTECK_SET_EMU_ID (5010) / GET_EMU_ID (5011) / WRITE_TO_T55XX (3017)
- Matching handlers in app_cmd.c for setting the emulated frame on
the current LF slot, reading it back, and programming a T55xx tag
Adds write_idteck_to_t55xx in lf_reader_main (modeled on the other
per-protocol T55xx writers), wrapping idteck_t55xx_writer and the
shared write_t55xx helper.
After this commit the firmware is fully functional for IDTECK: a
host can set an emulated frame, read it back, or clone it to a T55xx.
The CLI wiring is added in the following commit.
Adds IDTECK as a new LF protocol for tag emulation. IDTECK is a PSK1
encoding at RF/32 with a 64-bit frame: a 32-bit fixed preamble
0x4944544B ("IDTK") followed by a 32-bit card payload (one-byte
checksum + 24-bit card number in byte-reversed layout, matching the
format used by the Proxmark3 client).
The modulator drives LF_MOD (load-modulation, same hardware path used
for FSK protocols like HID Prox) via the shared utils/psk1 helper,
producing a 62.5kHz subcarrier with a 180-degree phase flip at every
differential bit transition. Because PSK1 is differential the reader
decodes phase transitions between consecutive bits rather than
absolute phase, so carrier phase-lock is not required — a free-running
subcarrier from HFXO (±40ppm) stays within the tolerance of consumer
readers.
The 16us subcarrier period is below the counter_top minimum of 3 at
the legacy 125kHz PWM base clock used for ASK/FSK protocols. To avoid
rescaling every existing protocol, pwm_init now selects the base
clock based on the active tag type (predicate IS_PSK1_TYPE): 1MHz for
PSK1, 125kHz otherwise. Legacy protocols are untouched.
The comment in lf_sense_enable is updated to reflect that the absence
of carrier phase-lock (envelope-only tag-mode antenna taps) rules out
coherent demod but does not preclude differential-phase encodings
like the one introduced here.
T5577 cloning configuration uses the existing T5577_MODULATION_PSK1
symbol combined with RF/32 bitrate and 2 data blocks. Emulation read
is not added: the tag-emulation ADC path is 125kHz envelope-filtered,
so PSK demod would need a dedicated edge-timing decoder (left as a
follow-up).
Factors out the PSK1 subcarrier generator into utils/psk1.{c,h}.
The helper takes a frame (MSB-first bytes), a bit count and a
destination wave-form buffer, and fills the buffer with PWM entries
expressing differential PSK1 as polarity flips at bit transitions.
No protocol uses this helper yet; it is introduced alone so that
individual PSK1 protocol files (starting with IDTECK in the next
commit) can plug into the same timing and encoding logic without
each re-implementing it.
The helper targets the 1MHz PWM base clock that will be selected by
pwm_init for PSK1 tag types; counter_top and duty constants are
defined accordingly.
The fix in EVT_END_SEQ0:
1. ANT_NO_MOD() — silences LF_MOD so the local drive no longer charges the peak detector
2. bsp_delay_ms(2) — 2ms settle, slightly above the ~2ms time constant so the detector drains to reflect only the external field
3. is_lf_field_exists() — now sees the real field state
4. If field gone → nrfx_pwm_stop() → EVT_STOPPED → lf_field_lost() runs correctly
5. If field present → ANT_MOD() restores modulation for the next PWM sequence
Split the single --id argument into --cn (8 ASCII chars) and --raw
(32 hex char T55XX bitstream, directly compatible with PM3 raw output).
Add Python-side PAC bitstream encoder/decoder for raw format support.
Output now shows CN and Raw labels matching PM3's format.
Add NRF_LOG module registration to pac.c for debug logging,
consistent with other protocol implementations.
Reassign PAC command IDs (3014/3015) to avoid collision with ioProx
(3010/3011) after rebase onto upstream/main.
Three fixes that together bring rapid-fire read reliability from ~20%
to 100%:
- Add MIN_SPIKE_CAP floor (8000) to prevent spike_cap from clipping
NRZ high when prescan correctly captures NRZ low. Without this,
spike_cap = raw_min*3 ≈ 2820 collapses the signal range.
- Reorder carrier-before-SAADC in pac_read(): start the 125kHz field
and wait 10ms before enabling ADC sampling, so prescan calibration
sees real NRZ signal levels rather than T55XX power-on-reset noise.
- Add auto-recalibration: if no valid frame is found after 20480
Phase 3 samples (~164ms, ~5 frame periods), reset the decoder to
Phase 1 and re-calibrate from fresh samples. This gives ~3
calibration attempts per 500ms scan window instead of just one.
Tested with Proxmark3 sim (15 consecutive rapid-fire reads, 100%) and
T55XX tag (write-read roundtrip + 15x rapid-fire, 100%).
Add pac_t55xx_writer() for encoding PAC card data into T55XX blocks,
along with the T5577_PAC_CONFIG (NRZ/Direct, RF/32, password-protected,
4 data blocks). Wire DATA_CMD_PAC_WRITE_TO_T55XX (3011) through the
command processor, dispatch table, and Python client.
Replace the 32-sample moving average + hysteresis demodulation with
Proxmark3-inspired per-sample thresholding and dead zone. This
eliminates ~16 samples of group delay per edge, reducing timing
jitter from ~11 samples to ~2-3 samples.
The new approach:
- Prescan: track raw_min, compute spike_cap (unchanged)
- Warmup: track min/max of clipped samples directly (not averaged)
- Detection: per-sample dead zone classification — sample >= high
threshold → 1, sample <= low threshold → 0, between → keep
previous state. Thresholds set at 75% fuzz of signal range.
Removes the avg_buf[32] circular buffer, avg_sum, avg_idx, and
sum-unit threshold/hysteresis state. Struct is 72 bytes smaller.
Widen integer types to prevent overflow UB:
- sample_count: uint16_t -> uint32_t (overflows at 524ms)
- interval, nbits: uint16_t -> uint32_t (matching sample_count width)