The release page now leads with the two supported installation paths —
release package (Linux tar.gz / Windows zip, setup/start, SHA256SUMS) and
source checkout for development — before GitHub's generated changelog.
The block is written by the workflow (`--notes-file` + `--generate-notes`),
so every future v* release carries it. The v1.9.7 release body is updated
manually to the same text; no code or version 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.
- tests: TestDecodeStep drives decode_step() with a scripted byte queue
(read/peek/consume stubbed over _replay + the queue; the re-align/PPS/
ATR/annotation sinks stubbed): 'CX'/'EX' exchanges frame and emit clean,
0x5F frames as a command (it was always eaten by the idle-noise gate
before v1.9.3), the 0x61 text-byte boundary is a one-byte suspicious-CLA
discard, the invalid-INS re-frame recovers a shifted SELECT, 0xFE still
takes the idle-noise path.
- tests: TestAtrHuntDetect drives handle_atr(): 0xC4 0xA4 / 0xC4 0x89
hand the header to DATA via _replay, CLA 0x00 stays excluded, an invalid
INS does not hand off.
- pd.py: the hunt command-detect guards peek_byte() returning None (a
capture ending right after a plausible CLA byte raised TypeError, seen
only by decode()'s crash protection);
test_eof_after_plausible_cla_is_clean pins the clean return.
- AGENTS v1.9.4 section; baseline regenerated (version + test listing;
compare_baseline: IDENTICAL); 95/95 unit tests.
- 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.
- handle_pps(): flag the first post-PPS character; _read_byte_clk() re-measures
the CLK period there and accounts the measurement's 16 edges. The Alcor
reader upclocks 4.0 -> 4.8 MHz at PPS (FI/DI still 512/32), so the ATR
measurement was stale: 64 vs 53.33 samples/ETU, no post-PPS APDU framed.
- _wait_clk_rising(): project to the n-th edge (skip to target - half a
period, take one edge) instead of "skip n-2 cycles + 2 edges", which lost
~1 edge per call on fractional periods and slid the samples ~0.5 ETU early
by the parity bit. xiaomi 7698 -> 2, 4gmodem 7218 -> 0, S21p_sim2 394 -> 0
CHKSUM lines (previously misread as dirty-wire noise).
- CONCAT gate: EMBEDDED_CLA excludes the structural 0x4x/0x5x classes (payload
false hit on the Alcor secured packet); mirrored in tools/vs_reader.py and
covered by a new vector in tests/test_gsmtap.py.
- tools/capture_baseline.sh: add the alcor_reader_sample fixture; baseline
regenerated (APDU counts and RESULT: OK unchanged everywhere, 0 CONCAT).
- 75/75 unit tests. README/AGENTS updated.
Follow-up to ad3ff6b: the two remaining prose mentions of the old
short name become SIMtrace Analyser (AGENTS.md v1.9.0 note, test
comment). Upstream 'simtrace2-sniff'/'simtrace2' references stay --
those are the Osmocom tool/hardware, not the renamed project.
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.
Links the first mention and documents it as the recommended tool for
visualising live traces and viewing saved captures, with GSMTAP pcap
import/export support.
Newer libsigrokdecode signals end-of-capture from wait() by raising
EOFError("samples exhausted"); Linux 0.5.3 instead returns None / raises
SystemError, so this only reproduced on Windows. The framework explicitly
accepts EOFError out of decode() as normal termination, but decode() caught
it in the generic except Exception handler -- printing a traceback, logging
DECODE ERROR, and resyncing up to 50 times, which flooded the log at the end
of every capture.
- decode() and the five "except SystemError: raise" guards now use
"except (SystemError, EOFError): raise", letting the end-of-stream signal
propagate for a clean exit. The critical guard is
_measure_clock_period(), whose try wraps a wait() loop.
- 2 new unit tests assert decode() re-raises EOFError and SystemError.
- 67/67 unit tests; test_8/test_7/xiaomi_phone unchanged (Linux path).
VERSION -> 1.7.1.
The decoder could end decode() early on an idle/busy live line; the frontend
symptom was a flood of "sr_session_stop: session was NULL" after the session
stopped. Harden the EOF detection and stop the capture from dropping.
pd.py:
- _at_eof(): a step that consumes only queued bytes (_replay / peeked_byte,
e.g. the invalid-INS re-frame) no longer counts as end-of-capture; bounded
to 64 consecutive no-advance steps.
- _stall_check(track=False): the idle-LEVEL wait in wait_data_falling returns
immediately at the same sample on an idle line -- not EOF. Only "pins is
None" ends the wait there; sample-stall detection is kept for the edge wait.
- resync_idle(): guard wait()==None (was an unguarded pins[...] TypeError at
stream end).
Effect: samsung_phone_sample.sr had halted at ~21.9M/320M samples with 1 APDU;
it now decodes through ~293M to a true end (939 APDUs, CHKSUM 176). The 6
CONCAT packets are real mis-framed STK bursts now reachable (known Samsung
de-concatenation family). All other traces unchanged.
start.sh:
- Auto-restart is now the default (--no-loop disables) so an intermittent
FX2 USB stop reconnects instead of ending the session.
- Per-session pcap suffix (FILE.N.ext) so a restart never overwrites the
previous capture.
- Filter the repetitive "sr_session_stop: session was NULL" teardown line
from stderr (real decoder errors still pass).
Validation: test_8 56/0/OK, test_7 65/0/OK, xiaomi_phone 640, xiaomi_cold
473, 4gmodem 327, A55 407/395, S21p 399/375, target S21p 369; 0 CONCAT;
65/65 unit tests (10 new). VERSION -> 1.7.0.
Add hunt-side live ETU recovery so a card already at a non-default F/D
(e.g. Samsung F=512/D=32 -> 16 CLK/bit) can be decoded without a 372-rate
ATR, and bound the all-0xFF hunt so an idle/unsyncable line can never loop
forever.
- _recover_etu_in_hunt(): measure the live ETU from DATA edges while still
in FIND START, then re-derive clock_skip; reuses _confirm_etu validation.
- _atr_hunt_failure_action(): pure recover/retry/resume_data decision, with
ATR_HUNT_ETU_ATTEMPTS=3 / ATR_HUNT_IDLE_RESETS=3 / ATR_HUNT_FAIL_LIMIT=12.
- Persistent counters reset only on a successful ATR parse (not RST re-arm),
so RST-cycling phones cannot starve the escape.
- 8 new unit tests (55 total).
Validated on the full trace suite (test_8 56/0/OK, test_7 65/0/OK, xiaomi
640, xiaomi_cold 473, 4gmodem 327, A55 407/395, S21p 399/375, target S21p
369; 0 CONCAT, RESULT: OK) with no regressions.
The recommended tap is the short cable with external pull resistors whose
pins are fixed and known in advance (D5=CLK, D0=I/O, D4=RST, D2=VCC), so
the auto-detection tool is no longer needed. Drop it and all references:
- Delete tools/detect_pins.py.
- README: replace the 'Detecting the wiring' section with a fixed-wiring
note; drop the Python/numpy prerequisite and the detect_pins capture
step from the Windows guide.
- setup.bat: remove the Python/numpy check and the 'find the wiring' next
step.
- 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).
- 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).
Full -A iso7816 printed all annotation rows including the per-byte 'bytes'
row (thousands of lines per capture; ~22k on xiaomi_phone, ~11k on A55).
Default is now '-A iso7816=apdus,iso7816=warnings' (decoded APDUs + decoder
warnings/errors only); --debug restores the full '-A iso7816' output plus
the existing -l 4. stderr decoder log (banner, PACKETEND hex, glitches,
discards) is untouched.
Residual CONCAT mis-frames (4gmodem/A55_sim2/S21p_sim2) traced to a silent
5-byte drop at an invalid-INS T=0 header (0x0f 00 a4 08 04). Drop only the
stray leading byte and re-frame the remaining four via _replay so the real
command (00 a4 08 04 02 2f 00 -> 61 1c) is recovered.
- _note_discard()/_consume_first(): every consumed-but-unemitted byte is
now logged; idle 0x00/0xFF discards are rate-limited, real data always
gets an ANN_WARN annotation.
- All read-until-SW loops log skipped non-SW bytes.
- All traces: CONCAT 0; BAD_FCS/GARBAGE 0; test_8/test_7 RESULT OK;
47/47 unit tests; baseline byte-identical.
History rewritten to a single root commit so no example trace is stored
in the repo (fixtures are local-only, gitignored). README keeps only
generic <trace>.sr placeholders; AGENTS.md retains the fixture names.