catarrh e1903649a7 Hygiene: drop dead code, re-raise SystemError, resource+doc cleanup, add LICENSE
- pd.py: remove _lock_etu_from_edges (long-disabled) and the no-op
  while-True FIND START wrapper.
- pd.py: re-raise SystemError (libsigrokdecode's clean EOF signal) in the
  six 'except Exception: pass' guards so put()/wait() at end-of-capture
  exits cleanly instead of being swallowed.
- pd.py: fix _measure_clock_period docstring (averages, not robust-min);
  drop stale 'line 1633' comment; document the synthetic pcap UDP header
  (zero IP checksum / fixed loopback+ports are intentional).
- signal_diagnostic.py: add --max-samples cap + document whole-capture
  RAM cost; fix stale '--rst/--vcc=ignore' guidance (-> --no-rst/--no-vcc).
- tests: make test_valid_cla_interindustry_classes use subTest (was a
  no-op tuple expression).
- start.sh/start.bat: note the -A multi-spec form is sigrok-cli 0.7.x.
- Remove stale tests/baseline_new.txt (v1.1.3-era, ATR-included mode).
- Add top-level LICENSE (GPL-2.0-or-later).
2026-09-11 01:33:12 +03:00

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

Components Components used in the build

In action with phone The rig tapping a phone SIM session

In action The rig tapping a card-reader session

SIM adapter The SIM-tap adapter

SIM adapter pinout Tap wiring on the SIM contacts

Short cable The short-cable tap — reduces loop area and rise-time artifacts

Short cable wiring Short-cable wiring detail

Short cable gear Short cable with the breakout gear

Short cable pull resistors 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: direction 1 = reset asserted (high→low), 0 = deasserted (low→high); level = line level after the transition.
  • VCC: direction 1 = 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/0x3F per 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).

  • --loop auto-restarts the capture when it ends on its own (rc=0).
  • --max-restarts=N caps restarts (default 20; 0 = unlimited).
  • --restart-delay=S seconds to wait between restarts (default 2).
  • --log captures the rc= verdict line for the session end.
  • The decoder logs SIGNAL INTEGRITY WARNING when >= 8 of the last 64 frames fail parity.
  • Desynced TPDUs (mis-framed exchanges) are kept but flagged with GSMTAP_FLAG_BAD_FCS in the GSMTAP header res byte — 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)

  • native decoded 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.
  • detect never 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:

  1. Python 3.8+ from python.org ("Add to PATH" during install), then: pip install numpy
  2. 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.dll is required by the bundled Python runtime).
  3. 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 vcc channel unassigned (do not pass --vcc) and keep vcc_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_detect is enabled (guarded via has_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 14 STK 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.

S
Description
FX2/SIM ISO 7816 sniffer decoder (GitHub mirror)
Readme
3.1 MiB
0 Stars 1 Watchers 0 Forks
Languages
Python 83.8%
Shell 11.8%
Batchfile 4.4%