Python CLI/ Firmware/ LF 125kHz// March 2026
Tool Documentation

lf sniff & data commands
for ChameleonUltra

Raw LF field capture, graphical waveform plotting, Manchester decode, and modulation detection —
a PM3-style analysis toolkit built into the ChameleonUltra Python CLI.

Firmware Python CLI LF 125kHz New Commands
Author
Niel Nielsen
Platform
ChameleonUltra
Sample rate
125kHz / 8µs
Firmware change
DATA_CMD 3020
Contents
  1. 01Why lf sniff
  2. 02lf sniff — capture
  3. 03data hexsamples — hex dump
  4. 04data plot — graphical waveform
  5. 05data manrawdecode — Manchester decode
  6. 06data modulation — signal identification
  7. 07Typical workflows
  8. 08Installation

01Why lf sniff

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.

How it works

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.

02lf sniff — capture

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.

CLI
lf sniff
lf sniff --timeout 500
lf sniff --timeout 2000 --hex
lf sniff --timeout 2000 --out capture.bin
FlagDefaultDescription
--timeout MS2000Capture duration in milliseconds (max 10000)
--hexoffPrint hex dump with level indicators immediately after capture
--out FILEnoneSave raw bytes to binary file for offline analysis

After capture, the summary line tells you immediately whether the signal looks useful:

Output
Captured : 4000 bytes (32.0ms)
Range    : 0x000xff  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.

03data hexsamples — hex dump

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.

CLI
data hexsamples
data hexsamples -n 512
Output
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:

_0x00–0x0f — gap / field off
.0x10–0x3f — ringing / decay
-0x40–0x7f — low
+0x80–0x9f — mid
o0xa0–0xbf — steady carrier
O0xc0–0xdf — high
#0xe0–0xff — clipped / max

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.

04data plot — graphical waveform

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.

CLI
data plot
data plot --start 100 --len 200
data plot --ascii
FlagDefaultDescription
--start N0First sample to display
--len NallNumber of samples to display
--asciioffForce terminal ASCII output

Plot colour reference

LF field — raw ADC value (0–255)
Mean — average ADC level of the capture
Gap threshold — mean ÷ 2. Values below this are real field drops
Gap region — samples below gap threshold, shaded red

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.

05data manrawdecode — Manchester decode

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.

CLI
data manrawdecode
data manrawdecode --clock 64
data manrawdecode --clock 32 --invert
FlagDefaultDescription
--clock N64Clock divisor in Tc (e.g. 64 = RF/64 = 512µs/bit)
--invertoffInvert logic levels (high=0, low=1)
Example output — PM3 EM410x sim at RF/64
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.

06data modulation — signal identification

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.

CLI
data modulation
Example output — EM410x at RF/64
Samples  : 4000  (32000µs)
Range    : 0x000xff  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.

07Typical workflows

Verify an unknown LF signal

CLI
# 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

Validate RTF gap commands (e.g. EM4305 read)

CLI
# 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

Test with PM3 simulation

CLI
# 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

08Installation

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.

Arch Linux
# Graphical plot (recommended)
sudo pacman -S python-pyqtgraph python-pyqt5

# Or matplotlib fallback
sudo pacman -S python-matplotlib python-pyqt5
Firmware files changed

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.hDATA_CMD_LF_SNIFF = 3020
app_cmd.ccmd_processor_lf_sniff(), registered with before_reader_run

Python files changed

chameleon_enum.pyLF_SNIFF = 3020
chameleon_cmd.pylf_sniff(timeout_ms) method
chameleon_cli_unit.pylf sniff, data hexsamples, data plot, data manrawdecode, data modulation

← Back to Blog