- pd.py ts_from_samplenum: drop c_uint32 truncation + dead sampleOverflowCount (pcap timestamps wrapped backwards past ~4.5 min at 16 MHz). - pd.py handle_atr: guard None reads at EOF (was TypeError (None & 0x80) on a capture truncated mid-ATR-hunt). - start.bat: add --protocol (+ :protocol=T=0 default) to match start.sh's mid-session decode; correct the 'flag-for-flag' header comment. - start.sh: reject --rst/--vcc assigned to the same pin (was unchecked). - setup.bat: set DEVICE_MSG on blocked/skipped probe so the trailing 'unknown' message is suppressed. - vs_reader.py: RESULT now fails on CONCAT / payload mismatch / suspicious CLA, not just GARBAGE (OK text unchanged; clean traces stay identical).
sigrok_iso7816_stream
Fork of svenso/sigrok_iso7816 with live GSMTAP UDP streaming, making a cheap FX2 logic analyzer (Saleae clone) behave like a SIMtrace2 sniffer:
SIM card ⇄ reader (passive tap on the contacts)
→ FX2 logic analyzer (fx2lafw, 8ch, 24MHz max)
→ sigrok-cli + iso7816 decoder
→ GSMTAP UDP :4729 → simtrace2-pysniff / Wireshark
The tap in practice
The rig tapping a phone SIM session
The rig tapping a card-reader session
Tap wiring on the SIM contacts
The short-cable tap — reduces loop area and rise-time artifacts
Short cable with the breakout gear
Short cable with external pull resistors — 10 kΩ to GND on VCC (pulls low when off), 47 kΩ to VCC on DATA (faster rise to high)
What was added on top of upstream
| Feature | Description |
|---|---|
| Live GSMTAP stream | Every decoded event is sent as a GSMTAP-SIM UDP packet (default on, mimics simtrace2-sniff) |
| Correct sub_types | ATR=0x01 and PPS=0x02 now tagged properly in PCAP output (upstream hardcoded everything to APDU=0x00) |
| ATR/PPS in PCAP | ATR and combined PPS request+response are emitted as PCAP packets |
| RST tracking (optional) | rst_detect=true: emits reset assert/deassert events (sub_type 0x10) |
| VCC tracking (optional) | vcc_detect=true: emits power on/off events (sub_type 0x11) |
| Direct PCAP file | pcap_file=<path> writes PCAP without shell redirection |
| Start-bit deglitch | 2-sample deglitch in the start-bit detector eliminates sub-ETU LOW glitches from FX2LP pull-up artifacts, preventing byte misalignment on fast phone traces |
Decoder options
| Option | Default | Description |
|---|---|---|
clock_option |
native |
native, detect, sample_as_clock (see upstream FAQ) |
protocol |
auto |
auto, T=0, T=1 |
gsmtap_enable |
true |
Live GSMTAP UDP streaming |
gsmtap_host |
127.0.0.1 |
GSMTAP destination host |
gsmtap_port |
4729 |
GSMTAP destination port |
pcap_file |
(empty) | Write PCAP to this file in addition to binary output |
rst_detect |
false |
Track RST pin events (requires the rst channel to be assigned) |
vcc_detect |
false |
Track VCC pin events (requires the vcc channel to be assigned) |
Channels: clk, data (required); rst, vcc (optional — assign only when
the corresponding wire is physically connected).
GSMTAP events
GSMTAP header: version 2, type SIM (0x04), UDP port 4729.
| sub_type | Name | Payload |
|---|---|---|
| 0x00 | APDU | raw T=0/T=1 packet bytes |
| 0x01 | ATR | raw ATR bytes |
| 0x02 | PPS | PPS request + response bytes concatenated (deviation, see below) |
| 0x10 | RST event (custom) | [direction, level, 0x00] |
| 0x11 | VCC event (custom) | [direction, level, 0x00] |
Event payload byte semantics:
- RST:
direction1 = reset asserted (high→low), 0 = deasserted (low→high);level= line level after the transition. - VCC:
direction1 = power applied (low→high), 0 = removed (high→low);level= line level after the transition.
0x00/0x01 match libosmocore's gsmtap.h: standard values and payloads.
0x02 reuses the spec's GSMTAP_SIM_PPS_REQ value but carries the request
and response bytes concatenated in one packet; the spec splits PPS into
PPS_REQ (0x02) / PPS_RSP (0x03), so Wireshark dissects these are
"PPS request" and PPS_RSP (0x03) is never sent. 0x10/0x11 are custom
extensions not defined in libosmocore's gsmtap.h.
Header extensions: flags such as GSMTAP_FLAG_BAD_FCS are written into
the header res byte, which official gsmtap.h marks reserved (RFU) —
flag usage there follows the simtrace2-sniff convention and is not part
of the libosmocore spec.
Usage
Quick start after cloning (see Installation below):
./setup.sh # check sigrok-cli, register decoder symlink
./start.sh # capture + GSMTAP stream to 127.0.0.1:4729
start.sh defaults: pins D5=CLK, D0=I/O, D4=RST, D2=VCC, 16 MHz samplerate.
Override via args:
./start.sh --clk=D1 --data=D6 # different wiring
./start.sh --no-rst --no-vcc # CLK+I/O only
./start.sh --host=10.0.28.7 # stream to a remote analyser
./start.sh --pcap=session.pcap # also record decoded events to PCAP
./start.sh --debug # full decoder + sigrok output (all
# annotation rows + libsigrok -l 4);
# by default only decoded APDUs and
# decoder warnings/errors are shown
Detecting the wiring (tools/detect_pins.py)
Not sure which FX2 channel carries which SIM signal? The probe loops short captures until the wiring map is complete:
python3 tools/detect_pins.py # live probe (default): trigger a
# SIM reset when prompted
python3 tools/detect_pins.py capture.sr # or analyze an existing capture
Each segment fingerprints every channel (toggle rate, idle level, median
inter-transition gap, first-rise ordering across VCC -> RST -> ATR).
Ambiguous static lines are reported as such instead of guessed; the run
ends with the detected mapping and a ready-to-paste start.sh command
line -- partial mappings still print usable flags for the known channels.
Requires numpy.
GND is not detectable. It carries no signal to fingerprint, so the tool cannot find or verify it. Plug it first and check it manually (continuity meter or known-good contact) -- a missing or loose ground masquerades as every other fault at once: floating lines, phantom start bits, all-zero frames.
Robustness against noisy signals
Live captures can glitch (a phantom start bit during power-up used to shift the whole ATR parse off-by-one). The decoder now:
- validates the ATR's TS byte (
0x3B/0x3Fper TS 102 221 §5.2.2) and resyncs on the next real falling edge if it is bogus; - never lets a decode exception kill the capture — errors are logged, annotated, and the state machine resyncs (end-of-stream still exits cleanly);
- bounds T=1 block-chain follow-up so garbage cannot loop forever.
After an accepted PPS the decoder follows the speed switch: in
native CLK mode clock_skip becomes F/D CLK cycles per bit (e.g.
512/16 = 32), matching both sides' new UART rate. Live capture
confirmed that keeping 372 after PPS aliases all subsequent traffic.
A warm reset (the reader re-asserts RST every ~1.6 s) makes the card
answer with a fresh ATR at the default 372 cycles/bit. The decoder
re-arms the ATR hunt on RST deassert: state back to FIND START,
native skip reset to 372, hunt count cleared — so the re-ATR parses as
an ATR instead of being mis-read as TPDU traffic at the PPS-negotiated
rate. A 1.5-ETU start-bit guard additionally rejects the phantom
0x00 an idle glitch can insert before that ATR. Requires
rst_detect=true (the rst channel assigned); without it a mid-session
reset is still mis-read.
Troubleshooting live capture
start.sh runs a single --continuous capture until you press Ctrl+C
(rc=130). stdin is detached from sigrok-cli, so pressing keys/Enter in
the terminal does not stop it (sigrok's "press any key to stop
acquisition" prompt is disabled) — that keypress was previously
mis-read as a "premature abort", but there was never a device fault.
Most of the early "noise" seen in captures (parity errors, mis-framed
garbage, a bouncing VCC line) was a badly-decoded state machine, not the
hardware: re-seating the tap did nothing, while the decoder rewrite fixed
it completely (warm-reset re-arm, 1.5-ETU start-bit guard, 10 ms VCC
debounce, T=0 exchange reassembly). A clean capture now decodes to zero
garbage — see the vs_reader.py validation below.
If a capture does end on its own (rc=0), the device genuinely stopped
streaming; the FX2's USB bandwidth ceiling at sustained high sample rates
is the usual suspect.
./start.sh --pcap=s.pcap --log=s.log # also record decoded events, log rc=
./start.sh ... --samplerate=24M # more timing headroom if needed
./start.sh ... --klog # also record kernel journal
./start.sh ... --debug # full output: all annotation rows
# + libsigrok internals at -l 4
# (default: APDUs + warnings only)
start.sh captures a continuous stream at the configured rate (default 16
MHz) until Ctrl+C (rc=130). A healthy capture never ends by itself; if the
FX2 drops the USB transfer mid-session (a flaky device, rc=0 "device
stopped streaming"), pass --loop to auto-restart — it re-launches the
capture on every rc=0 until you press Ctrl+C or hit --max-restarts=N
(default 20; 0 = unlimited), with a --restart-delay=S gap (default 2 s).
--loopauto-restarts the capture when it ends on its own (rc=0).--max-restarts=Ncaps restarts (default 20;0= unlimited).--restart-delay=Sseconds to wait between restarts (default 2).--logcaptures therc=verdict line for the session end.- The decoder logs
SIGNAL INTEGRITY WARNINGwhen >= 8 of the last 64 frames fail parity. - Desynced TPDUs (mis-framed exchanges) are kept but flagged with
GSMTAP_FLAG_BAD_FCSin the GSMTAP headerresbyte — visible in Wireshark as a BAD-FCS warning. - A T=0 header with a structurally invalid INS (ISO 7816-4 §12.1.1:
0x00,0x6x/0x9x, or odd LSB) is almost always a one-byte shift from a stray leading byte (e.g. a leftover SW2 read after a PACKETEND). Instead of silently dropping the whole 5-byte window (which let the next byte become a bogus CLA and swallow the remaining dialog into one CONCAT packet), the decoder drops only the leading byte and re-frames the rest, recovering the real command. Every consumed-but-unemitted byte is explicitly logged (discarded byte 0x…: reason); idle 0x00/0xFF discards are rate-limited, non-idle discards always produce a decoder annotation.
Performance: in native CLK mode the decoder calibrates samples per CLK
edge once from the ATR's first frame, then advances bit timing with
single sample-skip waits instead of one wait per CLK edge. The PPS speed
switch is followed via clock_skip = F/D; there is no post-PPS or
parity-triggered recalibration.
Mode comparison on FX2 hardware (A/B result)
nativedecoded real traffic (ATR, PPS switch, 100+ valid APDUs in a 12 s window); the one persistent artifact was the VCC sense line "bouncing" — the ~1 kHz ringing the 10 ms debounce now suppresses.detectnever produced a usable session on this rig: it missed the PPS speed switch entirely (no PPS handling fired), aliasing all post-PPS traffic; a second attempt caught a dead I/O line.
native stays the default. Use --clock=detect only for offline
experiments without PPS. A run full of all-zero frames means the tap
lost contact with a line.
Parity is even per TS 102 221 ("an even number of bits set to '1'
including the parity bit"); frames failing the check are flagged as
CHKSUM ERROR but still decoded.
Equivalent raw sigrok-cli commands:
# Minimal: CLK+I/O only, live stream to localhost:4729
sigrok-cli -d fx2lafw --config samplerate=12M --continuous \
-P iso7816:clk=D0:data=D1:clock_option=detect -A iso7816
# Full wiring: CLK + I/O + RST + VCC with line event tracking
sigrok-cli -d fx2lafw --config samplerate=12M --continuous \
-P iso7816:clk=D5:data=D0:rst=D4:vcc=D2:clock_option=native:rst_detect=true:vcc_detect=true \
-A iso7816
# Stream to a remote analyser / disable streaming / write PCAP file
... -P iso7816:...:gsmtap_host=10.0.28.7 ...
... -P iso7816:...:gsmtap_enable=false ...
... -P iso7816:...:pcap_file=trace.pcap ...
# Classic upstream mode: binary PCAP to stdout
sigrok-cli -i capture.sr -P iso7816:clk=2:data=0 -B iso7816=pcap > out.pcap
Pin mapping
The FX2 clone channels are assigned via -P iso7816:clk=N:data=N:rst=N:vcc=N
(no re-plugging needed when wires are mixed up). Default tap wiring used by
start.sh:
| FX2 channel | SIM contact | Signal |
|---|---|---|
| D0 | C7 | I/O |
| D2 | C1 | VCC (digital level only!) |
| D4 | C2 | RST |
| D5 | C3 | CLK |
| GND | C5 | GND |
FX2 input threshold is TTL-ish (~1.4V): 1.8V-class cards may read unreliably; 3V/5V classes are fine.
Samplerate and 8-bit mode (important): the FX2's nominal 24 MHz is only
available when sampling in 8-bit mode, i.e. when only channels D0–D7 are
enabled. Enabling the 9th channel (A0/analog on CWAV AX clones) or any
analog input forces 16-bit collection, which libsigrok caps at 12 MHz.
start.sh/start.bat therefore pass -C D0,D1,D2,D3,D4,D5,D6,D7
explicitly and default to 16 MHz (comfortably under the 24 MHz USB
bandwidth ceiling; ~3.3 samples per 4.8 MHz clock cycle). setup.sh/
setup.bat probe and report your hardware's actual maximum under the
same restriction.
Installation
Linux
git clone git@github.com:anttro/sigrok_iso7816_stream.git
cd sigrok_iso7816_stream
./setup.sh
setup.sh verifies sigrok-cli and the FX2 firmware package
(sigrok-firmware-fx2lafw) are installed, symlinks the repo into
~/.local/share/libsigrokdecode/decoders/iso7816 (user-local decoders shadow
system-wide ones), and probes the connected device for its maximum supported
samplerate.
Windows
Prerequisites:
- Python 3.8+ from python.org ("Add to PATH" during install), then:
pip install numpy - sigrok-cli Windows installer (0.7.2 x86_64) from
sigrok.org/wiki/Downloads.
If it fails to start with error
0xc0150002, install the Visual C++ 2010 Redistributable (msvcr100.dllis required by the bundled Python runtime). - fx2lafw firmware ships bundled with the installer — no separate download.
Driver setup (FX2 dongles need WinUSB, the vendor driver won't work):
- Run Zadig, which ships inside the sigrok-cli installation directory (also reachable from the Start menu). Select the FX2 device in its dropdown and install the WinUSB driver.
- After firmware upload the device re-enumerates with a different VID/PID — if it still isn't found, run Zadig a second time for the re-enumerated entry.
- Windows binds drivers per USB port when no serial number is present: moving the dongle to another port may require repeating Zadig.
Then:
git clone git@github.com:anttro/sigrok_iso7816_stream.git
cd sigrok_iso7816_stream
setup.bat
setup.bat verifies sigrok-cli and bundled firmware, registers this repo's
decoder via a junction into %LOCALAPPDATA%\libsigrokdecode\decoders\iso7816
(confirmed by sigrok-cli -L listing iso7816), checks Python/numpy for
tools\detect_pins.py, and probes the device's accepted samplerates.
Capture works the same as on Linux:
tools\detect_pins.py rem find the wiring (loops segments until done)
start.bat rem capture + GSMTAP stream
Note: batch files need CRLF line endings; the repo enforces this via
.gitattributes, so a fresh clone on Windows is already correct.
RST/VCC detection notes and limitations
- Events are detected asynchronously before the first ATR (power-on / reset release are captured once the level is stable) and at packet boundaries between T=0/T=1 packets afterwards.
- Edges occurring entirely within a packet transfer are observed as a net level change at the next boundary; short pulses inside a packet can be missed.
- VCC is digital-only: the decoder sees powered/not-powered, never the voltage class (1.8/3/5V).
- RST and VCC use level-stability hysteresis at 10 ms (samplerate-scaled): a new level is reported only once it has been continuously stable for ≥ 10 ms — either because it persists that long, or because it ends after being stable that long. This makes power-up and power-down symmetric: a VCC sense line that rings on the rising edge (noisy/floating tap) no longer loses its "VCC on" event — it fires once the line settles HIGH, instead of being eaten by edge debounce. RST uses the same scheme, so a brief RST glitch no longer triggers a phantom warm-reset re-arm.
- VCC events are generally unreliable — prefer not to track VCC. A floating
or unconnected VCC sense pin reads HIGH at all times (logic-high with no load),
so the decoder sees "powered" constantly and emits spurious power events on
every bounce. Unless your tap derives VCC from a clean, loaded source, it is
better to leave the
vccchannel unassigned (do not pass--vcc) and keepvcc_detect=false; RST tracking is usually enough to see resets. Only enable VCC tracking on a tap with a solid VCC sense (and a solid GND — see wiring notes). - Unassigned optional channels are ignored even if
rst_detect/vcc_detectis enabled (guarded viahas_channel()).
Testing
Verified against the upstream example captures:
# ATR+PPS+54 APDUs from the test_8 reader capture (~19s decode)
# GSMTAP stream verified with tshark (ATR/APDU dissected natively),
# custom 0x10/0x11 events verified with a UDP listener, and end-to-end
# into simtrace2-pysniff server (POST /api/capture/start → decode → stop).
A synthetic VCD generator covering RST/VCC edge cases lives in the project
notes (projects/SIGROK_ISO7816.md in the parent workspace).
Validation against the reader SDK (tools/vs_reader.py)
On a controlled rig (PCSC reader driven by software) the reference APDU log from the reader SDK can be diffed against the decoded PCAP:
sigrok-cli -i <trace>.sr \
-P iso7816:clk=D5:data=D0:rst=D4:vcc=D2:clock_option=native:rst_detect=true:vcc_detect=true:gsmtap_enable=false:pcap_file=out.pcap \
-A iso7816
python3 tools/vs_reader.py <reader-log>.log out.pcap
vs_reader.py aligns each reader-log exchange to the capture APDUs
(byte-exact LCS match) and classifies the leftovers as proactive/CAT
(unseen by the reader SDK), pre-capture/dropped, or garbage.
The mid-session capture whose warm resets previously produced 4 garbage
events — with the re-arm + start-bit guard it decodes to 0 garbage
(RESULT: OK - capture fully explained).
test_7 (4 ATR resets fragmenting the session) now captures all 37/37 reader exchanges (up from 10/37 before the deglitch fix). Samsung phone traces with 16 CLK/ETU (F=512/D=32) now de-concatenate to 924 individual T=0 exchanges (previously merged into 52 oversized APDUs).
Replayable trace fixtures (kept locally, outside the repo) cover
controlled-rig reader sessions (with their reader-SDK ground-truth logs),
phone captures with gated CLK, and cold-boot captures that start
before power-up (VCC/RST low at t=0). The four Samsung fixtures are
cold boots taken with a new short cable (30 mm wires, external pull
resistors GND→VCC and VCC→DATA), named channels DATA/VCC/RST/CLK on bits
0/2/4/5; the sim2 variants are cropped to the power-on window.
Cold-boot captures (capture starts before power-up)
When the capture starts pre-power-up, the decoder must arm the ATR
hunt because RST is still low at sample 0: it primes the initial
VCC/RST levels on the first step (instead of defaulting them to deasserted),
and on an RST-low-at-start it enters FIND START with a cleared ETU, so the
pre-ATR power-up DATA transitions cannot lock a bogus bit rate. It then
decodes the full ATR (3b9f96801fc78031e073fe2117574a3382033339001e) with
0 garbage:
sigrok-cli -i <coldboot>.sr \
-P iso7816:clk=CLK:data=DATA:rst=RST:vcc=VCC:clock_option=native:protocol=T=0:rst_detect=true:vcc_detect=true:gsmtap_enable=false:pcap_file=out.pcap \
-A iso7816
An RST deassert that lands while an ATR parse is already in progress no longer
re-arms the hunt mid-read — it logs RST deasserted (hunt/ATR already in progress) and leaves the in-flight ATR alone (test_8/test_7 CHKSUM 18→0).
Capture scenarios: reader (constant CLK) and phone (Clock Stop Mode)
The decoder handles the two physically different SIM sniffing setups:
-
Reader / constant-CLK capture — the reader drives a steady CLK for the whole session. In mid-session mode the capture decodes to 0 garbage (
RESULT: OK) against the reader-SDK ground truth (tools/vs_reader.py <reader-log>.log out.pcap). Warm-reset reader captures use the synthetic-T=0-ATR fallback. -
Phone / stopped-CLK capture — the phone gates or stops CLK between transfers (Clock Stop Mode), so the ETU is not the 372-clock default and cannot be inferred from CLK. Run with
protocol=T=0; the decoder recovers the ETU from the DATA line via edge counting, locks it, and decodes with the drift-immune edge-list bit reader. T=0 case 3/4 commands (e.g.80 12/80 14STK envelope) are consolidated into a single command→response APDU. A 2-sample start-bit deglitch eliminates sub-ETU LOW glitches caused by FX2LP internal pull-up artifacts, preventing byte misalignment on fast phone traces.
Proven data rates and ETUs
All traces decoded at 16 MHz sample rate, 0 garbage, RESULT: OK. Since v1.5.0 every example trace also reports 0 CONCAT (the invalid-INS re-frame eliminated the last multi-exchange swallow family on 4gmodem/A55_sim2/S21p_sim2).
| Trace | CLK mode | ETU (samples) | CLK/ETU | Convention | Notes |
|---|---|---|---|---|---|
| test_8 | constant | ~43 | 372 (standard) | Direct | PC/SC reader, ATR+PPS+APDUs |
| test_7 | constant | ~43 | 372 (standard) | Direct | PC/SC reader, warm resets |
| xiaomi | gated/stopped | ~53 | recovered from DATA | Direct | Phone, mid-session |
| samsung | gated/stopped | ~72 | 16 (F=512/D=32) | Direct | Phone, non-standard F/D, STK menu |
The decoder auto-detects the ETU from the trace and selects the appropriate
bit reader (CLK-sync for constant CLK, edge-list for gated/stopped CLK).
No manual configuration is needed beyond clock_option=native and
protocol=T=0 for phone captures.
Recommended invocations:
# Reader capture (controlled rig, CLK always running)
sigrok-cli -i <trace>.sr \
-P iso7816:clk=CLK:data=DATA:rst=RST:vcc=VCC:clock_option=native:rst_detect=true:vcc_detect=true:gsmtap_enable=false:pcap_file=out.pcap \
-A iso7816
python3 tools/vs_reader.py <reader-log>.log out.pcap
# Phone capture (mid-session, gated/stopped CLK)
sigrok-cli -i <phone>.sr \
-P iso7816:clk=CLK:data=DATA:rst=RST:clock_option=native:rst_detect=true:vcc_detect=false:protocol=T=0:gsmtap_enable=false:pcap_file=out.pcap \
-A iso7816
License
GPL-2.0+, same as upstream.


