Raw LF field capture, graphical waveform plotting, Manchester decode, and modulation detection —
a PM3-style analysis toolkit built into the ChameleonUltra Python CLI.
When debugging LF RFID protocols on the ChameleonUltra, you often need to answer a basic question: is the field actually doing what the firmware thinks it is? For reader-talk-first protocols like EM4305, the entire exchange depends on gap-encoded commands being sent correctly into the field. Without a way to observe the field directly, a firmware bug that silently prevents gaps from being sent looks identical to a tag that isn't responding — both produce LF tag not found.
The ProxMark3 has lf sniff and a full suite of data analysis commands for exactly this reason. These commands bring the same capability to the ChameleonUltra, using the SAADC already present on the nRF52840 to sample the antenna field at 125kHz.
The nRF52840 SAADC samples the LF antenna voltage at 125kHz via PPI from the PWM period end event. Each sample is an 8-bit value: ~0xb0 = steady carrier, 0x00 = field off (gap). When the PWM stops for a gap, sampling stops too — gaps appear as absent samples and as zero-value bytes in the output.
Turns the LF antenna on, captures ADC samples, and stores them in memory for the data commands. The capture buffer holds up to 4000 bytes — 32ms of signal at 125kHz.
lf sniff
lf sniff --timeout 500
lf sniff --timeout 2000 --hex
lf sniff --timeout 2000 --out capture.bin
| Flag | Default | Description |
|---|---|---|
| --timeout MS | 2000 | Capture duration in milliseconds (max 10000) |
| --hex | off | Print hex dump with level indicators immediately after capture |
| --out FILE | none | Save raw bytes to binary file for offline analysis |
After capture, the summary line tells you immediately whether the signal looks useful:
Captured : 4000 bytes (32.0ms)
Range : 0x00 – 0xff mean: 0x76
Gaps : 1874 samples below 0x3b (real field drops)
A flat carrier with no tag reads 0xb0–0xb3 with no gaps. A modulated signal (tag broadcasting or RTF gaps being sent) shows a wide range and gap count above zero.
Dumps the last capture as a hex table with a signal level indicator column on the right — the same format as pm3 data hexsamples but with an added visual level column making gaps immediately visible without reading hex values.
data hexsamples
data hexsamples -n 512
00 | ff ff 01 00 ff ff ff ff ff ff ff ff ff ff ff ff | ##__############
01 | ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff | ################
02 | ff ff ff ff ff c7 88 3c 01 01 00 00 00 01 01 01 | #####O+.________
03 | 01 01 01 00 01 00 01 02 00 00 01 00 01 01 00 01 | ________________
04 | 00 00 01 ff ff ff ff ff ff ff ff ff ff ff ff ff | ___#############
The level column maps each byte to a character representing signal strength:
In the example above, row 00 shows ##__#### — two clipped carrier samples, then two gap samples, then carrier restored. That is one Manchester bit transition at RF/64.
Opens a live graphical waveform window. Uses pyqtgraph if installed (recommended — interactive, zoomable), falls back to matplotlib, then to an ASCII plot in the terminal.
data plot
data plot --start 100 --len 200
data plot --ascii
| Flag | Default | Description |
|---|---|---|
| --start N | 0 | First sample to display |
| --len N | all | Number of samples to display |
| --ascii | off | Force terminal ASCII output |
The exponential rise from zero visible at the start of each carrier-on period is antenna ringing — the LC resonance of the 125kHz coil settling after the field is restored. This is why gap timing compensation is necessary in RTF protocols: the tag cannot detect the field until the ringing subsides, which takes roughly 200µs.
Attempts to Manchester-decode the last capture and print the raw bit stream. Binarises the signal at mean ÷ 2, measures run lengths, then reconstructs bits from half-period and full-period transitions.
data manrawdecode
data manrawdecode --clock 64
data manrawdecode --clock 32 --invert
| Flag | Default | Description |
|---|---|---|
| --clock N | 64 | Clock divisor in Tc (e.g. 64 = RF/64 = 512µs/bit) |
| --invert | off | Invert logic levels (high=0, low=1) |
Clock : RF/64 (64 Tc = 512µs/bit)
Threshold: 0x3b Inverted: False
Bits : 55
10000011000011111111000111000001111100111111111
Hex: 4187f8e0f9ff
A full EM410x frame is 64 bits. Capturing 55 bits from a 32ms window is expected — the capture starts at an arbitrary point in the repeating frame. Run lf sniff --timeout 5000 to capture multiple complete frames.
Analyses the capture to identify the clock rate and modulation type, and reports whether RTF gap commands are present in the field. Useful for quickly verifying that a reader-talk-first command is actually being transmitted.
data modulation
Samples : 4000 (32000µs)
Range : 0x00 – 0xff mean: 0x76
Half-period : ~33 samples = 264µs
Full period : ~528µs
Nearest RF : RF/64 (512µs/bit)
Modulation : Manchester (RF/64)
RTF gaps : 1874 samples below 0x3b — gap commands present
The RTF gap line is the key diagnostic for EM4305 debugging. Zero gaps = the field is flat and no command is being sent. A non-zero count confirms gap commands are present in the capture.
# Place tag near antenna, capture 2 seconds
lf sniff --timeout 2000
data modulation # identify clock and encoding
data plot # visual inspection
data manrawdecode # decode bits if Manchester
# Terminal 1 — PM3 passive sniff (independent hardware)
pm3 --> lf sniff
# Terminal 2 — CU runs read command
lf em 4x05 read
# Then on CU — check gap presence in next capture
lf sniff --timeout 500
data modulation
# PM3 simulates EM410x tag continuously
pm3 --> lf em 410x sim --id 0102030405
# CU sniffs and decodes
lf sniff --timeout 2000
data modulation
data plot --start 100 --len 200
data manrawdecode --clock 64
The firmware change adds DATA_CMD_LF_SNIFF = 3020 and the raw_read_to_buffer() function. All data commands are pure Python client-side — no firmware rebuild needed for those.
# Graphical plot (recommended)
sudo pacman -S python-pyqtgraph python-pyqt5
# Or matplotlib fallback
sudo pacman -S python-matplotlib python-pyqt5
lf_reader_generic.h — new header exposing raw_read_to_buffer()
lf_reader_generic.c — circular buffer increased 128→512, 2ms settle delay
data_cmd.h — DATA_CMD_LF_SNIFF = 3020
app_cmd.c — cmd_processor_lf_sniff(), registered with before_reader_run
chameleon_enum.py — LF_SNIFF = 3020
chameleon_cmd.py — lf_sniff(timeout_ms) method
chameleon_cli_unit.py — lf sniff, data hexsamples, data plot, data manrawdecode, data modulation