The alfa-dsa control-commands run showed the error case: an applet-level
refusal (envelope 6200 for an unknown command) carries no PoR at all, so
a por expectation on such a step can never match. The authoring guide's
status-words section now states that a 62XX/63XX envelope does not
guarantee a response packet - other warning paths still deliver one (the
low-counter case arrives via SEND SHORT MESSAGE) - and that a step
expected to be refused should leave por at its "any" default or assert
"none"; help EN/RU notes the same. docs/test-scripts.md only, no code
change; sw cache simple-v392 (help files are precached).
The script author read por.sw as the envelope SW, so a por.sw expectation
on an applet's own TAR failed with (none).
- docs/test-scripts.md: a 'The three status words (envelope SW / PoR
status / R-APDU SW)' table in the authoring section - where each one
comes from, the script field and the report row - plus a cross-reference
from the por check paragraph; help EN/RU 8.9 points at it.
- a failed por.sw check on a response_type: 'raw' decode carries the check
detail 'the response is application data - an applet's own TAR has no
R-APDU SW; assert it with data' (the PWA renders check details).
- the runner's log prints APP DATA=<hex> (no R-APDU SW) for application
responses instead of the misleading R-APDU SW=(none) data=...; the
compact/scripting lines are unchanged.
- the step editor's custom-PoR block gets a one-line hint about the same.
Tests: 821 frontend + 730 Python; sw cache simple-v391.
pySim's CompactRemoteResp was applied to every response packet, so a
third-party applet's own TAR response parsed as count=128/SW=0110/data=''
(live 2026-10-04: the alfa-dsa applet's `80 01 10`) - a fabricated status
word and a lost response.
_decode_por now takes the plaintext command script's length (cmd_len): the
compact structure's command count cannot exceed it (TS 102 226 5.1.2
Table 5.1 - each command needs at least one byte), so a larger count is
classified `response_type: 'raw'`, the secured data becomes
`decoded.last_response_data` (a por.data check matches it) and no SW is
decoded; `secured_data` is exposed in every case.
Callers pass cmd_len where they know the plaintext: the test runner's scp80
step (None for a pre-built packet; carried in ctx['last_scp80'] for the
submit-PoR path), /api/send-ota (both paths), the RAM step sender and the
counter probe; a caller without it keeps the previous parse.
Docs: test-scripts.md and api.md describe the raw fallback.
Tests: 821 frontend + 729 Python; sw cache simple-v390.
An audit of docs/test-scripts.md against the engine (testscript.py,
events.py, the runner) found a handful of doc-level gaps, now fixed:
- the scp80 param defaults (tar = the preset's ISD role TAR, spi1 = that
TAR's MSL, spi2 = 01, kvn = the first keyset);
- the counter rule as implemented: the run advances/persists the counter
after every answered scp80 step when SPI1 requests a counter check
(refused packets move the local value forward too - it only moves up);
- respond is optional (an omitted object answers ok/0x00);
- the run-log section: the exact READ BINARY/READ RECORD/UPDATE BINARY,
SCP80 WRAPPED=/SP= and 'TEST-RUN step N failed:' lines;
- Known limitations: an expanded-format 61xx has no in-packet GET RESPONSE
(TS 102 226 5.2.1.1) - send the GET RESPONSE as a follow-up scp80 step;
- sms check da: a leading + and whitespace are ignored. The engine now
drops the + (the TPDU carries the international flag in the dialling
type, so '+79...' could otherwise never match the parsed digits);
docs/api.md and the local AGENTS note it.
Tests: 821 frontend + 726 Python.
Review of 7250dbd:
- the step editor's menu-select form gains a Match case select
(Case-insensitive (default) / Case-sensitive); the step summary flags a
case-sensitive step with (case), the saved params carry
case_sensitive: true only when chosen, and the docs say the editor
authors the flag.
- docs/api.md: fix the 'with an optionally' grammar.
- the Variant-2 menu test's comment claimed a contains match while it is an
exact one; a real contains leg is added ('ыбор' -> item 128).
- timeless rationale for the case-insensitive default; the format note now
dates the change.
sw cache simple-v389 (index.html/help changed).
Tests: 821 frontend + 726 Python.
No test scripts have been authored yet, so the format is still free to
change - the menu-select text match is now case-insensitive by default
(menu labels are UI text; case_sensitive: true still forces an exact-case
match, and a miss on it names the case-insensitive candidates).
Verification against the live card's own bytes (captured 2026-10-04):
- the Annex A Variant 2 Cyrillic menu (ids 128..130) resolves by text and
the case behaviour above is pinned ('кофе' -> item 130);
- the applet's SEND SHORT MESSAGE (alpha 'Отправка...', SMS-SUBMIT to
+79332505884) pins the new alpha and sms checks and the run-log line.
docs/help/api updated; sw cache simple-v388 (help pages changed).
Tests: 819 frontend + 726 Python.
The test scripts get the pieces an applet test needed:
- scp80 check.por accepts an object {status?, sw?, data?} asserting the
decoded PoR of this very exchange (inline or via SEND SHORT MESSAGE);
the evaluation is shared with the expect-step por check
(_por_check_results). The editor's PoR select gains 'custom' with the
three fields (an empty object refuses on save).
- a new alpha content check reads the command's Alpha identifier (05/85,
Annex A coded) - the text check stays bound to the 8D Text string, so a
command carrying both keeps them distinct.
- a new sms content check asserts a SEND SHORT MESSAGE's submit TPDU:
DA / PID / DCS / UDL / UD (the parsed fields are logged and reported;
an applet's own SMS, not the OTA PoR the por check decodes).
- scp80.params.format wraps the C-APDU in the TS 102 226 5.2.1 Command
Scripting template (expanded = AA..., expanded-ae = AE 80...00 00); the
log shows WRAPPED=, the report C-APDU=<effective> format=<fmt>.
- docs: Worksheet 1 addresses an RFM TAR (B00000/B00001/B00010 compact,
B00120/B00140/B00130 expanded) with the own-TAR and MSL/spi1 notes, and
file-write is documented as a direct APDU that does not fire
EVENT_REMOTE_FILE_UPDATE.
Tests: 819 frontend + 724 Python; sw cache simple-v387.
The review of v3.18.3 found the event steps logging twice: the shared sender
writes its generic `ENVELOPE(Event Download): type=... data=...` /
`ENVELOPE SW: ...` pair, and the runner's step-annotated line repeated the
same data. `_send_event_download` gained `log=True`; the runner's envelope
and event branches pass `log=False`, so an event step logs exactly one line -
the annotated one, which already carries the step number, the event name,
the effective source and the response SW. All other callers keep their
output unchanged (netsim, the interactive event form, the BIP/timer paths).
The review's test-pinning gaps are closed:
- the shared sender's per-segment answer line
(`OTA SEND: ENVELOPE i/N -> <SW>`) is asserted in the multi-segment test;
- the scp80 step's report `sent` C-APDU/SP is asserted in the run-log test;
- the single-ENVELOPE-line behaviour (and the source override) is asserted
in the event-action test.
Tests: 816 frontend + 718 Python; sw cache simple-v386.
Verifying a script from the log was impossible: the runner wrote no card
lines, SCP80's plaintext C-APDU / secured packet / PoR landed only in the
report, and a menu selection logged nothing (the interactive path showed just
"STATUS poll (menu-select 9000)" / "STATUS -> 9000").
Every step now writes a `TEST-RUN step N:` line family to stderr, request
before response:
- generic actions: the APDU/raw event data sent and the SW/RESP received
(`APDU TX=...`, `ENVELOPE(Event Download) ... data=...`, `READ/UPDATE ... -> SW=...`);
- SCP80: `C-APDU=` (or `SP=`), `SECURED=<hex> (N B) TAR=... SPI1=... SPI2=... CNTR=...`,
the response SW, and the PoR - `PoR[inline] status=... TAR=... CNTR=... raw=...`
plus `R-APDU SW=... data=...` (with the bad-format note);
- expectations: `FETCH=<apdu> -> SW=... <raw>`, `CMD 0x.. <NAME> qual=..`,
`TR=<apdu> -> SW=...`, and `PoR[sms-submit] ...` + `R-APDU` for the SEND
SHORT MESSAGE transport;
- status: `STATUS 1/N -> <sw>` per attempt;
- menu selection: `MENU-SELECT ENVELOPE=<apdu> item=<id> ('<text>') -> SW=...`
(both the runner action and /api/menu-select; the old STATUS lines stay as
context).
The shared secured-packet sender logs each segment's answer
(`OTA SEND: ENVELOPE i/N -> <SW>`), the scp80 step's report `sent` now carries
the C-APDU/SP hex, and `--apdu-trace` remains the raw transport view.
help EN/RU and docs/test-scripts.md gained the "reading the run log" note.
Tests: 816 frontend + 718 Python; sw cache simple-v385.
The audit of the help and READMEs against the current UI found four stale
pill enumerations and five undocumented behaviours:
- The Simulator tab has had five pills since the BIP pill landed; the help
(EN/RU) and both READMEs said "four" and omitted BIP. All four now name it
(the help links §8.10).
- Test-script help: the envelope action's source override (82/83), the event
action's fields being a JSON object in the step editor, edits being written
to the store as you change them (no Save button; an incomplete draft stays
local), and the templates wording adjusted.
- Subscribed-events help: the form's "event data (hex)" preview and how to
copy it into a raw envelope test step.
- SCP80 help: the counter probe's amber outcome (a security verdict after a
counter rejection saves the value and warns).
- Cards help: the key table's "Counter" row aligns with the editor's
"Counter (hex)" heading.
sw cache simple-v384 (help files are precached).
The four findings from the review of the 3.15-3.18 batch:
- The event step's `_eventFieldsError` flag is cleared at the start of each
collect: fixing malformed JSON used to leave the save refused until the
editor was closed and reopened. Regression test covers invalid -> fixed.
- The script store keeps an entry that no longer validates on load (a
tightened engine rule or a hand edit): it is served, fails at run start with
the validator's message and can be fixed in the editor, instead of being
dropped from memory and erased by the next save. Mutation paths still
refuse it. (The presets store already had this behaviour.)
- Writebacks are serialized per script (`_testWriteChains`): two quick saves
of an unsaved draft no longer create two server-side scripts - the window
could be minutes while a RAM operation holds the card lock. Regression
test asserts one create + one update.
- Deleting a script cancels its pending debounced save and marks it deleted,
so a queued writeback never resurrects it; a create already in flight is
undone with a delete. Regression test covers the pending-save case.
- The event vectors moved to a shared fixture
(frontend/tests/event_vectors.json) asserted by BOTH suites - a one-sided
builder change now fails the other suite instead of drifting silently.
Tests: 816 frontend + 716 Python; sw cache simple-v383.
The events without a semantic builder (MT call, IMS registration, slices, ...)
are network-sourced, but the raw `envelope` action had no `src` override - the
only way to express them was the semantic set. `envelope` now accepts
`src` (82 terminal / 83 network) exactly like the `event` action; the step
editor exposes it and the authoring guide gets the row.
Also: the guide marks `por` as SCP80-only (the normaliser refuses it
elsewhere) and the semantic-events section names the shared src override.
Tests: 813 frontend + 717 Python; sw cache simple-v382.
The other-applet batch: a test script can send the common event downloads
semantically instead of raw hex.
- events.py: server-side mirrors of the PWA's event forms for Location status
(0x03), Access technology change (0x0B), Network rejection (0x12 - the
LA/RA/TA object selected by the registration group, the extended EMM cause
and the extended information object) and Data connection status change
(0x1D - status/type/TI plus the optional cause, host-clock 8.39 date-time,
location information, access technology, APN and PDP/PDN/PDU type). Fields
are validated before the run (a bad value is a script error, not a step
failure); `src` overrides the device-identities source (default terminal,
matching the Phone forms).
- The engine gains the `event` action kind; the runner builds and sends it.
The PWA's action-kind list stays in sync (drift-guarded - the guard itself
caught its own regex bug on the first run).
- Byte parity: the vectors are generated from the PWA's own builders and
pinned in tests/test_events.py; the frontend suites assert the same values.
PLMN and SCTS helpers are mirrored too.
- The Phone tab's event form shows the built "Event data (hex)" (updated as
the fields change), so every event without a builder can be used through
the raw `envelope` action.
- The test-step editor offers the event select + a JSON fields textarea;
malformed JSON refuses the save with a clear message.
- docs/test-scripts.md (the semantic-event table, an event example, the
format stamp), docs/api.md and help EN/RU updated.
Tests: 812 frontend + 717 Python; sw cache simple-v381.
The applet-testing batch agreed after v3.15/3.16:
- menu-select by text: the step names the item by `text` (+ `mode`
exact/contains, `case_sensitive`) instead of the install-dependent id
(both or neither is refused); the runner resolves it against the card's
cached menu and raises a step error naming the available items when nothing
(or several items) matches. The cache is refreshed on every SET UP MENU
fetch - the proactive chain and the expect step - so an applet install (new
items/ids after its REFRESH) no longer leaves a stale menu behind. The
step editor offers the id or the text + mode and the client-side pre-check
bounds them.
- TERMINAL RESPONSE object order: both builders emitted the Item identifier
before the Result (and the scripted builder its Text string before it too);
TS 102 223 6.8.0 mandates Command details, Device identities, Result,
Duration, Text string, Item identifier. `testscript.build_tr` and
`_menu_send_response` now follow it; `respond.raw` is documented as
appended after the standard objects, and the exact-byte tests pin the order.
- Text codings: the scripted response codes the GSM default alphabet via the
GSM 03.38 codec ('00' packed, '04' 8-bit) instead of latin-1, keeps '08'
UCS2 and sends the spec's null text string for an empty answer; the packing
helper is shared with the interactive path (`testscript.pack_gsm7`). A
UCS2 GET INPUT answer is documented and tested.
- docs/test-scripts.md gained the handoff brief for the applet agent: the
value-collection table, fill-in worksheets for the two applet scenarios, a
menu-by-text example, the validation checklist and a format stamp; help
EN/RU and docs/api.md updated.
Tests: 809 frontend + 705 Python; sw cache simple-v380.
The scripts lived in the browser's localStorage although only the local
server can run them. The new test_scripts.py store
(~/.pysim-simple-server/test_scripts.json, --test-scripts PATH) sits next to
the card presets: stable uuids and timestamps, atomic writes, and every
mutation is validated with the runner's own engine (the submitted shape is
kept - the normaliser's output is not re-normalisable: it fills defaults
like an 'any' PoR check that the check normaliser then rejects), so a stored
script cannot fail at run start for a validation reason. Import accepts the
old localStorage export as-is (export/import remains the migration path; the
PWA import also takes an exported array).
- /api/test/scripts (GET card-free: path/count + scripts), POST create,
/update, /delete and /import serve the store; the endpoints answer 503
without a store and 500 on a write failure, like the preset endpoints.
- /api/test/run takes a stored script_id (resolved when no inline script is
sent); the PWA sends the edited copy inline plus the id for the report,
and the run snapshot carries script_id.
- The PWA's Test script pill loads from the store, writes through on edits
(debounced name, immediate step/new/copy/delete), reports load/save/delete
failures inline, and exports the portable {name, steps} shape.
Tests: 808 frontend + 701 Python; sw cache simple-v379.
Three gaps blocked SCP80 applet testing from the PWA:
- testRunPreset() still marshalled the pre-v3.8 flat preset fields
(kic/kid/keys/counter/tar/spi1/spi2, all empty on a stored preset), so
every SCP80 run failed with "SCP80 preset is incomplete: no keyset is
defined". The PWA sends the matched preset's id now and the server
resolves it from the store (_test_run_preset_from_body; the inline
`preset` body stays for API callers), so a run always uses the stored
keysets, TARs and counters. Regression tests on both sides.
- testscript._normalise_params dropped the scp80 step's `kvn` (the editor
exposed it and the runner and validation consumed it, so scripts
silently used the preset's first keyset). kvn is validated (1-15) and
passed through; the client-side problem check bounds it too.
- The PoR delivered as a SEND SHORT MESSAGE (PoR-in-submit, SPI2 bit 0x20)
is decoded for the expect step that fetches it: the new `por` content
check asserts the response status / R-APDU SW / response data, decoded
with the preceding scp80 step's context (concatenated SMS accumulate
across expectations). The inline transport stays on the action
(check.por / check.data).
- The new `files` content check asserts the File List of a REFRESH
(TS 102 223 8.18, tag 12/92) - e.g. qualifier 00 = NAA initialization
with full file change notification - as an order-insensitive set of FID
paths.
The step editor exposes both checks; help EN/RU, docs/api.md and the new
authoring reference docs/test-scripts.md document the format. Tests: 803
frontend + 680 Python; sw cache simple-v378.
The ladder only advances on cntr_low; any other verdict stops the search.
A stop on a non-por_ok verdict after at least one cntr_low keeps the value
anyway - the earlier rejection proves it is above the card's counter - but
the response reports success: false with counter_saved, the verdict and the
saved counter ("something is likely wrong with the security settings");
the PWA shows it amber. Without a prior cntr_low nothing is saved, and a
missing PoR stays an error. actual_response_sms_submit/ussd count as
accepted.
Docs: docs/api.md, repo AGENTS. sw cache simple-v377.
Tests: 672 Python + 798 frontend green.
_counter_probe_params validates SPI2, forces b1 (PoR required; a caller's
00 becomes 01) and defaults to the RAM listing transport 21; the no-PoR
error names the effective SPI2, and ramSyncCounter posts
ramListingSpi2('40') for the RAM view (the failed operation's transport).
Live 2026-10-02 (NP BAP): the probe had sent the plain SCP80 form's 00
(no PoR) - the card answered the ENVELOPE 9000 and sent nothing, so the
probe stopped after one packet with "card verdict: no PoR".
Docs: docs/api.md, repo AGENTS. sw cache simple-v376.
Tests: 670 Python + 798 frontend green.
An audit of every event form against the pinned specs (31.111 7.5.x/9.3,
102 223 8.87, TS 101 220's master tag table) found three missing objects,
three malformed/wrong ones, and settled the slice-data scope.
- 0x00 MT call: the IMS URI object is cut at 100 bytes and now carries the
URI truncated object (8.135, F3 00) when the cut happened.
- 0x1F Slices: the form gains the Partial NSSAI object (8.154, F9, rel 18);
a single envelope carrying it is marked as the Last Envelope (8.79,
F0 00) automatically (7.5.27.2).
- 0x06 Card reader status and 0x07 Language: the builders now emit their
object tags (A0 01 ..., AD ...) - they emitted bare values before.
- 0x18 Incoming IMS data: the IARI tag is F6 (76/F6), not F7 (which is the
IMPU list tag - the two-column 9.3 table had been misread).
- Single-envelope mandate: the event data is capped at 244 bytes (inner 252;
the D6 long-form length and the APDU Lc stay in one byte). The server
refuses more with a 400 and the PWA shows the size error before sending.
Chained (multi-envelope) delivery, the More Data object and envelope
sequences are not implemented - documented in the help and the repo AGENTS.
- Tests: event form vectors (URI truncation, the restored tags, partial /
Last Envelope, the size helper) and the server guard; the release
reference and its PWA/code mirrors gain the three objects and the
corrected IARI key.
- sw cache simple-v375.
The keyset table's column is headed "Counter (hex)" with the placeholder
0000000001, and cardsAdd refuses a save whose counter is not 1-10 hex
digits (empty included; whitespace stripped) - the same rule the store
enforces. The store keeps its import-compatible empty -> 1 default for
API/import callers.
Docs: repo AGENTS (v3.13.4 note). sw cache simple-v374.
Tests: 667 Python + 797 frontend green.
elfTagListRetry walks the untried tag-list forms for P1=10 and P1=20
(skipping combinations the main pass already refused via the expandedTried
map; only the tag-list forms are retried - the no-tag-list form never
carries CE - and the walk stops as soon as a version appears). The form
memory can skip the tag-list forms for P1=10 when the Apps listing served
only the no-tag-list form (live 2026-10-02: EP BAP - its P1=10 tag lists
were never queried while the P1=20 retry refused them).
elfPerAidVersions filters to packages that actually have an applet
candidate before the six-query cap: the budget counts queries, not list
positions (EP BAP's first six packages are Oracle/JC-API entries with no
app instances - they consumed the cap and the A113 packages were never
queried).
Docs: help EN/RU, repo AGENTS (v3.13.3 note). sw cache simple-v373.
Tests: 667 Python + 796 frontend green.
ramDeriveElfVersions matches an applet instance to its package through a
shared ramElfAppMatch: the app's C4 (Load File AID) when the card reports
the real one, then the package's module AIDs (the applet instance AIDs),
then the shared-prefix fallback (the same tiers ramElfAppletCandidate uses
for the per-AID queries). Live 2026-10-02 (test-eSIM): the card returns
C4 = the applet's own AID (c4 10 ...3908 for package ...3900), so the old
C4-only derivation filled nothing; the module lists map ...3900 -> ...3908
(9.3), ...0200 -> ...0208/...494D4508 (1.1) and F0414C46416001 ->
F0414C4641600101 (0.1).
The ELF version hint names the remaining case: a package without an
applet instance has no version to derive either (the Oracle
A00000006203010101 stays bare - the card refuses every ELF tag-list form,
so its CE is unreachable).
Docs: help EN/RU, repo AGENTS (v3.13.2 note). sw cache simple-v372.
Tests: 667 Python + 796 frontend green.
The explorer's next-occurrence page (P2=03) is retried in the other
command formats (ramPageFormats: the expanded codings first - the serving
one first when it is expanded - and the compact command string last,
because a compact page attempt can consume the page without delivering
it) and the format that serves a page is kept for the following ones.
Live 2026-10-02 (test-eSIM card): a compact page 2 answered 61B6 and the
chained GET RESPONSE 6A86, after which the AA and AE 80 retries both saw
the occurrence already gone; the AA-wrapped page returns 182 B with the
remaining app instances' CE/CC/EA, while AE 80 is refused. A page that
no format serves ends the listing.
Only an explicit 6310 promise counts for the "(first page only)" flag
(ramExpandedMore): a probe after a 61xx page that every format refuses is
the card's final answer, so that card's ELF listing is no longer flagged
(its expanded registry has three entries; the fourth package exists only
in the compact listing - the ELF tag-list forms and per-AID queries are
refused in every format).
In Auto the explorer also queries entries the bulk listing left without
expanded fields individually by AID (ramAppsMissingDetails, <= 6 packets,
the compact command string carries the GET RESPONSE for 61xx).
Docs: help EN/RU, repo AGENTS (v3.13.1 note). sw cache simple-v371.
Tests: 667 Python + 794 frontend green.
The consumption half of the release guard: the event-download forms hide the
parameters introduced after the effective release and say so.
- catEffectiveReleaseInfo(cards, iccid): the equipped card's preset (ICCID
match, the same rule the SCP80 views use) resolved through presetRelease(),
else the default Rel-12.
- eventFieldsForRelease(cfg, rel): a field whose introduction release (`rel`)
is above the effective one is hidden; a field without a known release is
never hidden. The EVENT_FORMS annotations verified against the pinned
specs/histories: the MT call IMS URI (Rel-12, CR 12.4.0 "URI support in MT
Call event"), the CSG cell selection PLMN ID (Rel-12, 12.1.0), the IMS
registration IMPU list + status code (Rel-11), the slices S-NSSAI-mapping
and rejected objects (Rel-18).
- The form notes what was hidden: "hidden parameters: N · introduced after
Rel-12 · preset: X" (or "default release"), with a tooltip naming the
source; the send path already skips absent fields, so no builder changes.
- help EN/RU; sw cache simple-v369.
The release selector lives in the preset rather than a global chip: presets
already travel with the card (ICCID auto-selection, export/import), and the
operations that will consume the release are preset-aware.
- presets.py: the optional `release` field (4-18; empty = unspecified) joins
PRESET_FIELDS; a value outside 4-18 is refused on save/import; the store
schema version is 5.
- catrelease.py: DEFAULT_RELEASE = 12 and preset_release() - the effective
release is the preset's own value, else Rel-12 (the safe level for cards
whose release is not known; an unset release never filters anything away).
The PWA mirrors both (CAT_RELEASE_DEFAULT + presetRelease()), drift-guarded.
- Cards tab: the editor's header row gains a compact CAT release select
(unspecified / pre-Rel-6 stored as 5 / Rel-6 ... Rel-18); the empty option
names the Rel-12 default and the tooltip explains the guard; the field is
collected, cleared and restored with the other form fields.
- Nothing filters yet: the Simulator's event parameters (stage 3) will use
the effective release.
- docs/api.md + help EN/RU updated; sw cache simple-v368.
The full "Keysets (number: ...)" context repeated the column's topic; the
header now reads just the byte pair and the counter. The header text node
also carried a literal \u2014 escape (it only rendered correctly once
translatePage() rewrote it) - now a real em dash.
sw cache simple-v367.
The keyset rows were a stack of flex divs with no column labels. They are
now a fixed-layout table like the TAR table: a thead with # (keyset number,
with its tooltip), KIc, KID, KIc key, KID key, Counter and the action
column, and <tr> rows of py-1 px-2 cells with w-full inputs (the columns
drive the widths: 40/56/56 px, the two key columns split the remainder,
112 px for the 10-digit counter). Ids and behaviour are unchanged (the
number span still toggles its red flag, the inputs keep their handlers).
Guards: html.test.js checks the keyset table headers and tbody;
cards_form.test.js checks the row markup. sw cache simple-v366.
Tests: 665 Python + 788 frontend green; the whole inline PWA script
syntax-checked.
The TAR table used the default auto layout with w-full, so the browser
distributed the surplus width across all columns and TAR/MSL/description
looked evenly spread. table-fixed with explicit column widths (TAR w-28,
MSL w-20, action w-12; the description column takes the remainder) and
w-full inputs keep the first two columns tight and left-packed. The ADM
placeholder is just "ADM" now. sw cache simple-v365.
Tests: 665 Python + 787 frontend green; the whole inline PWA script
syntax-checked.
- PIN/ADM: label left, input right (one line per code) - five rows instead
of the stacked label-above-input pairs.
- TAR rows: the TAR and MSL inputs are left-aligned like their headers and
the cells use the header padding (px-2), so values and headers line up
instead of the values floating centred.
- Add TAR and Add keyset: larger and green (bg-emerald-600) and both sit
below their lists, left-aligned (Add keyset moved down from the header
row; the "OTA keysets" label stays above the list). The TAR hint is a
plain paragraph under the button now.
- Guards: html.test.js pins the green/order of both buttons and the
label-left code rows; cards_form.test.js pins the TAR row alignment.
sw cache simple-v364.
Tests: 665 Python + 787 frontend green; the whole inline PWA script
syntax-checked.
The PIN/ADM and TAR fieldsets now share one responsive row (side by side
from lg, stacked below): PIN/ADM is a fixed 224px column (lg:w-56) that
stretches to the TAR box's height, TAR takes the rest (flex-1 min-w-0).
The codes are a vertical list (label above each input) and the hint is
shortened to "All codes are optional" - the specifics stay in the input
placeholders and the help. A structural guard pins the row and the short
hint; sw cache simple-v363.
Tests: 665 Python + 785 frontend green; the whole inline PWA script
syntax-checked.
- the ADM input capped the key at 8 chars, but the server's _verify_adm
takes 4-16 hex digits (padded to 8 CHV bytes) and the old input had no
limit; maxlength=16 now, with a static guard in html.test.js.
- cardsTarAdd/cardsTarRemove re-rendered the table from the save-path
reader, so an emptied role field was silently refilled with its default.
The renderer no longer invents defaults; a raw reader (cardsTarsFromTable)
feeds Add/Remove and the language-switch refresh, while the save path
(cardsTarsFromForm) keeps the default-filling for the mandatory roles.
- the editor's dynamic TAR/keyset rows now rebuild on a language switch
(refreshDynamicI18n) from what is typed - their placeholders and titles
carry t() strings. (The title/Save label were already refreshed there.)
- dropped the now-unused cardsRoleMsl helper and its test references.
- docs: the ADM is documented as 4-16 hex digits (help EN/RU, api.md,
AGENTS). sw cache simple-v362.
Tests: 665 Python + 784 frontend green; the whole inline PWA script
syntax-checked.
The Cards tab now shows the preset list by default; "Add preset" / "Edit"
open a separate editor view (Save stores and returns to the list, Cancel
discards; ramGoToPreset opens it too). The editor keeps the name/ICCID
header (with From card) and groups its fields into bordered blocks:
- PIN/ADM: the new optional card codes PIN1/PUK1/PIN2/PUK2 (plain text,
4-8 decimal digits) plus ADM, moved out of the top row; nothing consumes
the PIN/PUK codes yet.
- TAR: one uniform table (TAR, MSL, description); the first three rows are
the fixed role entries with the immutable English descriptions "ISD
compact format" / "UICC shared FS RFM compact format" / "ADF FS RFM
compact format"; further rows are deletable with an optional description.
- SCP80 (keyset editor) and SCP81 (PSK pair) as before.
Store: PRESET_FIELDS gains pin1/puk1/pin2/puk2 (optional; whitespace
stripped; 4-8 decimal digits validated on save and import), SCHEMA_VERSION
4, docs/help/AGENTS updated. TAR rows use unified ids (cards-tr-N-*), the
editor switches via cardsShowList/cardsShowEditor.
Tests: 665 Python + 783 frontend green; the whole inline PWA script
syntax-checked. sw cache simple-v361.
The table behind the release-aware command/parameter filter: which proactive
command, event, data object and PLI qualifier appeared in which release,
seeded from the curated reference in the workspace
docs/uicc/ETSI-3GPP-Releases.md (3GPP TS 31.111 Annex W, ETSI TS 102 223
Annex V, ETSI TS 101 220 Annex Q; the version major is the release).
- pysim_simple_server/catrelease.py: CAT_RELEASE (events/commands/objects/pli),
release_label() - everything below Rel-6 collapses to "pre-Rel-6" - and
item_release(); an item absent from the table is one the histories do not
pin down and is never filtered.
- The PWA mirrors it as CAT_RELEASE + catReleaseLabel() next to the other
release table (ETSI_TOOLKIT_RELEASE); no behaviour change yet - stage 2
(the card-release selector and REL-x badges) and stage 3 (the filtering of
the event list, PLI and push builders) consume it.
- Drift guards: tests/test_catrelease.py (module <-> PWA mirror, event-list
coverage through 0x1F minus the Void 0x1A, helper vectors) and
frontend/tests/cat_release.test.js (the PWA side).
- sw cache simple-v360.
Stage 1 of the release guard: which proactive command, event, data object and
PLI qualifier appeared in which release.
Sources: the change histories of the three pinned specs - 3GPP TS 31.111
Annex W, ETSI TS 102 223 Annex V and ETSI TS 101 220 Annex Q. The version
major equals the release (V7.x = Rel-7 ... V18.x = Rel-18); ETSI aligned its
numbering with 3GPP, but the documents differ, so every row carries its source
document.
Content: curated tables for the 32 events, the proactive commands, the data
objects/TLVs the tool builds, the PLI qualifiers and selected TERMINAL PROFILE
features (release + source/CR per row; 'baseline' = the original Rel-4 spec,
'?' = not pinned down by the histories), the per-release feature entries of the
three histories, and a machine-readable JSON summary as the seed for the
code-side table (catrelease.py + the PWA mirror).
The data keeps the real release (Rel-4/5/6 ...); the UI will collapse
everything before Rel-6 to "pre-Rel-6".
- /api/ram-install-app referenced undefined spi1/tar (the resolution block
was only added to /api/ram-install and /api/cap-compat), so every request
answered 500. The same preset/MSL resolution is now in place, with
regression tests for both modes (install / make selectable) and for the
no-MSL refusal before any card I/O.
- esc() now escapes " and ' too: the TAR editor renders free-text
descriptions (and other values) into attribute contexts, so a crafted
preset import could inject attributes. Three tests asserting the old
unescaped output updated; a dedicated esc test added.
- _spi1_for_tar names the no-TAR case in its hint (a pre-built packet
without tar/spi1 used to report "TAR ?").
- preset free TAR rows no longer store an empty "role" key (the documented
shape); a non-list `tars` in an update is refused (400) instead of being
silently ignored.
Live: the no-MSL request answers 400 with the hint; a make_selectable with
a bogus AID runs the format probe and the INSTALL (derived SPI1 16 from the
preset MSL), the card answers remote SW 6A88 and the counter advances - no
500. Suites: 658 Python + 775 frontend.
No version bump: completes the unreleased v3.10.0 (6360dc5 was never
pushed). sw cache simple-v359.
Presets drop the card-wide SPI1/SPI2 and the fixed tar/uiccTar/usimTar
fields: a preset now carries `tars` - the three mandatory role entries
(isd/uiccRfm/usimRfm, spec defaults pre-settled) plus free TARs with an
optional description, each with its MSL (Minimum SPI1, TS 102 226
8.2.1.3.2.4). v3.9.1 presets convert on load/import (the spi1 seeds all
three MSLs); duplicate TARs and empty TAR/MSL values are refused; the
store schema version is 3.
Operations derive a packet's SPI1 from its TAR's MSL (_spi1_for_tar): the
RAM ops, the test runner (per-step overrides still win), the counter probe
and each TAR-probe row; the plain send keeps an explicit SPI1 with a
`warning` when it is below the MSL, and a TAR outside the preset without an
explicit SPI1 is refused with a hint. The TAR probe resolves per checked
TAR before sending and reports the effective spi1 per row. The secured
packet form's SPI2 default is 00 (the RAM/probe operations pin their own).
PWA: the Cards form edits the TAR table (role rows with MSL inputs plus
Add/Remove free rows with a description), the list shows a TARs (MSL)
column, the SCP80 TAR field suggests the preset's entries (datalist) and a
preset apply seeds the SPI1 from the operation TAR's MSL with an amber
warning below it; the preset-wide SPI1/SPI2 fields and the header marker's
SPI requirement are gone (completeness = a keyset + the three role entries
with their MSLs). Help EN/RU, docs/api.md and AGENTS updated; sw cache
simple-v358.
Live: TAR probe 000000 at its MSL 16 (registered, por_ok, counter
advanced); a send without spi1 derived 16 (por_ok, counter advanced); a
send with spi1 0A returned the warning and the card refused it (6200, no
counter consumed); an unknown TAR without spi1 was refused with the hint.
- The probe no longer takes a caller-supplied `apdu`: it always sends the
read-only SELECT MF (the counter advances on the packet's security
acceptance whatever the applet answers - 6A86/6D00 are normal there), so
no caller can turn the sync probe into a write.
- Version 3.9.1: server VERSION, pyproject.toml and the PWA header; sw cache
simple-v357; docs/api.md and the repo AGENTS.md updated, and the workspace
AGENTS.md gains the v3.9.1 history entry (its "Currently" line was stale at
v3.7.1).
- The RAM step line (and the plain send's PoR line) now name a bad-format
failure ("bad format 02 (wrong length)") instead of reading "PoR ok": the
step record carries por_bad_format_name and ramStepLine renders it.
- ramSyncCounter follows the view the notice came from ('ram'/'sp' - preset
and keyset selectors alike) and refreshes the keyset labels after a sync.
- The ETSI-release line no longer hides behind the memory block (it needs
only the ELF list).
- /api/send-ota reports final_cntr only for a packet the card accepted: the
failed-envelope path used to persist the packet's own counter on a
rejection (the audit trail's small bump on cntr_low).
- _ram_detect_format's docstring corrected (both expanded codings execute on
the live ISD; the earlier bad format was a length-mismatched template).
- Tests: ramStepLine bad format, the view-aware notice, the accepted/rejected
final_cntr cases, and an end-to-end /api/counter-probe endpoint test
(400s, no write on a rejected walk, accepted+1 stored).
- sw cache simple-v356.
The expanded registry returns the package version as two bytes of hex (010C)
and the ELF rows format it with ramFmtVersion (010C -> 1.12) - the release
lookup used the raw value, so the live card showed
"ETSI release: unknown (uicc.toolkit 010C)". cardEtsiRelease formats it the
same way (a non-4-hex value passes through unchanged) and the tests now use
the raw vectors the card actually returns.
- sw cache simple-v355.
The explorer's card-resources block now names the card's ETSI release, using
the same mapping as the CAP analysis (capmem.ETSI_TOOLKIT_RELEASE: uicc.toolkit
version -> REL-N, basis in brackets; the live card's 1.12 -> REL-15/16/17).
The table is mirrored in the PWA (drift-guarded by tests/test_cap_memory.py,
like JC_FRAMEWORK_SDK); the version comes from the expanded registry's CE tag
and degrades to the version hint when the listing has none; SIM-only cards (no
uicc.toolkit) get no label, the same rule as SIM-only CAPs.
- cardEtsiRelease(elfs) (pure) + UICC_TOOLKIT_AID / ETSI_TOOLKIT_RELEASE in the
PWA; rendered after the free-memory lines with a tooltip naming the basis.
- Tests: helper vectors (1.12/1.11/1.5, unknown version, versionless,
SIM-only), the rendering guard, and the table/AID drift guard.
- Help EN/RU; sw cache simple-v354.
The TCA loader's Explore traces settle the format question: it sends the same
P2=00 listing queries wrapped and unchained (`AA0A2208 80F24000024F0000`,
pages `...01024F0000`) - no GET RESPONSE anywhere - while the live card needs
the chain and answers 6700 to the wrapped unchained form; and the live ISD
refuses the compact P2=02 registry query (6700) while serving it wrapped
(`AA11220F 80F2400209 4F005C054F9F70C5CC00` -> 6310 + entries with CE
versions). Both the listing and the registry queries now follow the
operation's command format and fall back across formats.
- ramFormatIsExpanded(fmt) + ramFmtLadder(runFmt, working) (pure): the run's
format first, then expanded, expanded-ae and compact as the last resort; a
format that served data is tried first and never repeated.
- ramGetStatusApdu(p1, p2, fmt): the chain exists in compact only (TS 102 226
5.2.1.1) - an expanded run sends the bare query.
- paginate: the P2=00 listing follows the run's format and pins it per send,
so a GET RESPONSE can never ride inside an expanded packet.
- expandedPass: each registry form goes through the format ladder; the chained
61xx retry is compact-only (it was guarded for 'expanded' but not
'expanded-ae'); the serving format is remembered for the remaining listings
and named in the report ([AA]/[AE 80]); ramExpandedGroups carries it.
- Tests: the helper + ladder vectors, the unchained expanded listing builder
(both codings), the fallback/chain source guards, the report marker.
- Help EN/RU, the repo AGENTS.md; sw cache simple-v353.
Live findings that drove this: the "card counter 0000000000" the low-counter
notice showed is not the card's counter at all (the PoR's CNTR is a copy of
the command's counter, TS 102 225 5.2 table 3, and a rejection leaves it
zeroed); and an earlier "no card accepts the expanded format" observation was
a hand-made template whose declared length did not match its body - the card
validates the lengths and answers with a Bad format TLV.
- _cntr_low_fields(status) reports the verdict as a flag; card_cntr/
suggested_cntr and the UI's "(card counter ...)" text are gone - the value
is not observable. The notice offers a bounded Sync counter probe instead.
- POST /api/counter-probe: a doubling counter ladder (start+1, +2, +4, ...)
with a read-only command until the card accepts one, then accepted+1 is
persisted (monotonic, source counter-probe). Rejected attempts change
nothing on the card; the search stops at a ceiling (default FFFFFFFF, far
below the 40-bit maximum where the counter blocks) and after max_attempts
(default 40, clamped 1-64); a non-cntr_low failure stops it at once.
- _parse_response_scripting decodes the Bad format TLV (90, TS 101 220 table
7.20; error types per TS 102 226 table 5.12) and returns a dict; the PoR
carries bad_format/bad_format_name and _ram_step_result fails the step with
"bad format 02 (wrong length)" instead of falling through to the compact
decoder (which read the AB tag as 171 commands).
- _wrap_expanded_apdu(apdu, form) emits the definite (AA) or the indefinite
(AE 80 ... 00 00) scripting template; the RAM command formats are compact/
expanded/expanded-ae and _ram_detect_format probes compact, then both
expanded codings (the expanded probes use Le='00' and no chained GET
RESPONSE, TS 102 226 5.2.1.1). The RAM form gains a Format selector.
- Live-verified: the verdict carries no value; the probe walked
0x1000001/0x1000003 (low) then 0x1000007 (accepted, stored 0x1000008); the
ISD executes both expanded codings (R-APDU 08A0000000030000000F80, SW 9000)
and a length-mismatched template answers bad_format 02 - decoded now.
- Tests: the ladder, the probe walk + persistence, params validation, both
template forms, the Bad-format TLV (definite and plain tag styles), the
live ISD response through _decode_por, the step-result failure, and the PWA
guards; help EN/RU, docs/api.md, the repo AGENTS.md; sw cache simple-v352.
Live finding (Network rejection events): the card asks the terminal for the
current date and time (PROVIDE LOCAL INFORMATION, qualifier 03) and our TR
answered "performed successfully" with no data. TS 102 223 6.4.15 says the
terminal shall return the current date and time, and the 6.8.0 TR structure
requires the Date-Time and Time zone object (8.39) for that qualifier.
- _pli_data_hex(qual): the TR Config dictionary entry wins; when it is empty
the date/time qualifier is answered from the host clock via _encode_scts
(the SMS timestamp builder, TP-SCTS coding).
- _build_tr and _DefaultProactiveHandler put the command-specific data objects
after the Result TLV per the 6.8.0 order (the POLL INTERVAL Duration echo
too; real terminal responses pin the order: 8103010300 02028281 030100
0402011E).
- _decode_tr decodes qualifier 03 with the same 8.39 coding (swapped
semi-octet BCD digits, time-zone sign bit 0x08, 'FF' = unknown); it read
plain decimal bytes and a 0x80 sign before.
- The PWA PLI dictionary codec for the date/time entry writes the same coding
(it wrote plain hex bytes and a 0x80 sign), and the TR Config panel notes
the empty-entry default. sw cache simple-v351.
Tests: builder vectors (east/west/unknown zone) + dictionary override + the
empty non-date/time qualifier + the object order (PLI and POLL INTERVAL) + the
handler path + the TR decode + a frontend codec round-trip test.
The six forms that still missed conditional/optional objects now carry them:
- Location status (0x03): Location Information (MCC/MNC + LAC + Cell ID) -
included only for Normal service, as TS 102 223 8.19 requires.
- Channel status (0x0A): Bearer Description (B5, type + parameters) and
Other address (BE, IPv4 dotted / IPv6 hex / the empty "dynamic address"
form); new eventIpBytes() helper.
- Call disconnected (0x02): IMS call disconnection cause (D5, SIP/Q.850 +
2-byte cause).
- Network rejection (0x12): Extended information (F2, CAG ID / NID / RedCap).
- CAG cell selection (0x1E): CAG Human-readable network name list (D7,
80-tagged names).
- Slices status change (0x1F): Allowed slices with S-NSSAI mapping (F7),
Allowed slices (F8), Rejected slices with mapping (D7) and Rejected slices
(B1) beside the served list.
Every implemented event form now carries all the objects of its spec table.
Tests: the new build vectors. Help EN/RU and both AGENTS files; sw
simple-v350.
Group 1 (TS 102 223): Data available (Channel status B8 + channel data length
B7), Display parameters changed (rows/columns + sizing/fonts/effects, C0),
Network search mode change (manual/automatic, E5), Browsing status (browser
error code, E4) and Frames information change (default frame + per-frame info,
E7).
Group 2 (TS 31.111): IMS registration (IMPU list as 80-tagged URIs + IMS status
code), Incoming IMS data (IARI), CAG cell selection (status + mechanism, access
technology, CAG information list) and Slices status change (allowed / rejected
/ served / partially, access technology, served S-NSSAIs) - all with source
network.
Still unimplemented by design: 0x0D Local connection, 0x16 Contactless state
request (3GPP: not required) and the eCAT containers 0x19/0x1B; 0x04/0x05/0x13/
0x1A are data-free, where the one-click send is spec-correct.
Tests: the new build vectors. Help EN/RU, both AGENTS files; sw simple-v349.
MT call, Call connected, Call disconnected, Browser termination, (I-)WLAN
access status and CSG cell selection no longer fall back to the generic
"send a bare notification" prompt:
- MT call (TS 31.111 7.5.1A.2): transaction identifier, calling party number
+ TON -> Address (EFADN BCD coding, encBcdDigits/eventAddressTlv),
subaddress, IMS URI, media type; source network.
- Call connected (7.5.2A.2) / Call disconnected (7.5.3A.2): transaction
identifier, near/far-end source select, media type, and a cause - absent,
the zero-length radio-link-timeout form or custom bytes (the hex field shows
only for "custom").
- Browser termination (TS 102 223 7.5.9.2): user / error cause.
- (I-)WLAN access status (TS 31.111 7.5.1.2): no coverage / coverage /
connection on-going.
- CSG cell selection (TS 31.111 7.5.3.2): status + selection mechanism,
optional access technology, CSG ID, HNB name and PLMN ID; source network.
The device-identities source (TS 102 223 8.7: 82 terminal / 83 network) is now
selectable: EVENT_FORMS carries a fixed `src` or a `srcField`, and
_send_event_download()/`/api/event-send` take an `event_src` override (an
invalid value falls back to the terminal).
Tests: the BCD/Address builders, each event's build vectors, the "no not_yet
left" guard and the server source override. Help EN/RU, both AGENTS files;
sw cache simple-v348.
- The 0x1D event form now offers the Date-Time and Time zone object
(TS 102 223 8.39): an optional select that includes the host clock's
date-time, built by eventDateTimeTlv() as seven semi-octet BCD bytes in the
TS 23.040 9.2.3.11 order (units high, tens low) with the time zone in
quarters of an hour and its sign in bit 3 of the first semi-octet.
- The Cards list's empty-state colspan is 8 now (the table has had 8 columns
since the keyset refactor, not 10).
- The expanded-registry "(first page only)" note marks only an unfulfilled
6310: ramExpandedMore() keeps the promise set when the follow-up page was
refused/failed or the page cap stopped the paging, and clears it when an
accepted page ended the listing (a completed multi-page listing was
annotated wrongly before).
Tests: eventDateTimeTlv (fixed dates, both timezone signs), the 0x1D build
with and without the date-time, and ramExpandedMore's cases. Help EN/RU and
both AGENTS files; sw cache simple-v347.
Gating: the SCP80 "Send to Card", the RAM Execute and the TAR-probe Execute
buttons carry `data-needs-keyset`; pysimApplyAvailability() disables them
(with the hint "Select a card preset and its keyset" when the card itself is
fine) unless the view's preset is selected and the form's keyset number is one
of its keysets - `spKeysetStartReady`, the same rule the click-time guards
enforce, which stay as the last line of defence. spKeysetSync() refreshes the
gate on every preset/keyset change, and the KIc/KID index selects now call
spKeysetSync() so the keyset selector follows a manual index change too.
Version 3.9.0: server VERSION, pyproject.toml and the PWA header; sw cache
simple-v346; the workspace AGENTS gains its v3.9.0 history entry.
Tests: indicator.test.js drives the gate (no preset -> disabled with the hint,
an undefined keyset -> disabled, a matching keyset -> enabled) and guards the
three buttons' attribute; the cards/keysets/ram sandboxes stub
pysimApplyAvailability. Help EN/RU notes the disabled start buttons.
The ✕ in the Cards keyset editor was a neutral bordered button with only a
red hover; it now matches the preset list's Remove (bg-red-600 text-white,
hover bg-red-700), keeping the glyph and the "Remove keyset" tooltip.
keysets.test.js guards the red style. sw cache simple-v345.