- the Install section now leads with the release artifacts (the wheel via
pipx/pip, the direct release URL, `gh release download` for the
version-agnostic case, and the source archive) and keeps the git clone as
the development path. It finally names the launchers (./start.sh,
start.bat): a clone needs no install, but the README never said how to start
it while the Running section only showed the console script that an install
provides.
- Windows: the USB section now says the .bat launchers mirror the shell ones
and call the `python` command (Python 3 must be on PATH); the Install section
carries the same note.
- start.bat: --db/DB was parsed and then never passed to the server; it is
passed now, and --gsmtap-port is sent only for gsmtap capture (as start.sh
already did). Both .bat files passed values unquoted and stripped the
quotes of pass-through options, so paths with spaces broke
(--db "C:\My Data\...", --web-dir, --pcap, --output); values are quoted now
and pass-through keeps the original quoting (argument assembly through
delayed expansion).
- the launcher usage comments include the `disabled` capture mode.
- verified the argument assembly with Wine cmd.exe and an echo harness: the
quoted paths survive, --db arrives, --gsmtap-port appears only for gsmtap;
547 Python + 80 Node tests.
- pyproject declared build-backend = "setuptools.backends._legacy:_Backend",
which does not exist: `python -m build`, `pip install .` and even
`pip install -e .` died with "Backend ... is not available". It is
setuptools.build_meta now.
- the wheel and sdist carried only the Python packages, and _default_web_dir()
resolved <site-packages>/../frontend, so an installed server started fine but
answered every static request with 404. setup.py's build_py now copies the
checkout's frontend/ into simtrace_analyser/web/ inside the build (the PWA's
tests excluded), and _default_web_dir() prefers that packaged copy, falling
back to <repo>/frontend for source and editable runs. The source directory
is resolved from setup.py (so an sdist build works) and a missing frontend/
fails the build loudly instead of shipping a package without its UI.
- MANIFEST.in grafts frontend/ (with the READMEs, the udev rule and the four
launch scripts) into the sdist, so a wheel built from the sdist has the PWA.
- README (EN/RU): an install-from-a-release section (pipx/pip with the wheel,
the direct release URL for pip) and the --web-dir default documented as the
bundled PWA, or <repo>/frontend from source.
- tests/test_packaging.py pins the packaged-first resolution (including the
empty-directory fallback), the source checkout default, the real build
backend, the manifest graft and setup.py's bundling.
- verified: a fresh venv install from the wheel and from the sdist serve
GET / (200, byte-identical index.html, PWA v2.7.10) out of
site-packages/simtrace_analyser/web; an editable install still resolves
<repo>/frontend. 547 Python + 80 Node tests.
- the detail header used --bg-input in dark mode (plus a light-only #dbeafe
override), so it only matched the list selection in the light theme; it now
uses var(--selected-bg) — the same variable as the selected row — so the two
are in sync in both themes (light unchanged)
- .apdu-entry:hover used --bg-panel (a grey in the same family as the GET
RESPONSE pair rows); both themes now set --hover-bg — the accent at 16%
(dark) / 10% (light), a translucent tint that stacks with whatever the row's
background is — and a single hover rule uses it: hovering tints the row
blue, lighter than the selection and a different hue from the pair rows.
The rules stay ordered .apdu-pair → :hover → .selected, so hovering the
selected row keeps its selection colour
- Node tests pin the hover variable per theme, the hover rule (and that the
old grey rule is gone) and the detail header's variable; 540 Python +
80 Node tests
- SW cache simtrace-analyser-v91 (cosmetic, no version bump)
- the selection used --bg-input (#334155, a neutral slate) in dark mode, which
sat right next to the GET RESPONSE pair rows' grey tint
(--pair-row = rgba(148,163,184,0.12)) and was easy to mistake for them; the
light theme already had its own blue (#dbeafe) through a
[data-theme="light"] override
- both themes now set a --selected-bg variable (dark #1e3a8a = blue-900 — the
palette counterpart of the light blue-100 — light #dbeafe unchanged) and a
single `.apdu-entry.selected` rule uses it, so the override is gone
- Node test pins the variable in both themes and the single rule
- SW cache simtrace-analyser-v90 (cosmetic, no version bump)
- the P3 row read "0x02 — 2 bytes" for both kinds of P3; the decode now marks
it (p3_kind): a command that carries data has Lc (the command-data length),
a case-2 command (APDU_SPEC 'le', e.g. READ BINARY/RECORD, GET RESPONSE,
SEARCH…) has Le (the expected response length). The detail row appends
"· Lc (command data length)" / "· Le (expected response length)"
(«Lc (длина данных команды)» / «Le (ожидаемая длина ответа)»)
- a SEARCH RECORD without an SFI searched the current EF (TS 102 221
§11.1.7.1) but showed no file at all; it now resolves like READ/UPDATE:
the SFI's declared file when SFI-addressed (an unresolved SFI stays marked
"SFI n"), otherwise the current selection — with the stale-selection note
when that EF is not a record file. Verified on session 193: the 5.371 s
search (P2=0x04, no SFI) now shows 4FA1, the 5.434 s one (SFI 2) shows 4F3A
- tests: p3_kind cases (Le/Lc/none) and the SEARCH target cases (current EF,
SFI-resolved, unknown SFI, transparent selection); a Node pin for the P3
row and the two new Russian strings; 540 Python + 77 Node tests
- PWA_VERSION/pyproject/version.py 2.7.9, SW cache simtrace-analyser-v89
- the PBR SFIs were learned only by the tracker; the message decode's
per-channel SFI map still did not know them, so the SEARCH RECORD addressed
by SFI 2 after the PBR read showed "SFI 2, unknown file" (the reported
case: session 193, 5.434 s — the PBR read at 4.451 declared ADN 4F3A/SFI 2)
- database._advance_state now parses an EF_PBR record it sees (a READ
RECORD's data or an UPDATE RECORD's body) and merges its FID+SFI pairs
into the channel's SFI map, so SFI-addressed SEARCH/READ resolve without
the file's own FCP
- the learned relations are cached per DF for the session: the live trace
leaves DF_PHONEBOOK for DF_TELECOM (SELECT 7F10/6F06) and comes back, and
the DF change used to rebuild the map from the static table only, dropping
the PBR's pairs — the re-entry now merges the session cache
- verified on session 193: the SEARCH at 5.434 s (SFI 2) now resolves to
4f3a (EF_ADN), as do the later ones (31.562/253.475/279.874 s)
- tests: the DB-level case — a PBR read, an excursion to DF_TELECOM and back,
then an SFI-addressed SEARCH asserting the declared file; 538 Python +
76 Node tests
- PWA_VERSION/pyproject/version.py 2.7.8, SW cache simtrace-analyser-v88
- EF_PBR (TS 31.102 §4.4.2.1) is the card's declaration of its phonebook:
the constructed tags A8/A9/AA announce the type-1/2/3 groups and each nested
primitive tag C0…CC names the file kind plus its FID — and, in a three-byte
object, its SFI (length 2 = FID only). pySim has no parser for it (its
EF_PBR class still carries a "# TODO: a80ac0034f3a…" stub), so the decoder
follows the spec: new simtrace_analyser/pbr.py
- the tracker decodes every EF_PBR record it observes (a READ RECORD's data
or an UPDATE RECORD's body) and uses it for the DF it belongs to: the
phonebook files get their symbolic names (4FA1 = EF_PBC, 4F3A = EF_ADN,
4F01 = EF_IAP, 4F4A = EF_EXT1, 4F3D = EF_CCP1, 4F61 = EF_EMAIL, …), an
earlier placeholder name is upgraded when the PBR arrives later, and the
declared SFIs feed the DF's SFI→FID map (so SFI-addressed reads resolve
without waiting for an FCP)
- verified on the live sessions 47/193:
DF_TELECOM/DF_PHONEBOOK/EF_PBC (4FA1) and the SFI map
{2→4F3A, 4→4FA1, 6→4F4A, 7→4F4F}
- also fixed: the "SFI = FID's five low bits" fallback (TS 102 221
§11.1.1.4.8) no longer applies to a DF/ADF's FCP, which has no SFI
- tests: tests/test_pbr.py (the two real records, SFI-less objects, padding,
truncation, merging, the shape check) and tracker cases (names, path,
SFI map, the later-PBR upgrade); 537 Python + 76 Node tests
- README/README_RUS document the PBR naming; PWA_VERSION/pyproject/
version.py 2.7.7, SW cache simtrace-analyser-v87
- the tracker kept only the immediate parent DF, so a phonebook file read as
"DF_PHONEBOOK/4FA1" — the trace's own selection path (00a4080406
7F105F3A4FA1) spells out the whole chain; the channel state now carries it
(selected_df_state() returns (df, path), e.g. ('5f3a', ['7f10', '5f3a'])),
the tracker stores it per file and _file_path() renders it:
"DF_TELECOM/DF_PHONEBOOK/4FA1"
- chain rules: a child DF selected by FID appends to the current chain, MF and
the ADF restart it, an AID is its own root, a path selection uses the DFs
in the path (an explicit '7fff' collapsing to the ADF), and an EF selects
keeps it. A leading MF is implicit in a longer path; MF-only files keep
"MF/…", ADF files "ADF/…" (the previous labels unchanged)
- the chain is learned from the FCP sighting too and extends a shorter path
recorded earlier, so entries created from a GET RESPONSE still get the full
path
- verified on the live traces (sessions 47/193): 5f3a/4fa1 ->
DF_TELECOM/DF_PHONEBOOK/4FA1, 5f3a/4f30 -> DF_TELECOM/DF_PHONEBOOK/EF_PBR
(4F30), while 3f00/2fe2 -> MF/EF_ICCID (2FE2) and 7fff/6f07 ->
ADF/EF_IMSI (6F07) stay as before
- tests: selected_df_state (path/FID/MF/ADF/AID/EF cases) and a tracker flow
with a path selection asserting the full path; 529 Python + 76 Node tests
- README/README_RUS show the chain; PWA_VERSION/pyproject/version.py 2.7.6,
SW cache simtrace-analyser-v86
- a record whose FCP understates the layout (the reported case: record_len 2
while the READ RECORD carried P3=0x2C and returned 44 bytes) had its rows
cut to two cells by the v2.7.3 length rule, hiding 42 newly-observed bytes
while the summary still counted them
- the filler now stops at the known length only while the data fits inside
it; beyond that — and when the length is unknown or absent — the row ends
at the data, so nothing observed is ever hidden and no bytes are invented
- verified on the live payload (session 55, 5f3a/4f09 change 0): before = 2
cells, both changed; after = 16+16+12 cells with 2 changed and 42 newly
observed, matching the summary line
- tests: the regression case (a 2-byte record_len with a 44-byte read) and
the unknown-length case, whose row now ends at the data; 527 Python +
76 Node tests
- PWA_VERSION/pyproject/version.py 2.7.5, SW cache simtrace-analyser-v85
- the header's trailing meta is labelled instead of bare numbers:
"ADF/EF_IMSI (6F07) — READ BINARY · APDU #490 · 0251.199 sec" (the record
part already read "Record 2 / 5")
- the summary counts only what happened: "{c} bytes changed · {n} bytes newly
observed" drops whichever part is zero ("4 bytes changed" alone when nothing
was first observed) and an empty diff reads "No changes" instead of
"0 bytes changed, 0 bytes newly observed"
- i18n: the combined key splits into '{c} bytes changed' / '{n} bytes newly
observed' plus 'No changes', 'APDU' and 'sec' ('{c}','{n}' placeholders keep
the existing replace-based substitution; the Russian keeps the established
wording: «Изменено байт: {c}», «Впервые наблюдается байт: {n}», «с»)
- tests: the header and summary assertions follow the labels, a short-file
case pins the omitted zero, and an empty-diff case pins 'No changes';
527 Python + 76 Node tests
- PWA_VERSION/pyproject/version.py 2.7.4, SW cache simtrace-analyser-v84
- the hex views padded every line to 16 bytes with '--', so a 2-byte file
looked like it was missing data; the row now ends at the known file/record
length (from the FCP — hexImageRows gained the size parameter), while '--'
still marks bytes *inside* the file that were never observed (an unknown
length keeps the old row padding, nothing better is available)
- the offset column is shown only when a view spans more than one row — a
four-hex-digit 0000 above two bytes was noise
- the legend lists only the marks actually rendered: 'never observed' is gone
from a short/fully-observed view, and 'newly observed' when nothing was
first observed (the summary line still counts both)
- tests: pure hexImageRows cases (the 2-byte file, a 9-byte record with an
unobserved tail, the unknown-size fallback) plus two renderFileDiff cases
pinning the missing filler/offset and the trimmed legend; 527 Python +
75 Node tests
- README/README_RUS describe the new hex-view behaviour;
PWA_VERSION/pyproject/version.py 2.7.3, SW cache simtrace-analyser-v83
- the 'Proactive + events' preset is gone: the proactive one (FETCH,
TERMINAL PROFILE and the SW 91xx initiators) plus the individual ENVELOPE
entry cover it, and its Russian key is removed with it
- with several entries checked the filter button now reads "Selected: N"
(«Выбрано: N») instead of "N commands"; a single entry keeps showing its
name and an empty selection stays "All commands"
- tests: the removed preset's case is replaced by an absence pin in the menu
test, and the summary case follows the new wording; 527 Python + 72 Node
tests
- README/README_RUS list two presets; PWA_VERSION/pyproject/version.py 2.7.2,
SW cache simtrace-analyser-v82
- the old "FETCH: ANY PROACTIVE" grouping label is gone: filter_label() now
gives every command its own label, so the menu lists FETCH and TERMINAL
RESPONSE individually — the multi-select filter (and the presets) cover the
combinations that the grouping used to bundle
- the proactive presets keep their meaning: they name FETCH and TERMINAL
PROFILE and reach the rest (a TERMINAL RESPONSE included) through the SW
91xx rule; selecting FETCH now also brings its GET RESPONSE, which carries
the fetched proactive command
- tests: the proactive-preset case follows the split (a TERMINAL RESPONSE is
covered by 91xx, not by its name) plus a new case pinning the separate
labels and the FETCH/GR pairing; 527 Python + 73 Node tests
- PWA_VERSION/pyproject/version.py 2.7.1, SW cache simtrace-analyser-v81
- the single-command <select> becomes a checkbox dropdown: the filter is a
set of entries (an empty set shows everything), so any number of commands
can be shown at once instead of one
- three fixed presets select whole conversations:
* Proactive sessions — FETCH, TERMINAL PROFILE and every command the card
answered with SW 91xx (the proactive-session initiators)
* Proactive + events — the same plus ENVELOPE
* File operations — SELECT, READ BINARY, READ RECORD, UPDATE BINARY,
UPDATE RECORD (their GET RESPONSE, which carries the data/FCP, comes
along through the existing response_for linkage)
presets and single commands combine as a union, and the menu lists the
session's commands with their counts
- the button summarises the selection ("All commands" / one name /
"{n} commands"); Escape or a click outside closes the menu; focusing an
APDU from the Files panel/map still clears the filter first
- tests: a new filter test (the matching rules per entry kind, the GR
linkage, the presets, the union, the summary, the menu rows, the RU keys
and the wiring pins), and the focusMessage tests follow the new state;
527 Python + 72 Node tests
- README/README_RUS describe the filter; PWA_VERSION/pyproject/version.py
2.7.0, SW cache simtrace-analyser-v80
- an update row whose image changed (or whose bytes were first observed)
offered a generic "Details" link; it now says "Show diff" — Russian
«Изменения» — while a card-side change keeps its "Changed on card!" link
(both open the same change modal)
- the now-unused 'Details' RU key is gone; tests update the wiring pin and
the Russian-translation list; 527 Python + 61 Node tests
- README/README_RUS mention the new label; PWA_VERSION/pyproject/version.py
2.6.5, SW cache simtrace-analyser-v79
- a card-side change is discovered by a read operation, so the timeline row
now reads "Read EF_IMSI" — the row keeps its warn colour and layout — with
"Changed on card!" as the trailing underlined link that opens the change
modal; an update row keeps the generic "Details" link
- the 'Changed on card' verb string is gone; the new 'Changed on card!' key
carries the Russian «Изменено на карте!»
- tests: the verb/label cases updated (card_change -> Read, "Read EF_LOCI")
and the Files-view wiring test pins the kind-dependent link label;
527 Python + 61 Node tests
- README/README_RUS describe the new row wording; PWA_VERSION/pyproject/
version.py 2.6.4, SW cache simtrace-analyser-v78
- the header title drops its "UICC" prefix — "GSMTAP trace analyser" is enough
- the trace topbar's search input is half its previous width (flex basis
33.333% -> 16.667%); the session name now ellipsizes instead of wrapping
the tighter row
- the side-panel view selector moved out of the scrollable side panel into
the trace topbar: right-aligned over the panel at the panel's width
(--side-panel-w: 260px, the negative right margin cancelling the topbar's
16px padding so the edges line up), so it stays visible at all times while
the panel scrolls
- tests: the topbar/selector position pins (the Files-view wiring test and
style.test.js), the shared width variable, the halved search basis and the
title; 527 Python + 61 Node tests
- README/README_RUS: the selector's new place; PWA_VERSION/pyproject/
version.py 2.6.3, SW cache simtrace-analyser-v77
- focusMessage()/scrollToType() highlighted the target and removed the class
after 1500 ms, so a Files-row (or map/category) jump looked selected for a
moment and then lost it; on top of that any re-render (the 500 ms live
poll, a filter/search change) rebuilt the list and dropped the highlight —
even for a clicked entry
- the selection is now persistent app state (STATE.selectedIdx): the render
marks the entry (gap rows included), focusMessage records the index before
rendering, showDetail records it for plain clicks, scrollToType persists it
too (clearing the previous highlight), and a new session/capture resets it
so a stale index cannot mark an unrelated APDU
- tests: a stub-driven focusMessage case (state + detail updated, no render
and no timer), the filter-clearing case, and source pins for
renderMessages, focusMessage, scrollToType, showDetail, loadSession and
startCapture; 527 Python + 58 Node tests
- README/README_RUS: the jump leaves the record selected; PWA_VERSION/
pyproject/version.py 2.6.2, SW cache simtrace-analyser-v76
- the APDU list's 12px rule is folded into the global scrollbar style, so
the side panel, APDU detail pane, dialogs (file-diff body), session
lists/grid and the hex views' horizontal bars all use the same 12px bar
with a 6px rounded thumb — no per-element overrides remain to drift,
and the horizontal bars get an explicit height instead of the browser
default
- Node test pins the global rule (12px width/height, 6px thumb radius) and
the absence of per-element overrides; 527 Python + 55 Node tests
- SW cache simtrace-analyser-v75 (cosmetic, no version bump)
- the side panel had no scrollbar rule of its own, so it fell back to the
global 6px default while the APDU list uses 12px; the two now share one
rule (width 12px, thumb radius 6px = half the bar), so they cannot drift
apart again — the other panes keep the thin global bar
- Node test pins the shared rule (and the 6px global default)
- SW cache simtrace-analyser-v74 (cosmetic, no version bump)
- jumpToMessage() rendered the APDU list through the unqualified
renderMessages(), whose tail pins the view to the bottom (the live-capture
stick), and only then let focusMessage() smooth-scroll back up to the
clicked record — the visible jump-to-end-then-back. It now renders with
skipScroll, so the only scroll is the focus itself
- the content-follow became conditional: shouldStickToBottom() pins the view
to the newest messages only while the reader is already at the bottom
(40 px slack), so the 500 ms live poll no longer pulls away a view the
reader scrolled to — a Files/map jump is no longer yanked down by the next
batch. A freshly opened session or a new capture still lands at the
bottom (renderMessages(false, true))
- tests (node): the jump's paging/render(true)/focus sequence with stubs, the
no-session no-op, the stick decision (at bottom / within slack / scrolled
up / explicit jump / forced / first render) and source pins for
renderMessages, jumpToMessage, loadSession and startCapture; 527 Python +
53 Node tests
- README/README_RUS note the follow behaviour; PWA_VERSION/pyproject/
version.py 2.6.1, SW cache simtrace-analyser-v73
- CLA decode (TS 102 221 §10.1.1/§10.3): standard channels 0-3 from b2-b1
and the extended channels 4-19 from b4-b1. The old further-format rule
read only b2-b1 (channel 8 read as 6, 19 as 7) and the ETSI '11x0 xxxx'
coding (0xC0-0xEF) was not handled at all; the class ranges now follow the
specs (0x80-0x8F UICC, 0xA0-0xAF SIM, 0xC0-0xEF ETSI extended, 0x20-0x3F
reserved, 0xFF invalid) and SM/chain are decoded per family — the SM
extraction and the chaining bit (b5, not b6) were off by one bit
- per-channel state: database._decode_rows/_replay_context use the shared
cla_channel() helper — before, CLA & 0x03 with no +4, so a channel-5
SELECT clobbered channel 1's selection and a channel-8 one channel 0's
(the local traces already open channels: 29 MANAGE CHANNEL opens, 1-2)
- COMPRION T=0 import: the synthesized GET RESPONSE keeps the channel —
a plain CLA for 0-3 (as real terminals do) and 01x0 for 4-19
- ATR historical bytes: the UICC's COMPACT-TLV objects (TS 102 221 §6.3.1)
with card service data, the card capabilities (ISO 7816-4 §8.1.1.2.7
Table 88: assignment methods and max logical channels — the workspace
cards announce 4 and "8 or more") and the status indicator ('00' trailing
bytes / the '8Y' TLV); a truncated ATR no longer raises
- MANAGE CHANNEL success replies decode the assigned channel byte
- capture/import: a 'tpdu' packet that is not APDU-shaped (shorter than the
header, CLA FF, ISO-reserved class) is skipped and counted, so a
character-level or mis-sampled stream no longer pollutes the decode
(the local DB's outlier sessions are exactly that)
- tests: CLA table vectors, per-channel state isolation on channels 8/12,
COMPRION GR channels, filetrack extended channels, ATR compact-TLV and
card-capability cases, the capture skip counter, the PWA rows; 527 Python
+ 47 Node tests
- README/README_RUS document the extended channels, card capabilities and
the skip counter; PWA_VERSION/pyproject/version.py 2.6.0, SW v72
- the file/DF/SFI dictionaries move to simtrace_analyser/ef_tables.py and are
extracted from the pinned specs: TS 102 221 (MF, Annex H.1), TS 31.102
(ADF USIM 4.2 and the child DFs 4.3/4.4/4.6, the H.1-H.12 SFI annexes, the
Annex L.6 Relay Node/certificate files; Annex A/E as the ADF/DF file
completeness cross-check), TS 31.103 and TS 51.011 - 20 DF tables (~370
entries) and 14 SFI tables, up from ~180 flat names and 3 SFI tables
- names are DF-scoped: a FID reused across DFs resolves to the file its DF
context names - 4F09 is EF_SUPI_NAI under DF_5GS but EF_PROSE_GC under
DF_ProSe, 6F07 is EF_IMSI in the USIM/SIM but EF_IST in the ISIM, 6F4E is
EF_EXT4 in DF_TELECOM but EF_EXT5 in the USIM ADF; the flat table keeps
only names that are identical everywhere and is the fallback when no DF is
known (the two names the old table had wrong there are fixed: 6F0A is
EF_AC_GBAUAPI, 6F53 EF_LOCIGPRS)
- the SELECT-path decoder resolves the 7FFF children against the USIM ADF
table, and the Files view names files DF-first - including files first seen
only via their FCP (a select-only file used to keep the bare FID, e.g.
ADF/6F3C is now ADF/EF_SMS (6F3C))
- tests/data/spec_files.json pins the extracted tables; tests/test_decode.py
asserts the DF/ADF tables cover them, the SFI tables match them exactly and
every flat name is a spec name for that FID; tests/extract_spec_tables.py
regenerates the fixture from the PDFs and --check re-verifies the tables
- README/README_RUS document the dictionary and its sources; PWA_VERSION and
pyproject/version.py 2.5.0, SW cache simtrace-analyser-v71
- modal max-width 900px -> 1100px
- the legend's swatch and label were separate flex children, so a line break
could split them and the three entries wrapped; each pair is now one
`.hex-legend-item` (nowrap, no shrink) and the legend itself is
`flex-wrap: nowrap` with `overflow-x: auto`, so it stays on a single row
- Node test: the modal test now checks the path header and that the legend
follows the views as three grouped items
- SW cache simtrace-analyser-v70 (cosmetic, no version bump)
- the before/after hex views (16 bytes per row, two side by side) had little
room at 760px; the modal's max-width is now 900px (the base `.modal` width
of 90% still keeps it inside the viewport on smaller screens)
- SW cache simtrace-analyser-v69 (cosmetic, no version bump)
- the modal header showed only the flat name (EF_ICCID, 4F09); it now shows the
path built from data already available: the tracker's DF identity named via
KNOWN_FIDS (MF, DF_GSM_ACCESS, DF_TELECOM, …), 'ADF' for the current
application (7FFF), DF_CHILDREN for DF-qualified EF names (4F52 →
EF_KcGPRS) and the EF.DIR labels for AID-selected applications —
e.g. `MF/EF_ICCID (2FE2)`, `ADF/EF_IMSI (6F07)`,
`DF_GSM_ACCESS/EF_KcGPRS (4F52)`, `AID A00000015141434C00/4F09`
- exposed as `path` in the change-details payload and in the files entries
(the timeline rows keep the short name — they have no room)
- the colour legend (changed / newly observed / never observed) moved below
the two hex views
- tests: `_file_path` cases (MF, ADF, DF-qualified EF, AID with and without an
EF.DIR label, FID-as-name) plus the payload field, and the Node modal test
asserts the path header and that the legend follows the views
- v2.4.2; SW cache simtrace-analyser-v68
- the modal's hex views carried a 16-byte ASCII column per row; file data is
rarely text and the column halved the room for the two before/after views,
so it is gone: `hexImageRows` no longer builds an `ascii` field,
`hexImageHtml` no longer emits it and the `.hex-ascii` CSS rule is removed
- tests: the hex-row test asserts the field is gone (so it cannot come back
silently); README/README_RUS wording updated
- SW cache simtrace-analyser-v67 (cosmetic, no version bump)
- `loadSession` begins by stopping a running capture; when there is nothing
to stop the server answers 400 'No active capture' (or 403 with capture
disabled) — and since the v2.3.1 `_fetch` status fix those answers throw, so
the whole load was skipped and the UI alerted "Failed to load session":
no session could be opened at all
- new `stopCaptureIfRunning()` swallows exactly that no-op answer; both
`loadSession` and the capture button use it, and the button now resets its
state outside the try so a tolerated stop still clears the UI
- the `_fetch` status check stays — real errors keep surfacing (an unknown
session still throws 'Session not found')
- tests: the helper resolves on a 400 and still calls once; source pins that
`loadSession` uses the tolerant helper (the regression guard) and that
`stopCapture` resets the state
- v2.4.1; SW cache simtrace-analyser-v66
- the timeline's Δ glyph became a **Details** button, and it now appears on
Update rows too (not just card-side changes); EN "Changed (card)" ->
"Changed on card", RU "Изменено (картой)" -> "Изменено картой"
- the details modal shows the affected unit — the whole file, or the single
record that changed — **before and after** the change as hex views (16 bytes
per row, ASCII column, never-observed bytes as `--`), with the bytes that
changed and the bytes the operation *first observed* highlighted in two
different colours, plus the changed/new byte counts and the APDU reference;
the old before/after byte table is gone
- `GET /api/sessions/<id>/files/image?key=…&change=…` returns those two views
(segments with offsets) plus the change's metadata; the images are
reconstructed from the current image by undoing the later changes
(`FileTracker.change_images`), so nothing is duplicated per event — every
value change is an event, hence the values are exact (a byte only first
observed later can appear as known in both views)
- tracker: change events carry `observed_ranges` (bytes that were unknown
before the operation) next to `ranges`; an update that only observes new
bytes now records an event (so it can be inspected); record-file ranges are
in-record byte offsets (the record number stays in its own field), which the
previous "record number as position" could not express for a hex highlight
- tests: change-images reconstruction (before/after per event, later changes
undone, records isolated, observation-only events, unknown gaps), the image
endpoint (200/404/400), and Node tests for the hex-image helper (highlight
classes, unknown gaps, ASCII, row limit) and the modal rendering
- v2.4.0; SW cache simtrace-analyser-v65
- `API._fetch` never checked the HTTP status: an error body (e.g. the 404
`{"error": "Session not found"}`) was returned as data, so the Files view
rendered it as "No file operations recorded" — and, worse, that bad payload
was cached per session id, which is why only re-opening the session (which
resets the cache) appeared to fix it. It now throws with the server's
message for any non-OK response (all 13 `await API.*` call sites are inside
try/catch) and marks the server unavailable only for a real network error
- `renderFileView` accepts a payload only when it carries a `timeline` array:
a genuine `timeline_total === 0` keeps the empty state, anything else shows
"Failed to load files: <reason>"; failures are not cached, so the next visit
retries; a response that arrives after the user switched sessions is ignored
- tests: HTTP-error vs network-error behaviour of `_fetch` (network error is
the only disconnected state), the valid/empty/error render paths and the
retry-after-error, the stale-response guard
- v2.3.1; SW cache simtrace-analyser-v64
- new `simtrace_analyser/filetrack.py`: a single-pass tracker over a
session's decoded messages, keyed by (DF, FID) and trace-wide (resets only
wipe the per-channel selection): counters (`select`/`read`/`update`,
successful operations only), a sparse image of the observed bytes
(transparent files by offset, record files per record number) and the
change log
- changes: **device** = an UPDATE seen in the trace (applied to the image,
`no_change` flags a provable no-op); **card** = a READ whose bytes differ
from the image on bytes already known (logged at the surfacing read with
the changed ranges and the before/after hex; never-seen bytes just fill
the image, they are an observation, not a change)
- SFI-addressed accesses join the same entry as path/FID ones: the SFI ↔ FID
relations come from the normative tables and from every surfaced FCP `88`
DO (including the spec's "SFI = 5 LSBs of the FID" rule when the DO is
absent); an SFI used before its relation is learned stays provisional and
merges into the resolved entry
- the DF identity follows TS 102 221 §8.4/Table 8.2: `selected_df_fid()` now
resolves an EF path selection to its parent DF and returns `aid:<hex>` for
applications selected by an unknown AID (named from EF.DIR when the trace
surfaced it); `Database._decode_rows` gained an `on_message` hook and
`track_files()`
- `GET /api/sessions/<id>/files`: the operation timeline, the per-file
counters/coverage/change log, the SFI relations and the AID labels
- PWA: the side panel's Map/Stats tabs became a view selector (Map / Stats /
Files); the Files view is a timeline of Read / Update / Changed (card) rows
(colour-coded, the same file names as the Map view), a row click jumps to
the APDU (loading the pages it needs) and a card-change row's Δ opens the
changed ranges with the previous and the new bytes; EN/RU
- bounds: the image freezes past 64 KB of observed bytes per file, the change
log keeps the last 1000 events per file (counters keep counting)
- tests: tests/test_filetrack.py (counters, resets, channels, image/diff
semantics, record files, SFI resolution and the provisional merge, EF.DIR
labels, caps, the endpoint) and Node tests for the Files-view helpers and
wiring
- v2.3.0; MIN_SERVER_VERSION 2.3.0; SW cache simtrace-analyser-v63
- the slot layout now wraps: the label and "Choose file…" share the first
line, the selected file name gets its own full-width line (the fixed
200 px label left only ~200 px for the name and overflowed on narrow
screens); the ellipsis styling is unchanged and only kicks in for
genuinely long names
- RU label "Импорт трассы COMPRION" -> "Импорт трейсов COMPRION" (the
button/dialog title and the PCAP-path hint that quotes it); EN strings
and LANG_RU keys unchanged
- SW cache simtrace-analyser-v62 (label/layout-only, no version bump)
- new `simtrace_analyser/comprion.py`: parses the tab-separated ISO Protocol
Layer export (metadata block, ATR/PPS exchanges, APDUs with continuation
rows, delays in etu) and re-encodes every exchange into the wire-shaped
TPDUs the analyser stores — a case-4 exchange becomes the command, a
fabricated `61 <len>` status word and a GET RESPONSE (command's logical
channel preserved), so command/response pairing, FCP/file decoding and the
GET RESPONSE filters work unchanged; Cold reset / Power off become RST/VCC
line events, deliberately without the sigrok CLK extension (the export
carries no clock measurement)
- optional matching **ISO Application Layer** export as the exact time base:
its one `Time:` row per exchange (ms precision) replaces the delay estimate
(0-based elapsed, `started = Session Name + first Time`), guarded by a
session-identity check (`Session Name`, falling back to `Working
directory`; the two layers' export timestamps differ and are not compared)
and an exchange-count check — mismatched files are refused, never mixed
- protocol-only imports keep the Delay-based timeline: card/line activity
time (host pauses absent), the unit assuming 3.8 MHz (`CLK_HZ`); the
estimate is not fed into the CLK/RST line-event annotation
- `POST /api/sessions/import?format=comprion` (raw text body, or a JSON
`{text, times_text}` bundle) with stable error codes (app_layer /
not_comprion / unsupported_layout / no_data / times_unparsable /
session_mismatch / time_base_unverified / time_base_mismatch); a COMPRION
file dropped on the PCAP path answers with a pointer (comprion_export);
session mode `comprion`, window from the Session Name
- PWA: import dialog with two separate pickers (Protocol Layer required,
Application Layer optional) whose hint switches between "activity time"
and "exact timestamps"; errors stay in the dialog with both selections
until fixed or cancelled (Cancel/Esc, also disabled while importing);
EN/RU
- tests: tests/test_comprion.py (parser, wire encoding, time base + guards,
import endpoint) and frontend/tests/comprion.test.js (file checks, slot
validation, dialog state machine with a DOM stub, RU coverage)
- v2.2.0; MIN_SERVER_VERSION 2.2.0; SW cache simtrace-analyser-v61
- tag '18' (TS 101 220: "18 only"): one byte per item, §9.4 coding;
'00' and reserved values mean "no next action" (decoded to None)
- items[] now carry a per-item next_action; a list whose length differs
from the item count is preserved raw (spec requires X = item count)
- detail view renders "Item 0xNN: text → COMMAND"; the APDU-list summary
is unchanged — next actions are detail-only, as requested
- tests: 3 real SET UP MENU samples (local sessions 47/53/57) + a
length-mismatch tolerance case
- v2.1.2; SW cache simtrace-analyser-v57
- REFRESH '07'/'08': use the TS 31.111 §8.6 names ("Steering of Roaming" /
"... for I-WLAN", per TS 23.122 / TS 24.234) instead of "Reserved (...)"
- envelope tags 'E1'-'E3': TS 31.111 §9.1 leaves them reserved for future
usage — drop the placeholder label and decode them as unknown
- TR result '28' = Bearer Independent Protocol temporary error (TS 102 223
§8.12); '29' stays reserved
- PLI qualifier '0B' = MEID of the terminal added; '05' documented as
TS 31.111's Timing Advance (ETSI text says "Reserved for GSM")
- ISO 7816-4:2005 Table 6: add '62 86' (sensor) and '63 81' (file filled)
- drop the two fabricated '66xx' names (no such rows in the pinned specs)
- tests: add the new value tests plus a guard that no decoder table emits
a "Reserved ..." label
- v2.1.1; SW cache simtrace-analyser-v56
- terminal power supply (tag 80): voltage class per Table 6.1, maximum
current in mA, clock in 0.1 MHz steps ('FF' = not indicated)
- extended logical channels (tag 81) — zero length per §11.1.19.2.2, a
nonzero length is preserved as raw
- additional interfaces (tag 82 b1 = UICC-CLF)
- eUICC device capabilities: tag 83 per GSMA SGP.22 v2.6 §3.4.2 Table 7
(LUId/LPDd/LDSd/LUIe-SCWS); tag 84 preserved raw (no pinned SGP.32 spec)
- unknown/private TLVs preserved via raw_tlv
- concise APDU-list summary; decoded rows in the PWA detail view
- v2.1.0; SW cache simtrace-analyser-v55
- tests: 6 real samples from the local trace sessions (49/55/58/61/206/208)
- move the analysis server out of simtrace2_pysniff into simtrace_analyser;
simtrace2_pysniff stays as the simtrace2-sniff replacement capture utility
- new server CLI: simtrace-analyser-server; python -m simtrace_analyser
(simtrace2-pysniff-server and python -m simtrace2_pysniff.server dropped)
- simtrace2_pysniff lazy-imports hardware symbols, so the server no longer
pulls PyUSB at startup; the GSMTAP/PCAP codecs work without libusb
- version 2.0.0 everywhere (was 1.22.21); PWA SW cache simtrace-analyser-v54
- frontend: server name, clone URL and i18n strings updated; vendored-parser
reference otaman -> SIMple
- README/README_RUS reworked product-first (analyser first, utility second)
- DB dir ~/.simtrace-analyser, /api status payload and hosted PWA unchanged
- _build_summary: fold cmd.location_status (Normal/Limited/No service)
into the 'Location status' event entry, e.g. 'Location status: Normal
service' (the detail view already showed it)
- tests: summary assertions for the Normal/No-service vectors
- Backend-only, no version bump
- _build_summary: append 'DA <tp_da>' after the SMSC part for the
MO SHORT MESSAGE CONTROL envelope (the envelope decoder already
stores the second address TLV as cmd.tp_da; the detail view already
showed it)
- test: constructed D5 envelope with SMSC + destination address
- Backend-only, no version bump
- decode.py: _build_summary emits 'Data: N bytes — FCP of <file> (...)' for
STATUS responses with data (FCP already decoded into decoded.response);
_fcp_brief builds the 'FCP of MF (DF or ADF)' / 'FCP of EF_xxx (...)'
fragment
- index.html: cmd_description suppresses the raw-hex fallback for
body-less STATUS rows (nothing to describe beyond type + SW)
- tests: no-data STATUS summary None; DF/EF FCP summaries; pluralization
- Bump 1.22.19, SW cache v50->v51
- More precise than 'ATR STARTS' (the ATR starts after the de-assert
delay, not at the edge) while staying human-friendly; reads as a pair
with 'RESET ASSERTED'
- Update tests and both READMEs (backend-only, no version bump)
- capture.py: map GSMTAP PPS request/response (0x02/0x03) to 'pps',
matching the pcap-import mapping (was decoded as TPDU live)
- decode.py: parse the optional RST CLK extension (sigrok v1.9.0+,
payload flag bit 0); ATR gains clk_hz/data_rate/etu_us derived from
TA1 Fi/Di (data_rate = clk_hz × D / F)
- database.py: CLK state is consumed by the next ATR (no stale rate
for an ATR without preceding RST); a late-arriving RST measurement
is backfilled onto the ATR it belongs to; replay context carries
the CLK across pagination/live polling
- PWA: ATR detail shows CLK/Data rate/ETU; RST event detail shows CLK
- tests: PPS mapping, CLK parse with/without flag, rate math,
DB consume + backfill round-trips
- README/RUS updated
- Bump 1.22.18, SW cache v49->v50
- Store raw session name in STATE.currentSessionRawName
- Re-render session-name-display in toggleLang() with current translation
- Add frontend.png screenshot to README
- Bump 1.22.15, SW cache v46->v47
- Insert .splitter div between .apdu-list and .apdu-detail
- Replace flex:1 / max-height:40% with --detail-height CSS var
- Drag handler with clamp 10%-80%, persisted in localStorage
- Bump 1.22.12, SW cache v43->v44
GsmtapListener.stop() closed the UDP socket permanently, but start()
only set _running=True without recreating it. Second capture session
would silently spin on the dead socket (OSError → None → continue).
- Add _closed flag to GsmtapReceiver, set in close()
- Store bind_port in GsmtapListener for socket recreation
- start() recreates GsmtapReceiver only if previous one was closed
- Uses getattr for backward compat with test fakes
SIGROK-derived PCAPs have relative timestamps (0, 1.37, 15.02) rather
than epoch time. The _REAL_TS_MIN check failed, so close_session() set
ended to wall-clock (same as started → duration 0s). Now close_session
accepts an optional duration_secs param; the import handler passes
last_ts - first_ts so the session gets the correct capture duration.
- Toggle buttons (Tiles/List) in sessions header, preference
persisted in localStorage
- List view: table with Name, Date, Duration, Messages, Mode, Actions
columns; same fields and buttons as tiles, just tabular layout
- Refactored loadSessions() into buildGridHTML/buildListHTML helpers
with shared attachSessionListeners() for both views
- i18n: Tiles, List, Name, Date, Duration, Messages, Mode, Actions
keys added to LANG_RU
- Bump 1.22.6, SW cache v37->v38
- error_names(m) now includes 'Data length mismatch' when
decoded.length_mismatch is set, showing a ⚠ badge + tooltip
in the APDU list row (alongside existing errors like bad FCS)
- Header title: SIM-ME -> UICC
- Bump 1.22.5, SW cache v36->v37
- capture-btn moved to first child of trace-topbar (before search input)
- Button always visible: shows Start/Stop/Connect-to-server as before
- When server capture is disabled: grayed-out Start Capture with
tooltip via existing i18n key 'Capture is disabled ...'
- No more hiding button when viewing saved session or when disabled
- Bump 1.22.4, SW cache v35->v36
- HTTP request logging now off by default (log_message guarded by
RequestHandler.log_requests); --log-requests enables it
- Add session lifecycle logging: POST /api/sessions/import and
DELETE /api/sessions/<id> emit session+name+messages/bytes or
session= lines via the shared _log() helper
- Capture start/stop and periodic status keep logging as before
T=15 in the ATR is a Protocol-and-Parameter-Selection capability,
not a data protocol like T=0/T=1. Previously 'Protocols: T=0, T=15'
was misleading.
- _decode_atr: exclude T=15 from protocols, set pps=True instead
- TCK presence: only-t0 rule now also requires no PPS (T=15 implies TCK)
- PWA ATR detail: 'Protocol: T=0' + 'PPS: supported' rows
- +1 test with the real UICC ATR (3B 9F 95 80 1F C7 ...)
- Bump 1.22.2, SW cache v33->v34
_decode_nai (decode.py) only treated an all-0xFF record as empty.
A record that is a lone identity tag, or a tag with a zero-length
value (e.g. 80 00 + FF erase filler — a standard unused EF_IMPU
record) slipped through to an ascii fallback and rendered as garbage
text (the "Text: �" artifact).
- Treat a lone tag (80 / 81) or tag+zero-length-value (80 00 / 81 00)
as empty.
- Only decode the TLV *value* bytes (UTF-8); never feed the TLV header
or trailing FF filler to the text decoder. Drop the fragile
raw.rstrip(...).decode('ascii','replace') fallback.
Tests: +4 (empty 80 00+FF record, all-FF, real SIP URI, lone tag).
Backend-only; stays on 1.22.0.
Follow-ups from the capture-reliability review:
- All server-side capture log lines are now prefixed with a local
timestamp (YYYY-MM-DD HH:MM:SS) for easier correlation.
- New --log-interval SECONDS CLI flag (default 60) tunes the
"Capture alive" heartbeat interval; passed through to CaptureManager.
Stays on 1.22.0 (backend-only). 369 tests pass.
The GSMTAP capture thread died silently whenever a single non-SIM or
truncated GSMTAP datagram arrived (read_packet() raises ValueError,
uncaught in the loop, killing the daemon thread while UDP kept
flowing) — matching the "stops listening after a sniffer restart"
symptom.
Fixes:
- GsmtapListener.iter_messages() now catches ValueError/struct.error
from read_packet(), drops the offending datagram (counted via a new
`dropped` counter) and keeps listening.
- CaptureManager._capture_loop() guards the DB insert so a transient
error can't kill the thread; a top-level guard logs
"ERROR: capture thread stopped on error …" + traceback and clears
the session so /api/status no longer reports a stuck "active".
- Server now logs capture liveness to stderr: "Capture started", a
periodic "Capture alive: session=N messages=M bytes=B dropped=D"
heartbeat (default every 60 s, configurable via log_interval), and
"Capture stopped" (or "empty session discarded").
Tests: +5 (listener survives non-SIM/short packet, capture loop
survives insert error, logs stop-on-error, heartbeat+stop log).
Stays on 1.22.0 (backend-only).
- decode.py: use the GSMTAP_FLAG_BAD_FCS constant instead of a literal
0x01 in gsmtap_flag_names() (import from ..gsmtap); no behavior change.
- tests: add a DB round-trip test (insert_message with BAD_FCS flag →
get_messages → decoded.errors) covering the end-to-end path, not just
the direct decode_sniff_msg call.
sigrok-iso7816-stream sets GSMTAP_FLAG_BAD_FCS (0x01) in the GSMTAP
header `res` byte on desynced TPDUs (mis-framed exchanges kept for
forensics). Propagate that flag through the whole pipeline so such
packets are explicitly surfaced instead of decoding as garbage:
- gsmtap.py: GSMTAP_FLAG_BAD_FCS const; GsmtapReceiver.read_packet()
now returns (sub_type, data, flags) from the res byte.
- server/capture.py: GsmtapListener forwards the flags to the DB.
- pcap.py: _parse_gsmtap_hdr/_scan_gsmtap/extract_gsmtap and
parse_pcap/parse_pcapng now yield a (ts, type, payload, flags)
tuple, so the flag survives a PCAP/PCAPNG round-trip.
- server/server.py: PCAP import stores the flags.
- server/decode.py: gsmtap_flag_names() maps bit 0x01 to
'bad FCS (desynced)'; merged into decoded errors for tpdu/atr/pps
and rst/vcc, so the frontend ⚠ badge + Errors row flag the entry.
User-visible: desynced TPDUs show a ⚠ "bad FCS (desynced)" marker
in the trace and detail views. Tests: +4 (decode BAD_FCS markers,
PCAP flag round-trip); existing tests updated for the new tuple
arities. Version 1.22.0, SW cache simtrace-analyser-v32.
Support the sigrok-iso7816-stream decoder (FX2 logic-analyzer passive
SIM sniffer, github.com/anttro/sigrok_iso7816_stream) which streams
GSMTAP-SIM on UDP 4729 and adds two custom sub-types for line events.
- gsmtap.py: GSMTAP_SIM_RST_EVENT (0x10) / GSMTAP_SIM_VCC_EVENT (0x11).
- capture.py: gsmtap_msg_type() maps 0x10→'rst', 0x11→'vcc' (shared by
the listener and tests); pcap.py _SUB_TYPE_NAMES gains the same
entries so PCAP re-imports of these captures preserve the events.
- decode.py: decode_line_event() decodes [direction, level, reserved]
into reset asserted/de-asserted or power applied/removed + level;
routed through decode_sniff_msg.
- database.py: reset-assert and power-removal invalidate the per-channel
file selection (card-level transition); de-assert / power-on are
markers only.
- frontend: RST/VCC labels (RESET ASSERTED / VCC OFF, …), a detail-view
branch (Event + Level + raw), and list styling (.apdu-type.rst/.vcc).
README notes the custom sub-type compatibility. Tests: +8 (line-event
vectors, sniff routing, subtype + PCAP round-trip mappings) — 359
total. Version 1.21.0, SW cache simtrace-analyser-v31.
- Pair CLA+INS and P1+P2 into single rows (halves the header rows
per C-APDU): "· CLA, INS 00 — UICC, A4 — SELECT".
- Skip the " — desc" suffix when the parser's desc merely repeats
the hex (generic Data fields showed "6F46 — 6F46").
- Restore the implied-CLA annotation on the C-APDU title row (was
dead code behind an operator-precedence bug).
Version 1.20.2, SW cache simtrace-analyser-v30.
The secured data of an SCP80 packet is a stream of C-APDUs (e.g.
SELECT / UPDATE BINARY sequences for OTA file management). Vendor the
working C-APDU parser from the otaman project (frontend-only, ~10 KB):
PARSE_INS table, describeP1P2, describeInstallDataLv, parseOneApdu and
parseCompactApdus (concatenated streams with implied-CLA handling).
Detail view now shows an "Application message" section for secured
packets:
- unciphered packets (SPI1.b3=0): the on-wire secured data decodes
immediately;
- ciphered packets: decodes from the client-side decrypted payload
once the user supplies the key (before that, ciphertext rows only);
- each C-APDU renders as a numbered row with indented CLA/INS/P1/P2/
Lc/Data/Le + descriptions (nested LV for INSTALL), with implied-CLA
annotation; raw-hex fallback when nothing parses;
- vendored parser uppercases input (otaman's expects uppercase hex).
Tests deferred to the follow-up step (node:test infra, planned).
Version 1.20.0, SW cache simtrace-analyser-v28.
Adds optional decryption of ciphered secured packets, entirely in the
browser — the key never leaves the frontend.
- decode.py: _decode_secured_packet exposes 'cipher_block' (the
contiguous on-wire ciphered octets CNTR..end, ready for CBC) and
raw kic/kid bytes for algorithm selection. Additive only.
- index.html: pure-JS DES/3DES core (FIPS 46-3 bit-array tables,
parity ignored) + CBC; AES-CBC via native SubtleCrypto; both with
zero IV per TS 102 225. decryptSecured() picks the algorithm from
the KIc nibble (02 AES-CBC, 05 3DES-2K, 09 3DES-3K, 01 DES) with
key-length validation.
- UI: ciphered packets show a key-hex input + Decrypt strip below the
detail table; keys are memoized per key-set for the session in an
in-memory OTA_KEYS map — never sent to the server, never persisted.
- Decrypted results cached by cipher_block (survive live-polling
re-renders); plaintext rows rendered as CNTR/PCNTR/RC/Secured data
(decrypted) alongside the ciphertext; wrong-key guard via the
decrypted padding counter (PCNTR > remaining → "invalid padding").
- 3DES implementation validated byte-identical against openssl
des-ede3-cbc (3-key, 2-key ×3 random vectors, single-key).
No CC verification (out of scope). Tests: +2 (cipher_block
contiguity, AES registry guard) — 350 total. Version 1.19.0,
SW cache simtrace-analyser-v27.
User-reported: the decoded CNTR (0x1830A44E41 ≈ 104 billion) cannot
be the real counter — the OTA platform shows ~200 for that card.
Root cause: per TS 102 225 Table 2 note 1, when SPI indicates
ciphering the whole block from CNTR through the secured data
(CNTR, PCNTR, RC/CC/DS, secured data) is ENCRYPTED. Only CPI, CPL,
CHI/CHL, SPI, KIc, KID and TAR travel in the clear. Our decoder
parsed the ciphertext bytes as if they were the real CNTR/PCNTR/RC
and presented them as such — misleading (the true values are only
recoverable after 3DES decryption with the card's keys).
Fix:
- _decode_secured_packet: emit 'ciphered': True when SPI1.b3 is set
(wire hex values kept for reference); docstring documents the
ciphered block.
- _decode_response_packet: accepts ciphered (command SPI2.b5, PoR
ciphering) and flags the response likewise; database context now
carries por_ciphered from the ENVELOPE so the GET RESPONSE PoR
inherits it. Standalone SMS-SUBMIT PoRs carry no SPI — state
unknowable, defaults to False (documented).
- Frontend: CNTR/PCNTR/RC-CC-DS labels get "(encrypted)" when the
respective packet is ciphered (command, response, and ENVELOPE
response renderings).
Tests: +4 TestSecuredCiphering (ciphered flagged, unciphered plain
CNTR e.g. 200, response flag, PoR-via-context). 348 total. Version
1.18.0, SW cache simtrace-analyser-v26.
stk_test1 msg 14036 regression: READ BINARY of EF_SPDI returned
"No decoded data available" — _decode_spdi (added v1.16.0) parsed
the A3 container value as bare 3-byte records (missing the inner
'80' value TLVs) and returned a list of strings, while
_build_summary/_file_summary expects the EF_PLMNwAcT shape
(list of {mcc, mnc} dicts). The resulting TypeError aborted the
whole message decode — this was the source of the 28 "Decode
error ... string indices" messages seen across all sessions.
_decode_spdi now unwraps inner '80' TLVs (bare-records fallback
kept) and always returns {'plmns': [{mcc, mnc}...]}. Replay of all
33 sessions: decode exceptions 28 → 0 (2 remaining undecoded tpdu
are 1-byte truncated capture records, not decodable by nature).
Tests: +3 (standard inner-80 form, bare-records fallback, exact
stk_test1 regression via decode_message). 344 total. Version
1.17.1, SW cache simtrace-analyser-v25.
Next to the relative elapsed time, the detail-view header now shows
the absolute local time (YYYY-MM-DD HH:MM:SS.mmm) computed as
session start + message elapsed offset.
- STATE.sessionStarted populated from /api/capture/start (live
capture), data.session.started (session view), and PCAP imports
(sessions.started already derives from the first packet timestamp
when real).
- Graceful fallback to relative-only when the session start is
unknown; applies to all detail header variants (gap, no-decoded,
state change, Fi/Di, ATR/PPS, APDU).
Version 1.17.0, SW cache simtrace-analyser-v24.
mobileid_activate_setpin_resetpin_20201222 regression: UPDATE BINARY
(plain offset addressing) after an SMS-PP DOWNLOAD / proactive group
showed "Writes: —" in the map view, although selection tracking kept
EF_SMSS selected the whole time.
Root cause: the sel-based branch only emitted result['file'] when
_decode_file_data() produced content — files without a registered
content decoder (EF_SMSS 6F43) silently lost attribution. The first
write only displayed via the frontend's SELECT-note fallback, which
masked the bug until a proactive group split the RW run.
Fix: decouple attribution from content decoding in both the command
branch and the GET RESPONSE branch — when the selected file has no
content decoder (or the offset is non-zero, where content decode is
intentionally skipped), emit {fid, ef} so the map, list, and detail
views always name the file. Content decoders still take priority.
Session 63 now decodes all three UPDATE BINARY ops as ADF/EF_SMSS.
Tests: +4 TestFileAttribution, one expectation updated to the new
contract — 342 total. Version 1.16.1, SW cache v23.
Spec verification against UICC_SPECS.md / TS 102 223 V18.3.0 §8.6
showed the Command Qualifier for SET UP IDLE MODE TEXT is RFU
("this byte is RFU") — no icon self-explanatory bit exists in any
generation (GSM 11.14 predates the command; TS 31.111 does not
override it). The v1.11.0 _idle_text_qualifier decoded bit 1 as
"icon self-explanatory" from an unverified source.
Remove the decoder; the command now uses generic RFU handling
(hidden at '00', raw hex otherwise). Tests updated accordingly.
Version 1.15.1, SW cache v21.
- Pair members (a command ending '61 XX' and its GET RESPONSE, via the
server-side response_for link) get a subtle slate row tint
(--pair-row, themed: rgba(148,163,184,.12) dark / rgba(100,116,139,.15)
light); hover/selected still take precedence.
- Paired GET RESPONSE rows are prefixed with '↳' marking them as a
continuation of the previous command; orphaned GRs stay plain.
- GET RESPONSE label uses cat-gr class: muted color, weight 600 like
other command names.
- Map view untouched (it skips GRs); detail view already shows
"Response for".
Version 1.15.0, SW cache simtrace-analyser-v20.
Self-review of 174df8a/d0f8fb8 found two real bugs plus smaller items:
- _decode_timer_value read semi-octets high-nibble first; TS 23.040
coding (referenced by §8.38) puts the first digit in the LOW nibble
— "09:25:03" decoded as "90:25:03". Regression test added with an
asymmetric vector; the original symmetric vectors masked the bug.
- TIMER MANAGEMENT qualifier used bits b2-b3 instead of the spec's
bits 1-2 (q & 0x03): '01' deactivate was hidden, '02' printed
"deactivate timer", RFU '04' printed "get current timer value".
Decoder fixed and wrong expectations corrected.
- Stale text_note: a compliant 8D Text String arriving after the
non-standard '10' quirk TLV now clears the note.
- Proprietary type format unified to uppercase ("Proprietary (0xF2)").
- GET INKEY: Yes/No bit now suppresses the b1/b2 character-set parts,
matching §8.6 ("character sets ... are disabled").
- Frontend renders string timer_id values verbatim instead of NaN.
Tests: +4 regressions, expectations corrected — 320 total.
Phase 3 of the spec audit: TS 102 223 §8.6 qualifier coverage for every
command that defines a coding (verified against V18.3.0 text):
- Value tables: SET UP CALL ('00-'05), LAUNCH BROWSER, GET READER
STATUS, SET FRAMES, ENCAPSULATED SESSION CONTROL ('00-'04), LSI
COMMAND ('00/'01).
- Bitmask decoders (deviations from default shown; all-default hidden;
pure-RFU falls back to raw hex): SEND SHORT MESSAGE packing bit,
PLAY TONE vibrate, DISPLAY TEXT / DISPLAY MULTIMEDIA MESSAGE
priority + clear-delay, GET INKEY (alphabet/UCS2/Yes-No/immediate-
digit/help), GET INPUT (alphabet/UCS2/hidden/packed/help),
SELECT ITEM (presentation type/soft key/help), SET UP MENU,
TIMER MANAGEMENT (start/deactivate/get value), LANGUAGE NOTIFICATION,
OPEN CHANNEL (link establishment/reconnection/background/DNS with
bearer-dependent note), CLOSE CHANNEL (NAA reuse note), SEND DATA,
DECLARE SERVICE.
- RFU-byte commands (MORE TIME, POLL INTERVAL, SEND SS/USSD, ACTIVATE…)
keep raw-hex fallback.
Tests: +11 TestQualifiers cases — 317 total. Version 1.14.0, SW cache
simtrace-analyser-v17.
TS 102 223 §6.6.13 support for the SET UP IDLE MODE TEXT (0x28)
proactive command:
- Command qualifier: COMMAND_QUALIFIERS entries may now be callables
for bitmask-style qualifiers; _idle_text_qualifier decodes bit 1
as "icon self-explanatory", hides 0x00, falls back to raw hex on
RFU bits.
- Icon Identifier TLV (0x1E) parsed into icon_id (image record
number in EF_IMG, 0 = none); rendered as an "Icon" detail row in
the PWA.
- Zero-length Text String TLV now yields text='' ("remove idle mode
text") instead of being silently skipped.
Tests: +6 TestIdleModeText cases (8-bit/UCS-2 text, icon TLV,
qualifier variants, empty text) — 285 total. SW cache bumped to
simtrace-analyser-v14.
- Add sim-analyser-light.svg (#2563eb, light-theme accent); dark
keeps the existing #3b82f6 variant.
- applyTheme() now swaps the favicon href alongside data-theme and
theme-color, so the icon follows toggle click, saved theme on
load, and OS-preference fallback (all route through applyTheme).
- Precache the new asset in the service worker.