Replace the "behaves like a SIMtrace2 sniffer" framing with the value
proposition: the simplest and cheapest way to trace ISO 7816 traffic with
an FX2 logic analyzer (Saleae clone). Mirrored in both languages.
The project news site moved off the shared news domain:
- project news: https://atroshin.ru -> https://sim-tools.ru
The old address redirects, so no link breaks; the READMEs now name the
canonical destination (the Related tools paragraph included).
The right-hand node is not necessarily a card reader: the tap works with
any device, and phone captures are the main use case. Rename it to
"Device" / "Устройство" in both diagrams and in their alt text and
descriptions.
Replace the ASCII signal chain in both READMEs with a rendered diagram
(resources/diagram_en.svg / resources/diagram_ru.svg): SIM card <-> reader
with the passive tap on the contacts, the FX2 logic analyzer, sigrok-cli
with the iso7816 decoder, GSMTAP UDP :4729, and the SIMtrace Analyser /
Wireshark consumers.
The SVGs are hand-authored, self-contained (no scripts, no external
references), carry <title>/<desc>, and each README embeds its own language
variant with the chain repeated in the alt text.
Release packaging: both diagrams are added to the archive allowlist. The
shared-image equality check ignores them (the READMEs embed different
files by design), while every other resources/ photo must still match
across the two READMEs. AGENTS.md documents the exception.
- Give the H1 a one-line description ("a passive ISO 7816 sniffer" /
"пассивный ISO 7816-сниффер").
- Replace the Russian file's top "English version" note with a symmetric
language line under the title in both files (English | Русский /
English | Русский).
- Add a "Related tools" / «Связанные инструменты» section before the
license: SIMple, SIMtrace Analyser and this project, with a note that
each is installed independently and links to the shared website and
Telegram channel.
Docs only; no decoder change.
- Move installation/startup to the top (after a short intro): download
(Linux tar.gz / Windows zip) and source checkout, each ending with the
setup/start commands.
- Usage (start options, wiring, pin mapping, clock modes, troubleshooting,
raw sigrok-cli) follows the install section instead of preceding it.
- Remove "Robustness against noisy signals" (decoder internals, not
end-user information).
- Remove historical explanations (early-version failures, "used to",
before/after fix counts) from troubleshooting, clock modes, capture
scenarios, cold-boot, validation and data-rate notes.
- Rewrite live-capture troubleshooting: early stopping (especially on
Windows) is commonly the wrong USB driver - the Saleae Logic driver
works only with Saleae Logic software, sigrok needs WinUSB (Zadig) -
with links to sigrok's Windows/FX2 documentation; keep the current
device-not-found, USB-bandwidth and auto-restart guidance.
- Move the tap photos below Usage; features/reference/development sections
follow. Russian README mirrors the same structure and edits.
Docs only; no decoder or release change.
- tools/build_release.sh: builds the Linux tar.gz and Windows zip from a
git ref (default HEAD) using an explicit `git archive` pathspec
allowlist: decoder (__init__.py, pd.py, gsmtap_stream.py), setup/start
scripts, tools/vs_reader.py, READMEs, LICENSE and the README-referenced
resources/ images. v* tags must match pd.py VERSION. Both archives
are listed and extracted and checked for VERSION, py_compile, bash -n,
.bat CRLF / .sh LF and tar exec bits; SHA256SUMS uses sha256sum or
shasum. Builds into a staging dir and only then replaces dist/.
- .github/workflows/release.yml: on a v* tag push run the unit tests,
verify tag == pd.py VERSION, build, then upload into an existing
release or create a draft and publish it; release-<tag> concurrency.
- README/RUS: Installation documents both ways (release download
recommended; source checkout for development), SHA256SUMS verification,
and the Windows prerequisites verified against sigrok.org (0.7.2
release installer as the tested version, nightly caveat, VC++ 2010
redist, bundled fx2lafw firmware and Zadig, per-port driver binding).
- start.bat: accept --opt=value alongside the original --opt value form
(the usage text advertised the = form while the parser required a
space); usage corrected and notes the core subset.
- .gitignore: dist/.
- AGENTS: release packaging, build verification and release process
(commit docs/workflow first, then tag and push, auto-generated notes).
No decoder behavior change; VERSION stays 1.9.7.
The audit's P2 items:
- ODD_INS: add 0x73 MANAGE SECURE CHANNEL and 0x75 TRANSACT DATA
(TS 102 221 Table 10.5); the tools/vs_reader.py mirror and the pinned
test are updated.
- 9E/9F are SW1: ISO 7816-3 10.3.3 Table 11 makes every 9X value SW1.
Remove the fabricated "extended-length procedure byte" that consumed
SW2 as a length and lost the status word.
- T=1: EOF-guard every block read, bound the M-bit chain with
T1_CHAIN_MAX, and drop the caller's duplicate chain loop -- it read the
next block after every I-block (usually the peer's R-block) and merged
the following I-block's INF into the packet. R/S blocks emit nothing;
the (possibly chained) I-block INF is the APDU payload.
- Parity: a packet containing a parity-error byte is emitted with
GSMTAP_FLAG_BAD_FCS (the CHKSUM annotation was invisible downstream),
with a log note when the exchange is otherwise clean.
- EOF guards: handle_atr (TA/TB/TC/TD, historical, TCK), handle_pps
(request/response fields incl. PPS1-3) and the T=1 block parser return
cleanly on a truncated capture instead of raising on None.
- Dead code: _edge_read was initialized True and never cleared, so the
ATR-based T=0 procedure loop (58 lines) and the 1.5-ETU guard branch
were unreachable. Remove the flag, both dead paths and the branch;
dedent the live loop (behavior unchanged).
- Samplerate scaling: _samples_for_us() replaces the hard-coded 16 MHz
thresholds -- start-bit deglitch (0.125 us), min_high floor (0.25 us)
and the ETU glitch floor (0.75 us). At 16 MHz the values are
unchanged.
Validation: 130/130 unit tests (13 new: 9E/9F SW1, parity BAD_FCS,
T=1 I/R/S blocks + M chain + truncations + one-block-per-step, PPS
truncations, truncated ATR TA, samplerate/ETU-floor vectors); the
10-trace suite is unchanged from v1.9.6 (identical APDU/CHKSUM/CONCAT/
BAD_FCS counts, RESULT: OK); compare_baseline.sh reports IDENTICAL.
VERSION -> 1.9.7.
The audit's P1 robustness findings:
- _etu_candidate() validates a measured burst: requires >=2 usable
spacings, rejects a guard-dominated burst (all-zero data: 10-ETU LOW
runs + a 2-5 ETU HIGH guard) and a lone 10-ETU spacing. A failed
measurement leaves bit_samples unset instead of committing the
clock_skip*3 fallback.
- _confirm_etu() no longer counts a dropped frame (empty bits) as
parity-OK, and a rejected measurement clears bit_samples,
_etu_measured and _clock_skip_confident (was: kept sampling at the
rejected rate).
- Hunt ETU recovery keys off _etu_measured: handle_atr pre-seeds
bit_samples, so the old "bit_samples is None" condition never fired.
- resync_idle() waits on the DATA idle level (plus RST/VCC edges,
bounded) instead of spinning on wait({'skip': 0}) at one sample.
- ATR acceptance: a DATA-state 0x3B/0x3F is a candidate only before the
first APDU (_apdu_seen); a parsed ATR is ignored on any parity error
or when it is a bare TS+T0. A data run misread at a frame boundary
could fabricate an ATR and force clock_skip=372 for the capture.
- Inverse convention per ISO 7816-3 7.1/8.1: idle HIGH and a falling
start bit in both conventions, msb-first inverse data, raw moments
5-7 TS-pattern detection (a real inverse TS reads as 0x03 through
the direct reader), and a TS parity exemption (the inverse pattern
is odd over moments 2-10). Spec-verified; no inverse capture in the
corpus.
Validation: 117/117 unit tests (18 new); the 10-trace suite is
unchanged from v1.9.5 (APDU/CHKSUM/CONCAT/BAD_FCS identical,
RESULT: OK everywhere); compare_baseline.sh reports IDENTICAL.
VERSION -> 1.9.6.
- Decode the first post-header byte as SW1 before the ACK/data decision:
a case-2 rejection (6A82/6CXX) carries no ACK and used to be read as
command data, silently consuming the exchange and the next one.
- Short responses (Lr < Le): after the P3-byte read, split the buffer at
the first plausible SW1 followed by a plausible CLA, replay the tail and
flag BAD_FCS; previously the SW scan discarded the real SW, merged the
following exchanges and lost the exchange at EOF.
- P3=0 is Le=256 for an outgoing command: keep the response data (read
until SW) instead of discarding it, including data bytes equal to INS.
- ~INS ACK: one data byte per ACK via the new helpers; case-4 inline data
is appended (permissive; ISO 7816-3 §12.2 maps case 4 to GET RESPONSE).
- tests: four TestDecodeStep vectors (immediate SW, short read + split +
replay, Le=256 data incl. an INS-valued byte); 99/99.
- AGENTS v1.9.5 + T=0 section, README/README_RUS bullets; baseline
regenerated (xiaomi_mi_a1_coldboot 473 -> 475 APDUs, all else identical);
compare_baseline: IDENTICAL.
- pd.py/AGENTS: the rejected '8X' range now reads 0x81-0x83/0x85-0x8F
(0x84 stays accepted); the 0x40-0x4F vs 0x50-0x5F citations are split
(chaining is ISO-only, TS 102 221 keeps b5=0); the bit-dense CLA list
gains 0xE7 (exact set verified).
- README_RUS.md: the invalid-INS note mirrored (the removed odd-LSB rule,
citation corrected to 5.1.2).
- tests: test_vs_reader_mirror (pd.py <-> tools/vs_reader.py drift guard),
test_odd_ins_set_pinned, the 0xE7 noise vector, and the 'EX' vector now
uses STATUS (e2 f2) as TS 102 221 Table 10.5 defines.
- tests/baseline.txt regenerated (test listing only); compare_baseline:
IDENTICAL.
v1.9.0 -- CLK frequency in RST events:
- RST events (sub_type 0x10) append the measured CLK frequency when known:
[direction, level, flags, clk_hz_be32], flags bit 0 = rate present; the
3-byte form is kept when the rate is not yet measured. Value is
samplerate / _samples_per_clock from the existing 16-edge measurement (PPS
changes F/D, not the CLK rate). VCC events unchanged. Backward
compatible (old consumers read the first two bytes only); consumers can
compute data_rate = clk_hz x D / F from the ATR/PPS Fi/Di.
- Unit tests for the payload codec; README/README_RUS/AGENTS updated.
v1.9.1 -- T=0 NULL is not an SW1:
- The 'scan for SW1' loops accepted any 0x6X byte, including the NULL
procedure byte 0x60 (card busy), so 'NULL + real SW1' was mis-framed as
the status word: the phone traces' SELECT MF came out as ...3f00 6061 —
real SW1 0x61 eaten as SW2, real SW2 0x29 left on the wire and discarded
as a suspicious CLA (desync cascade).
- plausible_sw() now accepts 0x61-0x6F and 0x90-0x9F only, per
ISO 7816-3:2006 10.3.3 Table 11 ('6X' or '9X', EXCEPT '60'; ISO 7816-4
enforces '60' as an invalid SW1) and UICC_SPECS.md 1.8. All seven
SW1-detection sites use plausible_sw(); NULLs met while scanning are
logged as rate-limited wait extensions (_note_pre_sw) instead of
suspicious bytes.
- tools/vs_reader.py::_plausible_sw() mirrors the fix; regression vector
for the 00a40004023f006061 artifact.
Validation: 74/74 unit tests; baseline regenerated and IDENTICAL (the SW
fix is count-neutral -- only the SW bytes change); all 9 example pcaps
regenerated -- vs_reader OK, zero APDUs ending in 60 61, SELECT MF now
61 29 / 61 25.
- Split the PPS exchange into the two standard GSMTAP-SIM subtypes:
0x02 (PPS_REQ, raw request bytes, span ss..es_req) and 0x03 (PPS_RSP,
raw response bytes, span es_req..end), replacing the single 0x02 packet
with concatenated request+response that deviated from libosmocore's
gsmtap.h. Wireshark now dissects both halves; simtrace2-pysniff needs
no change (its importer already maps 0x03 to 'pps' and _decode_pps()
parses the 0xFF-leading response frame). PPS annotation/log unchanged.
- gsmtap_stream.py: GSMTAP_SIM_PPS_REQ/RSP constants; header field labels
aligned with libosmocore's struct (signal_dbm/snr_db).
- Docs: GSMTAP event tables (EN/RU) updated, PPS deviation note removed;
corrected the res-byte flag attribution -- upstream simtrace2 never
sets res, so the GSMTAP_FLAG_BAD_FCS carriage is this decoder's own
extension (semantics mirror simtrace2's USB data flags).
- Tests: assert the PPS subtype values; baseline regenerated (only the
version line and test listing differed -- no decode-metric changes).
68/68 unit tests.
Full translation following the otaman README_RUS.md convention (prose and
code-block comments translated; commands, flags and paths unchanged).
Links back to the English README at the top.