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.
The Cards list now marks the row whose ICCID matches the equipped card
(cardsFindByIccid handles digits / spaced / raw EF hex alike): an emerald row
tint plus a small dot with a "The equipped card's preset" tooltip in the Name
cell. There is no highlight while the ICCID is unknown (no card, unreadable
EF.ICCID) or when no preset matches - the amber banner already covers that.
pysimCardStateUpdate() re-renders the list when the ICCID actually changes, so
a visible Cards tab follows equip/removal live (opening the tab re-renders
anyway through switchTab).
Tests: cards_iccid.test.js drives cardsRender with a fake tbody - the matching
preset is highlighted (both ICCID forms), the others stay plain and no ICCID
means no highlight; card_state.test.js stubs cardsRender. Help EN/RU and the
repo AGENTS updated; sw cache simple-v344.
A keyset only exists inside a card preset, but the SCP80/RAM/TAR keyset
selectors stayed enabled with no preset selected - showing either
"— no keyset —" or a stale "Keyset N — not defined in the preset" entry left
over from an earlier preset. spKeysetSync() now disables the selector and
reduces it to a "— select a card preset —" placeholder whenever its view has
no preset selected, and re-enables/populates it as soon as one is.
Clearing the preset dropdown ("— Select card —") also refreshed nothing:
cardsApply() returned before the sync, so the stale keyset list stayed; it now
syncs the selectors (and the preset warning) on the empty selection too.
The three selects get disabled:opacity-40 disabled:cursor-not-allowed (the
rules are already in the compiled style.css). Tests: keysets.test.js covers
the disabled/cleared state and the re-enable plus a cardsApply source guard;
cards_form.test.js and cards_counter.test.js gain tarPresetIdx in their
sandboxes. Help EN/RU and both AGENTS files; sw cache simple-v343.
EVENT_FORMS[0x1D] was missing, so its Send button posted event_data: null -
the ENVELOPE carried only the event list and device identities, none of the
event's data objects. The form now builds the TS 31.111 7.5.25.2 object set
(tags per TS 101 220 / TS 31.111 9.3): data connection status (9D), connection
type (AA), optional (E/5G)SM cause (AE), transaction identifier (1C), optional
location information (13, CR flag 0 as the spec requires for this command),
optional access technology (BF), location status (9B), optional APN (C7) and
PDP/PDN/PDU type (0B).
Adjacent spec fixes found while verifying:
- the Access Technology Change options (0x0B) did not match the pinned
TS 102 223 V18.3.0 8.61 coding (GSM Compact / GSM w-EGPRS / UTRAN w-HSDPA
...); they now use the shared ACCESSTECH list, whose two labels were also
corrected (TIA/EIA-553-A, TIA/EIA-136-270);
- _send_event_download() wrote the D6 length as a single byte - a long APN
would overflow it; 128..255 bytes now use the BER long form (81 <len>).
Tests: the 0x1D build (defaults, full form, fallbacks), the 8.61 option list,
the long-form D6. Help EN/RU and both AGENTS files; sw cache simple-v342.
The Simulator view's "STK menu" block already carries the legend, so the
emerald button's hardcoded "STK: " prefix was redundant. It now shows just
the card's menu title (e.g. "Alfa Mobile"), or "Menu"/"Меню" for a
title-less SET UP MENU (`stkMenuLabel`, unchanged).
The help pages (EN/RU) describe the button as the menu title, both AGENTS
files drop the old `STK: Menu` wording, and stk_menu.test.js gains a source
guard against the prefix returning. sw cache simple-v341.
Tests: 743 Node / 619 Python.
Documentation:
- help (§1/§3): the SCP80 tab is switched by three pills - the tab list and the
section intro now name the TAR probe (RU "TAR-сканер"); §3.1/§3.2 describe the
Keyset selector next to the preset selector (and that a successful PoR
advances the counter only when SPI1 carries a counter check); §4 describes
the keyset editor rows, the derived number and the per-keyset counters; the
probe section keeps its counter rule.
- README/README_RUS: the SCP80 tab says three pills and gains a "TAR probe" /
"TAR-сканер" section (standard list, verdicts, counter persistence); the
Cards section is rewritten for the server-side store - keysets, server-owned
per-keyset counters with the audit trail, JSON export/import as the migration
path, the keyset editor, the equipped-card mismatch banner - and the field
table, "Add a card" and "Edit / remove" follow.
Terminology:
- "key version" is no longer used for SCP80 keysets in the UI, the server error
strings and the docs: the selector is "Keyset", its options "Keyset 1 - ...",
the messages say "keyset N ...". The TS 102 225 A.1/A.2 quotes and the GP
PUT KEY P1 field keep the spec's own wording.
- RU keeps the word "keyset" untranslated ('Добавить keyset', 'Номер keyset',
"не является keyset пресета", ...) and uses "пресет" instead of
"предустановка" throughout the dictionary, the help and README_RUS.
sw cache simple-v340. Tests: 742 Node / 619 Python.
Two selector bugs in the new TAR-probe view and one stale-list bug in the RAM
view, all from the lazy population of the view-specific preset selects:
- cardsAutoSelectByIccid() set sp-card-sel and ram-card-sel only, so the TAR
probe view never got the equipped card's preset (and tarExecute() then ran
with preset_id: '', i.e. without persisting the counters it consumed);
- tarRender()'s keep-fallback was -1 instead of the remembered _ramCardIdx, so
the select stayed on "— Select card —" even when the auto-selection had
remembered the preset; the TAR pill switch now also calls tarRender();
- the keyset selectors showed a stale "N — not defined in the preset":
spKeysetSync() runs during the auto-selection, when ram/tar-card-sel have no
options yet, so those lists were built with no preset; ramRender() now
rebuilds them (spKeysetSync) once its preset is known - tarRender() already
did.
Tests: the auto-select covers tar-card-sel; a DOM-driven regression test
(ramRender with a remembered preset and a two-keyset preset must show
value="2" selected and no "not defined" entry); source guards for ramRender's
sync, tarRender's keep and the TAR pill switch. sw cache simple-v339.
The probe is not a RAM operation, so it moves out of the RAM view into a third
SCP80 pill (Secured packet | RAM | TAR probe, RU "TAR-сканер"), with its own
preset and key-version selectors, its own equipped-card mismatch banner and its
own result line; the help gains a "3.3 TAR probe" section and the anchor map
follows the pill.
The TAR field is a checklist now: pre-populated with the standard allocations
of TS 101 220 V18.3.0 Annex D, Table D.1 (each row carrying the document it
comes from, all checked by default, Select all / Select none), plus custom
TARs added through a small form (three hex bytes + an optional description,
removed with a x). Custom entries live in localStorage['simple_tar_probe'] on
a best-effort basis - losing them is harmless, the standard list is always
there. Only the checked TARs are sent; nothing checked is refused with a
message. A Python test guards the PWA's list against the server's
TAR_PROBE_TARS (the endpoint keeps its own default for API callers).
RAM leftovers removed (the tar-probe operation entry, the #ram-tar-rows block,
the ramExecute dispatch, ramTarProbe); getRamSpParams() takes the view's
selector id; tarRender/tarApplyCard/tarKeysetChanged/tarKeysetGuard mirror the
RAM view; spKeysetSync and spPresetWarningRender cover the new selectors.
sw cache simple-v338. Tests: 740 Node (the checklist rows, the custom
round-trip, the checked-list read, the renderer, the pill wiring) and 619
Python (the list drift guard).
Three things learned from probing a live card, plus the capability they enabled:
1. Counter tracking follows SPI1.b5b4 (TS 102 225 5.1.1): with 00 the counter
is "present, ignored, never updated", so a packet without a counter check
must leave every counter untouched. _counter_tracked() now guards
/api/send-ota (no final_cntr, no persistence, a log line), the RAM step
sender and the test runner; _preset_counter_persist() requires a non-zero
key version (the "single keyset" fallback could advance a keyset spuriously
for a keyless send). The PWA advances the form counter only when the server
reports final_cntr and says "counter not tracked" otherwise.
2. The spec's no-security form builds again: _ota_keyset() refused the valid
KIc/KID '00' (TS 102 225 A.2: '00' is legal with SPI1.b3=0 for KIc and
SPI1.b2b1=00 for KID) because pySim wants an algorithm. An unknown nibble
is now refused only when the algorithm is actually used (ciphering / a
ciphered PoR for KIc, RC/CC/DS / a MAC'd PoR for KID) and 'implicit' codes
the nibble as 0 otherwise - POST /api/send-ota {apdu, spi1:"00", kic:"00",
kid:"00"} now builds 0D00010000B0000000000000010000A40000, the exact packet
the card accepted (a fake 15/15 was refused with 6200). A no-security PoR
is decodable again too, and a requested-but-missing PoR sets por_missing
on the plain path as well.
3. TAR probe (POST /api/tar-probe, RAM op "Probe TARs"): one secured packet per
TAR - the standard allocations of TS 101 220 V18.3.0 Annex D, Table D.1
(TAR_PROBE_TARS), or a custom list - carrying a harmless C-APDU (SELECT MF)
with the preset's keyset and SPI 16/01. Each TAR is classified
(registered / no_answer / no_por / refused) from the ENVELOPE verdict and
the application's PoR, counters advance and persist normally, and the run is
refused when SPI1 has no counter check. Live on a card: 000000, B00000 and
B00001 registered (compact-format applications, answering 6D00 / 6B00),
the expanded-format TARs, USAT, multiplexing and CASD refused with 6200 -
with the keyless form the same card accepted every TAR and answered no PoR.
Docs (api.md, help EN/RU, AGENTS), sw cache simple-v337. Tests: 618 Python
(no-security build, the algorithm-only-when-used rule, the tracking rule, the
persist guard, the probe classification) and 736 Node (the probe renderer and
wiring, spCounterTracked).
Autoselection normally keeps the SCP80/RAM preset in step with the card, so
the mismatch is easy to miss. Both views now carry an amber banner above the
preset selector (the existing warning style - no new Tailwind classes):
- the equipped card matches no preset: "The equipped card <ICCID> does not
match any card preset - the selected preset may belong to another card.
Add it in the Cards tab (From card)"; with no presets at all the banner says
so instead;
- a preset is selected but it is not the equipped card's: a softer line naming
the matching preset ("check that its keys and counters belong to this card").
`spPresetWarning(iccid, presets, selectedIdx)` is pure (unit-tested with the
ICCID forms the store accepts: digits, spaced, raw EF hex), `spPresetWarningRender`
fills both banners and runs on every card-state poll, preset load, preset
selection and page init. No card session or an unreadable ICCID shows nothing
(the card-iccid gating already explains that state); the banner warns only -
it never blocks a send (preparing a packet for another card stays possible).
Tests: frontend/tests/preset_warning.test.js (5), the card-state/preset
harnesses stub the DOM-bound renderer. sw cache simple-v336.
A card usually holds several OTA key sets, and the SCP80 counter is tracked
per key version on the card, so a preset now carries one keyset per
GlobalPlatform key version - the b8..b5 nibble of KIc/KID (TS 102 225
5.1.2/A.2), the version is implicit in the bytes. The spec is explicit:
"a dedicated counter shall be associated to each key version" (Annex A.1) and
"the key version number indicated in the KIc and KID (bits b8..b5) fields
shall be identical when different from 0" - a mismatch is rejected by the card
with "Unidentified security error" (A.2).
Server (presets.py, schema 2):
- a preset carries `keysets: [{kic, kid, kicKey, kidKey, cntr}]`; validation
refuses a KIc/KID version mismatch (A.2), a version outside 01-0F ('00'
means "no security" and is a packet-level choice, not a keyset), duplicate
versions and missing keys/counters; a v3.8.0 flat preset converts into one
keyset on load and on import;
- `set_counter(..., kvn)` writes the counter of that keyset (monotonic per
keyset), the audit line carries the kvn, and `find_keyset()` serves the
lookups;
- the operations derive the key version from the request's KIc byte and
persist into that keyset: /api/send-ota, the RAM install/install-app/compat
steps (_ram_send_gp_apdu) and the test runner's SCP80 steps (which may name
a `kvn` per step and keep one working counter per keyset);
- a non-zero key version the named preset does not define, or a KIc/KID
mismatch, is refused before anything is built (_preset_keyset_check /
_keyset_guard: 400, or a step error in the test runner). Without a
preset_id only the mismatch rule applies.
PWA:
- the Cards form's KIc/KID/counter/keys inputs become a keyset editor (one row
per version: KIc, KID, both keys, that version's counter, Add/Remove; the
version is shown derived from the bytes and a mismatch is flagged);
- the SCP80 and RAM views get a Key-version selector beside the preset
selector, driving the KIc/KID bytes and loading that keyset's keys and
counter; the packet builder refuses a version the preset does not define
(spKeysetCheck/spKeysetGuard, pure and unit-tested);
- the counter syncs are per keyset (spCntrLocalSync/ramSaveCntr), the SCP80
form's counter edit posts the keysets, and the test-run write-back syncs
every counter the server reports (scp80_counters).
Docs/help/AGENTS updated; sw cache simple-v335. Tests: 610 Python (store
keysets, endpoint refusals, the counter landing in the right keyset) and 726
Node (keysets.test.js plus the updated harnesses).
- /api/send-ota no longer 500s on a counter-less body: the plain `sp` path
reports final_cntr only when the counter is usable (_counter_valid, 1-10 hex
digits) - before, the new plain-path report ran int('') and the request died
in the generic handler. The failure path is guarded the same way.
- _decode_por wraps the keyset/SPI construction in the same "never raise" rule
as the decode: a malformed counter or missing keys return None instead of
escaping the handler.
- presets.add() never accepts a taken id (a fresh uuid is assigned, like the
import path), the counter is validated as 1-10 hex digits instead of being
silently masked/kept, and set_counter() ignores a malformed value with a log
line instead of writing it.
- the preset endpoints answer a JSON 500 when the store write fails (read-only
home, disk full) instead of dropping the connection.
- tests: counter-less send-ota, store-write 500, duplicate id, counter
validation (add/update/set_counter), and the temp-file check no longer
matches the audit file; docs/api.md documents final_cntr for the plain path
and preset_id.
The presets lived in the browser's localStorage and the PWA wrote the SCP80
counter back after every operation, so an increment could be lost when the
response never reached the tab, when two browser contexts (or the demo origin
plus a local one) held separate copies, or when the tab was closed mid-run.
The counter is card state and the server performs every card operation, so the
store now lives next to the server:
- pysim_simple_server/presets.py: one JSON file
(~/.pysim-simple-server/card_presets.json, --card-presets overrides; Path.home()
resolves the same way on Windows), atomic writes (temp + fsync + os.replace),
an RLock, stable uuid ids, the ICCID normalisation the PWA uses (digits /
spaced / raw EF hex), a duplicate-ICCID refusal and a monotonic set_counter;
every counter change is appended to card_presets.json.audit.jsonl.
- API: GET /api/presets (card-free), POST /api/presets and
/api/presets/update|delete|import; the import also takes the old localStorage
export shape - that is the migration path, there is no automatic migration.
- Operations name a preset_id and the server persists the counter they
consumed: /api/send-ota (the plain SCP80 path reports final_cntr now too),
the RAM install/install-app/compat steps (_ram_send_gp_apdu) and the test
runner's SCP80 steps (_preset_counter_persist).
- PWA: the Cards tab is a thin client of the store (load on connect, CRUD via
the API, export/import), the counter write-backs became local display syncs
(the SCP80 form's counter input stays a deliberate human edit) and the
SCP80/RAM bodies carry preset_id; a new pysimFetchOk() surfaces the server's
error bodies instead of silently using them.
Policy: card keys in logs and local files are not a risk for this workbench
tool (AGENTS.md) - the store is not encrypted and no redaction is required.
Tests: tests/test_presets.py (store: ICCID forms, counter monotonicity and
wrap, atomic writes, corrupt-file recovery, import), the preset endpoints in
tests/test_fetch_routes.py (incl. the counter persistence via /api/send-ota)
and the updated PWA tests. sw cache simple-v334.
start.sh tees its console (exec > >(tee ...)) and exports PYTHONUNBUFFERED
so the log stays live; start.bat does the same through PowerShell
Tee-Object (UTF-16 on PS 5.1, UTF-8 on PS 7+). The log carries card keys
and request payloads of the test cards, so it is gitignored and must not
be committed - read it for the latest server output instead of
copy-pasting the console.
Two bugs in the v3.7.0 per-AID fallback, both visible in the live log
(2026-09-30 11:21):
- the candidate applet was searched by the AID prefix, but the applet
instance AIDs are the package's module AIDs (package A113...0200 has
modules ...0208 / ...494D4508, both present as applet instances - they
differ from the package AID in the last byte), so that package produced no
query at all. ramElfAppletCandidate (pure) prefers the module AIDs and
keeps the shared-prefix rule as a fallback;
- the single-AID expanded query answered 6134 (the card holds the data) and
the empty response was read, so no version was derived. The query is now
retried with the GET RESPONSE chained into the compact command string
(ramChainGetResponse) exactly like the ladder - a standalone GET RESPONSE
answers 6700 on this card.
Tests: the module->applet mapping with the live AIDs (incl. the versioned
applet being skipped and the prefix fallback), plus the source guards for the
chained retry.
sw cache simple-v333; tests: 557 Python / 717 Node.
No live card accepts a tag list for the ELF queries (tags/noea/lean all
answer 6A88 on P1=10 - the log of 2026-09-30 11:11), so the version-carrying
CE tag was unreachable there; the ELF listing itself (notags + chained GET
RESPONSE) works and carries AID + life cycle + modules only.
Two fallbacks:
- when the ELF rows have no version and the details pass never got a
tag-list form, the ladder is walked once more with P1=20 (a card's P1=10
implementation can be stricter) and merged by AID;
- any version still missing is fetched per applet AID: the AID search
qualifier (4F <len> <AID>, GP 11.4.2.3 Table 11-35) plus the apps tag
list selects a single applet entry, whose CE is its package's version
(the applet <-> package mapping is the AID prefix; at most 6 queries).
Also:
- the app and ISD rows show their parsed CE version (the expanded apps
listing carries it);
- an unfulfilled 6310 (the next-occurrence page refused, e.g. 6A86) is
reported as '(first page only)' instead of looking complete;
- the page loop only continues when the chained GET RESPONSE was accepted -
a refused retry no longer costs an extra P2=03 packet.
sw cache simple-v332; tests: 557 Python / 716 Node.
A cntr_low PoR verdict is not recoverable by retrying - every packet whose
counter is not above the card's is rejected again - so it now stops the
operation and is stated as plain data instead of a generic PoR error.
Server:
- _cntr_low_fields(status, cntr) -> {cntr_low, card_cntr, suggested_cntr}
(fixed-width wrap like the counter field); carried by /api/send-ota (plus
an 'OTA CNTR-LOW: card counter ...' log line) and by the RAM install /
install-app / cap-compat failure responses (from the failed step);
- _send_envelope's late-PoR poll now also runs after a warning ENVELOPE
answer (62xx/63xx - the live card answers 6200 for a low counter) and
_send_secured_packet passes the captured PoR on, so the verdict is named
instead of a bare 'ENVELOPE failed' (that is why it only surfaced on the
later SPI2=0x21 query in the live log).
PWA:
- spCntrLow(res) guards every flow; the Explore routes the whole run through
a checked sendOta(), so the paginate/expanded loops stop, no further
packet is sent and no counter is written back (delete and Send to Card
check it too);
- the result states the card's counter and offers 'Go to preset'
(ramGoToPreset: opens the failed operation's preset in the Cards tab for
editing and focuses the counter field) - nothing is written
automatically; the operation modal carries the same notice;
- help (EN/RU) documents the guard.
sw cache simple-v331; tests: 557 Python / 714 Node.
The live 500 in the Explore ('OTA send error: index out of range') was a
parser bug, reproduced with the live FETCH shape: _parse_proactive_header
walked the command TLVs reading the length as a single byte, so the card's
SEND SHORT MESSAGE with its 150-byte SMS TPDU (8B 81 96) was mis-walked
into the TPDU and eventually indexed past the end (val[2] on a short
slice) - the FETCH was left unanswered and the request answered 500.
- _ber_tlv_list/_proactive_body helpers: BER lengths (_ber_len_at) plus a
bounds check that stops at a truncated TLV instead of walking into the
value; _parse_proactive_header, _parse_display_text, _parse_select_item,
_parse_setup_menu_items, _tlv_map and _decode_poll_negotiation all use
them now (the same latent bug for any long-form TLV);
- _run_proactive_chain guards every step (header parse, proactive log,
on_fetch, BIP/timer handlers): an exception is logged with the offending
FETCH bytes and the TERMINAL RESPONSE is still sent - a decoder bug can
never leave a FETCH unanswered again;
- the expanded-registry report distinguishes a request error from a remote
SW: 'ELFs: request error (index out of range) - compact listing used'.
Regression tests: the live long-form FETCH shape parses (header + TLV map +
TPDU), truncated TLVs stop the walk, and a raising parser still gets a TR.
sw cache simple-v330; tests: 553 Python / 711 Node.
The live log (2026-09-30) showed our standalone GET RESPONSE was wrong: the
card answered 6700 (wrong length) to 00C00000<xx> for the 61xx answers of
the expanded ISD query and the ELF notags query. The compact path proves
how this card wants it: GET STATUS and GET RESPONSE chained in one compact
command string (80F2..C0000000).
- a 61xx answer now retries the same query with ramChainGetResponse
(+ C0000000, compact format only; the expanded AA/22 form has no command
string, so no retry there);
- a 61xx page still continues to the next occurrence (the GET RESPONSE's own
SW replaces the 6310 signal); the ISD stays a single occurrence;
- the standalone 00C00000<xx> attempt is gone (it only ever answered 6700).
This card also refuses every tag list for the ELF queries (tags/noea/lean
all 6A88), so CE cannot come from the ELF listing. Its apps listing does
carry the load file version (CE) together with the package AID (C4), so
ramDeriveElfVersions fills a missing ELF version from its applet instance
and the row annotates it: 'Version: 9.3 (from the app instance)'. The
version hint now shows whenever any ELF row lacks a version.
sw cache simple-v329; tests: 550 Python / 711 Node.
The complete live trace (2026-09-30) showed the card was not refusing nearly
as much as the report suggested:
- the ISD expanded query answered 6131 and the ELF notags query 617F/613F -
response data pending, and no GET RESPONSE was ever sent;
- the apps expanded query answered 6310 (more occurrences) and only the
first page was merged;
- the ELF tag-list forms (tags, noea) answered 6A88 because they still
carried the application-only C5 privileges / C4 ELF AID.
Fixes:
- 61xx -> ramGetResponseApdu(sw) = 00C00000<xx> (up to 4 rounds);
- 6310 -> the same query as a next-occurrence page (P2.b1=1, P2=03, up to
8 pages), so the apps listing is complete;
- a 'lean' ladder form with only the objects valid for the kind: ELFs get
4F 9F70 CE 84 (AID, life cycle, version, modules), the ISD/apps get
pySim's 4F 9F70 C5 CC; the ladder is tags -> noea -> lean -> notags;
- the report groups by listing label with the best outcome winning
(entries > accepted-but-empty > refusal): the P1=10/P1=20 ELF double
query no longer prints 'ELFs, ELFs' and a pass with data is never called
a refusal; the ELF version hint is form-aware.
sw cache simple-v328; tests: 550 Python / 709 Node.
The 'Fetch expanded registry details' checkbox is now on by default: the
expanded pass carries the package versions, the SD/load-file associations
and the applet menu entries; an explicit stored opt-out is still honoured.
Live 2026-09-30: a card with a non-default ISD AID answered 6A88 to the
reference expanded query (P2=02 + the tag list), so the ELF rows had no
versions. Cards differ in what they accept, so the query is now tried in
three forms (RAM_EXPANDED_FORMS):
tags - the reference tag list (incl. the TS 102 226 EA SCP Registry Data)
noea - the same list without EA (a card that errors on it)
notags - no tag list at all (GPPro's 4F00-only data field); the card
builds the default E3
The first form that answers is remembered for the run and tried first for
the later listings; the ISD gets a single attempt - its 6A88/6A86 is normal
('no data to report', GPPro) and must not poison the detection for the
apps/ELF queries.
Every listing's outcome is reported in the explorer (ramExpandedReport):
'Expanded registry: tag list + EA (Apps, ELFs) - ISD: no expanded data
(6A88) - compact listing used', or 'Expanded registry refused by the card -
compact listing only (package versions unavailable)'; the ELF section hints
when no version came back. Tooltip/help (EN/RU) updated.
sw cache simple-v327; tests: 550 Python / 707 Node.
ramShowInstallResult appends ramCompatDetailHtml(data, capMem) to the
install modal (the modal content is HTML now - the step lines are escaped
and ramOpModalFinish takes the asHtml mode), so a failed LOAD / INSTALL
[for load] shows the same block analysis as the compatibility test:
- the failing block's CAP section (load-file byte range and components, and
what completes there), with the JC VM spec 4.5.2 note - or 'the import
list already passed' when the failure is later in the file;
- the remote status word with its name;
- the import list at the CAP's own versions (a real install runs no probe);
the closing hint points at the compatibility test ('Set all 0.0' first,
then bisect), while a probe run keeps the lower-the-versions wording.
ramInstallFailHint stays as the short inline hint; INSTALL [for install] and
/api/ram-install-app failures show no block (the LOAD guard in the helper).
sw cache simple-v326; tests: 550 Python / 704 Node.
The compatibility test now runs in the same operation modal as the installs
(live progress from the server's ram_progress, Close disabled until done) and
its result gains a per-AID detail block instead of the comma list:
- the probe summary (probe: 2 imports -> 0.0 . +1 appended);
- the import list as tested (ramCompatImportRows): the CAP's imports with the
version the card was asked for and the CAP's own version when the probe
lowered it, appended synthetic entries and unmatched lines - each with the
resolved library name (jcAidName), so a raw AID list becomes readable rows;
- on a rejection, the failure analysis (ramCompatFailureHtml): which CAP
section the failing block covers (load-file byte range and component names,
'completes Import'), what the card checks at that point (JC VM spec 4.5.2)
or that the import list already passed, the remote status word with its
name, and what the probe state implies (every import at 0.0 -> a missing
package or another load-file check is the cause; otherwise lower the
versions and bisect).
ramCompatVerdict names the remote SW; the inline result keeps the verdict +
short probe note and the modal carries the escaped step lines + the detail
HTML (ramOpModalFinish gained an asHtml mode); the server reports
probe_unmatched as [{'aid', 'version'}] (_probe_unmatched), documented in
docs/api.md. The same failure block for the real install is the next step.
sw cache simple-v325; tests: 550 Python / 703 Node.
The compact (legacy) GET STATUS entry is <aid_len><AID><life cycle>
<privileges> + (P1=10) <module count> (<len><module AID>)*, cross-checked
against GlobalPlatformPro's populate_legacy (the 4th byte is privileges;
0x00 is required for load files). The parser read it as a 'version' one
byte late, so ELF rows showed the module count / the next entry's length as
Version: 00/10 - the compact listing has no version at all; it now comes
from the expanded CE tag (ramFmtVersion: 2 bytes -> major.minor, GP Table
11-37 Note 1) and merges (the bogus compact value no longer blocked it).
New data exposed:
- EA SCP Registry Data (TS 102 226 8.2.1.6) -> 80 Menu Parameters -> the
applet's STK menu entries: STK menu entry: 0x80 (position 1, Enabled)
- unknown registry objects are preserved (rawTlvs) and shown as
'Other registry data: 8F=752B . 90=012D ...' instead of being dropped
- 84 parses the GP coding (one Executable Module AID per TLV, repeated; the
concatenated 4F<len><aid> form stays as fallback) and the ELF expanded
queries add 84 to the tag list (with a tag list the card returns only the
listed tags - the fallback path would otherwise lose the module lists and
their Install buttons)
- life cycle names decode per object kind (GP Tables 11-3..11-6: ISD/card,
app, SD - detected via the Security Domain privilege - and ELF), with
multi-byte 9F70 (02 0700 on live cards) decoding the first byte and
surfacing non-zero extras; the R-APDU parser's LIFECYCLE_MAP labels were
aligned with the same tables
/api/ram-install measures the NV footprint: a silent GET DATA FF21 read
before and after the chain (also on failure) returns nv_before/nv_after/
nv_delta; the reads never fail the operation (no FF21 -> no fields) and the
PWA shows 'NV used: ~N B (free A -> B)' in the install result.
sw cache simple-v324; tests: 548 Python / 698 Node.
The SIM/UICC Toolkit parameters grid is grid-cols-3 now: the 12 narrow
fields (priority, timers, text length, menus, first pos/id, last pos/id,
channels, MSL, TAR, access domain) fit in four clean rows, Max services
keeps its own row (it was a lone half-row in the 2-column layout) and the
file-access group is one col-span-3 row: File system access (full) ·
ADF.USIM access (full) · ADF AID (hex) + From card.
The ADF AID cell is its own rc-tk-adfaid-row; the whole group is wrapped
in rc-tk-access-row, which updateRcTkMode hides in SIM/CA mode (hiding
only the old checkbox rows would leave the AID field behind).
grid-cols-3/col-span-3 were already compiled (no Tailwind rebuild); the
html test pins the 3-column grid and the spanning access row with its
three cells, the JS guard pins the CA/EA visibility.
sw cache simple-v323; tests: 540 Python / 689 Node.
Review of v3.6.48 found six issues; the two that matter in use:
- ramProbeParse('') errored, so the basic 'is this CAP compatible with
this card at all?' run (no overrides) was refused by an alert. An empty
list is valid now: the test runs with the CAP's own versions.
- ramCompatVerdict said 'Import list rejected' for every failure. It now
distinguishes: a request error (no steps), a failed format probe (the
card not answering), an INSTALL [for load] refusal, and only then the
import list - each with its own headline.
Plus:
- the result line shows the applied probe (ramCompatProbeNote: 'probe:
7 imports -> 0.0 · +1 appended', unmatched AIDs included);
- #ram-compat-block validates 1..240 like the install's block size;
- the server reports probe_unmatched (override AIDs matching no entry,
previously silent when another override matched) and logs them;
- tests: the endpoint's probe-400 path (a mini CAP + a dummy scc),
_cap_import_aids, the verdict cases and the probe note.
sw cache simple-v322; tests: 540 Python / 687 Node.
The import probe no longer lives in 'Install Package (.cap file)': a
diagnostic that can leave a LOADED-only package does not belong in the
operation meant to produce a working applet. /api/ram-install no longer
takes probe_imports; the install form's probe fieldset is gone.
New RAM operation CAP compatibility test (POST /api/cap-compat): the
server runs the format check, INSTALL [for load] and the LOAD blocks only
up to the block that completes the Import component (_cap_import_end +
_cap_compat_blocks, computed from the load file and the C4 TLV framing),
then stops - no last-block flag, no INSTALL [for install], so nothing is
committed and nothing needs deleting. The verdict (imports_ok,
boundary_block/total_blocks) states it covers the LOAD/import gate only;
the link gate stays the real install's business. Verified on the real
RemMobileID CAP: the boundary is LOAD 1/64, 2/128, 3/240 for 240/120/64 B
blocks - exactly the blocks where the live cards rejected the load.
_probe_import_versions(load_file, overrides, additions) now supports
append-only synthetic imports: +AID=version appends a new entry, the
Import component's size field and the Directory's Import size are patched,
and existing entries keep their positions (constant-pool import indices
stay valid); an AID that is already imported becomes a version override.
This is the card-capability query ('does this card export package X at
>= Y?') without building a probe applet.
PWA: new op section (CAP file + analysis + the version list + block size),
ramCompatTest with inline results and ramCompatVerdict, ramProbeParse
understands +AID=version; help EN/RU documents the operation, the two-step
bisect and the gate scope.
sw cache simple-v321; tests: 538 Python / 686 Node.
The install modal's progress bar froze around 70% and jumped to 100% at
the end: not a math bug - the UI's other polls (/api/stk-status,
/api/poll-status, /api/proactive-log, /api/menu, ...) were card-locked in
do_GET even though they only read cached state, queued behind the install
and exhausted the browser's 6-connections-per-host budget, starving
/api/status. _CARD_FREE_GET lists every card-free GET (status, version,
stk-status, poll-status, proactive-log, menu, events, terminal-profile,
pli-*, commands, net-state, mcc-mnc, scp81/bip status|script|log);
/api/cardinfo and /api/esim/* stay locked (they touch the card).
tests/test_fetch_routes.py holds _CARD_LOCK and asserts the cached
endpoints still answer - verified to time out on the pre-fix list.
The install modal is wider (max-w-3xl) and successful step lines are
compact: '✅ Step 3: LOAD (1/240) - 6101 · 274 B / 3 SMS' (no verbose SW
name, B instead of bytes, the SMS count only when >1); failed steps keep
the full detail and the hint.
sw cache simple-v320; tests: 533 Python / 684 Node.
The max-w-7xl mx-auto page container closed before #tab-scp81, so SCP81
and Cards rendered directly under <body> and stretched to the whole window
while the other five tabs were capped at 1280 px. The closing </div> moved
after #tab-cards (before the modals); a structural test walks the markup
and asserts every tab-content has the container as an ancestor (verified to
fail on the old nesting with 'tab-scp81 must be inside the max-w-7xl page
container').
The SCP80/RAM form's Card preset and Operation selects now share one
flex gap-2 items-end row with Execute (guarded by a markup test).
sw cache simple-v319; tests: 531 Python / 683 Node.
- Explore runs inline: no blocking modal (that stays for the install
operations, which can leave a half-loaded package; Explore leaves the
card consistent). A test guards that ramExplore never calls the modal.
- Minimal success line: just OK - the details live in the explorer below;
error runs keep the detailed 'Partial - ...' line.
- Counts moved into the section headings (Applications / Applet Instances
(n), Executable Load Files (ELFs) / Packages (n)); the redundant '1 ISD'
count is gone (P1=80 returns the ISD as a single occurrence, GP
11.4.2.1).
- The RAM command format line gained a tooltip: compact = TS 102 226
5.2.1 command string, expanded = the AA/22 scripting template (5.2.1.0),
detected per operation with a read-only GET STATUS probe.
- The expanded-registry checkbox is relabelled 'Fetch expanded registry
details (load-file / SD associations)' with a tooltip (extra secured
packets, card support varies, the fallback is automatic) and is now a
sticky per-browser preference (simple_ram_expanded).
sw cache simple-v318; tests: 531 Python / 681 Node.
The CAP details' Platform row named only the Java Card level; it now also
shows the ETSI/3GPP release the UICC API imports come from, with its basis
in brackets, e.g. 'Java Card >= 2.2.2 - ETSI REL-7 (uicc.toolkit 1.4) -
CAP format 2.1'.
capmem.ETSI_TOOLKIT_RELEASE maps the uicc.toolkit export version to the
release, sourced from the workspace corpus: sim-tools/ETSI-REL.md for the
prepared trees (api21_rel7_export_files carries uicc.toolkit 1.4 = the
REL-7 jar, api21_export_files 1.11 = the REL-12 jar) and
sim-tools/etsi/102241/README.md's package-version table for the rest
(1.0 -> REL-6/7, 1.3 -> REL-8, 1.5 -> REL-9, 1.12-1.14 -> REL-15..18; the
1.11 spec range is 13.01-14.02 but the tree the builds use is REL-12).
Exposed as requires_etsi_release/requires_etsi_basis in /api/cap-info and
the CLI; SIM-only CAPs (no uicc.toolkit) get no label.
Verified: RemMobileID -> REL-7 (uicc.toolkit 1.4), AlfaDSA -> REL-12
(uicc.toolkit 1.11). sw cache simple-v317; tests: 531 Python / 677 Node.
The probe's checkbox described the mechanism ('Rewrite the CAP's import
versions before the load'); it now reads 'Enable import probe' (purpose
first) with the mechanism in a tooltip, and the field carries two short
hint lines: what the card checks while loading, and the workflow (Set all
0.0 first; if the LOAD passes the list was the cause, then restore one
line at a time to find the package/version the card lacks).
Static tooltips are translatable now via data-l10n-title (handled by
translatePage, checked against LANG_RU by html.test.js together with
data-l10n); the help's RAM-install section documents the two-step
workflow (EN/RU).
sw cache simple-v316; tests: 530 Python / 676 Node.
Cards that cannot satisfy the CAP's import list reject the LOAD as soon
as the Import component is complete (live 2026-09-29: 6438/6985 in the
block containing its last byte, on several cards incl. an eUICC profile
whose ISD is not the default AID and which does not answer GET DATA 0066 -
no direct platform-version readout exists).
_probe_import_versions (pure) rewrites the Import component's two version
bytes per entry in the assembled load file: same length, component sizes,
Directory and offsets untouched; unknown AIDs and bad versions are
rejected. /api/ram-install takes probe_imports ({'all': '0.0'} or
per-AID), echoes the applied changes in the response and logs them. The
PWA's RAM install form gains a collapsed Import probe fieldset (checkbox,
AID=version lines prefilled from the CAP analysis, 'Set all 0.0') and the
result line reports the applied changes. Diagnostic only - never used for
a real install.
Also verified _cap_parse's component order against the reference TCA load
trace (Header, Directory, Import, Applet, Class, Method, StaticField,
ConstantPool, RefLocation, Descriptor - Import before Applet, not
ascending tag order), so the order is correct and the import list remains
the prime suspect.
sw cache simple-v315; tests: 530 Python / 676 Node.
pysimFetch() sends POST whenever a body is passed, but /api/test/run,
/api/test/stop and /api/test/clear were registered in _do_GET (since the
phase-1 commit 3a4a098): every Start fell through to the catch-all
404 {'error': 'Not found'} and the feature had never been exercised over
HTTP (a browser GET would not carry the JSON body either).
The three branches moved into _do_POST; safe under the RLock-based
_CARD_LOCK because _test_run_start re-acquires it before spawning the
worker.
New tests/test_fetch_routes.py:
- a PWA<->server route/method matrix (every body-carrying pysimFetch needs
a POST route, body-less calls a GET/POST route; routes are extracted from
_do_GET/_do_POST incl. the 'self.path in (...)' and startswith forms) -
verified to fail on the pre-fix routing with exactly the three endpoints;
- an HTTP smoke test on the real handler: POST /api/test/run {} -> 400,
POST stop/clear -> 200, GET /api/test/run -> 404.
sw cache simple-v314; tests: 527 Python / 675 Node.
The TERMINAL PROFILE chain parsed the SET UP MENU title with 'tag == 0x05'
only, so the live Alfa card's title (CR-set 0x85, 'Alfa Mobile') was
dropped: /api/menu returned items without a title and the PWA showed
'No menu set by the card' - card-specific only because cards pick the tag
variant (this one sends plain 0x8F items and a CR-set title in the same
command).
- _parse_setup_menu_command (pure, tested with the live FETCH bytes)
replaces the inline parse; the PWA shows a title-less menu as 'STK: Menu'
instead of 'No menu set by the card' (stkMenuLabel, pure).
- _cmd_tlv is now the single lookup helper: exact tag, then tag ^ 0x80
(bit 8 is the CR flag, TS 101 220 7.1.1); the old '| 0x80'-only helper and
the duplicate _cmd_tlv_any are gone.
- Remaining single-style checks fixed: _parse_setup_menu_items (0x8F/0x0F),
_parse_select_item title (0x85/0x05), _find_sms_tpdu (0x8B/0x0B - the
plain variant lost the SEND SHORT MESSAGE PoR TPDU), the response
scripting parsers (R-APDU 0x23/0xA3, executed count 0x80/0x00) and the
FF21 resource tags (0x81-0x83/0x01-0x03).
- tests/test_tag_variants.py flips the CR flag on every top-level tag of
real command fixtures and asserts identical parse results for all
parsers (verified to fail when a single-style lookup is reintroduced).
sw cache simple-v313; tests: 524 Python / 675 Node.
__main__ built a plain HTTPServer (single-threaded): during a RAM install
POST every other request waited in the accept queue, including the 2 s
/api/status poll that feeds the operation modal - its bar and step
headline never moved and the whole result appeared only at the end (live
2026-09-29).
_build_http_server() now returns a ThreadingHTTPServer (daemon threads).
Card access is unchanged: it is serialized by _CARD_LOCK, not by the
server - every POST is blanket-wrapped in do_POST, every /api/ GET except
/api/status and /api/test/status is wrapped in do_GET, and the background
threads (STATUS poll, _bip_data_available, test runner) take the lock;
the audit found no card-access holes. do_GET also lets non-/api (static
PWA) paths bypass the lock, so page assets stay answerable during long
operations.
Regression test tests/test_http_concurrency.py: a fast request must
complete while a slow one is in flight (fails on HTTPServer with exactly
the reported symptom: 1.01 s vs 0.20 s); test_startup_cardless patches
the new factory.
sw cache simple-v312; tests: 519 Python / 674 Node.
The menu chain paused only for DISPLAY TEXT / SELECT ITEM / SET UP MENU,
so a GET INPUT was auto-answered with a bare success TR and no Text
string - the live RemMobileID PIN prompt got an empty answer.
Server: _parse_get_inkey/_parse_get_input read the prompt, qualifier
flags (digits-only, UCS2, Yes/No, immediate, hidden, packed, help),
Response length 91 and Default Text 9D; the fetch handler pauses with
them; _menu_send_response(..., text=) validates the answer (digits set,
min/max, one char for GET INKEY) and builds the Text string TLV 8D in the
requested coding (SMS unpacked 04 / UCS2 08 / packed 00 via _pack_gsm7),
Yes/No as 01/00, empty input as the null text string, help as result 13
and the variable-timeout Duration echoed (6.8.4). /api/menu-respond
takes text; /api/stk-status carries pending_data so a card-initiated
prompt opens the panel; _decode_cmd decodes both commands.
PWA: stkInputHtml/stkInputHint/stkInputValidate render the constrained
panel (password field for hidden entry, numeric keyboard, default text,
Yes/No buttons, help, immediate-digit auto-submit); OK submits through
stkMenuOk; the poll opens the panel for card-initiated prompts.
sw cache simple-v311; tests: 517 Python / 674 Node.
_send_envelope defined its SEND SHORT MESSAGE capture handler inside the
91xx branch, but the 'STATUS poll (PoR not captured)' path used it: a
packet whose ENVELOPE answered 9000 and whose late PoR arrived on the
STATUS poll raised UnboundLocalError, returned 500 and aborted the RAM
operation mid-packet with the card's command left pending (live
2026-09-29, 120-byte LOAD blocks). The handler is now defined for every
path, and the late-PoR poll runs only after a packet's last segment
(poll_status from _send_secured_packet) - a concatenated download makes
no spurious mid-packet STATUS exchanges.
The PWA annotates every LOAD step with the CAP components its block
covers (ramStepComponents: 'completes Import' / 'inside Method' /
'Header...Import') and ramInstallFailHint names the component the failing
block completed - the live import-verification rejection lands in the
block completing Import (6438/6985 at both 240 B and 64 B blocks).
sw cache simple-v310; tests: 509 Python / 672 Node.
capmem derives the minimum platform from the CAP's Import component
(javacard.framework -> Java Card level via the SDK-kit corpus, int flag)
and /api/cap-info exposes it; the CAP details show it as a Platform row
('Java Card >= 2.2.2 - CAP format 2.1 - int support').
A failed RAM install at a LOAD / INSTALL [for load] step now appends a
hint naming the import requirement and the smaller-block fallback
(ramInstallFailHint, pure): JC VM spec 4.5.2 makes every import a
'>= version' demand on the card's own packages checked while the load
file is verified - live 2026-09-29, a CAP requiring Java Card 2.2.2 was
rejected exactly at the block containing the end of Import (6438 at
240 B blocks, 6985 at 64 B), not at a step count.
sw cache simple-v309; tests: 507 Python / 670 Node.
The live log showed the "stuck" state: after the menu watchdog (60 s)
answered a pending command, the PWA kept the panel; the next click got
{'error': 'no pending command'}, 400 and that reply was fed to the dialog
handler as if it were a card answer. A page reload then cancelled in-flight
polls, flooding the log with BrokenPipeError tracebacks.
- stkMenuRespond detects the already-answered reply (stkNoPendingError) and
closes the panel (stkMenuPanelReset) instead of treating it as an answer;
the 5 s status poll drops the panel whenever the server reports
pending: false, even when the Phone tab is hidden.
- /api/terminal-profile (POST) and /api/rescue finish a pending command
with a cancel TR first, so re-sending the TP can never shadow an
unanswered FETCH.
- _send_json and _serve_static swallow BrokenPipe/ConnectionReset/
ConnectionAborted with one "CLIENT GONE" line (a reload cancels polls);
the handlers' state changes still stand.
Tests: stkNoPendingError + the already-answered respond path; version
3.6.35; sw simple-v308; stylesheet rebuilt.
A CAP install is a long chain of LOAD commands whose progress and results
were only visible at the bottom of a large form - and an interrupted
install can leave a half-loaded package.
- Server: _RAM_PROGRESS (+ _ram_progress_begin/step/end) records the live
step of the running RAM operation; /api/ram-install and
/api/ram-install-app wrap their chains in it (total = steps + 1 for the
format check) and /api/status carries it as ram_progress - lock-free, so
the 2 s poll works while the request holds _CARD_LOCK.
- PWA: #ram-op-modal (the STK panel pattern) blocks everything while a RAM
operation runs: title, spinner, status line, progress bar, per-step
result lines, Close disabled until it finishes; a beforeunload guard
protects the window during the run. install-cap, install-app /
make-selectable and Explore all use the shared helpers
(ramOpModalBegin/Status/Finish); the install results stay in the form
area as before once the modal is closed. The status poll feeds the modal
via ramOpProgress (headline from the pure ramOpProgressText).
- Tests: ramOpProgressText (JS) + the progress lifecycle (Python); the
card-state and capmem suites stub the DOM-bound hooks.
- Stylesheet rebuilt; version 3.6.34; sw simple-v307.
The suggested quota row mixed a decimal C6 with hex C7/C8, so C6=27246
could be misread as hex (about 6x off). It now renders as one consistent
line via the pure capQuotaText(sug): decimal bytes with the hex in
parentheses (C6=27246 B (0x6A6E) . C7=270 B (0x010E) . C8=5143 B (0x1417)),
with a tooltip naming the tags (C6 = non-volatile code space / load file,
C7 = volatile RAM, C8 = non-volatile data).
The static "Notes" disclosure (NVRAM definition + "estimate only") is
removed from the CAP details box - the help (EN/RU, the Install Package
row) carries it now; only the dynamic scan warnings (e.g. unknown opcode)
stay visible, since they mean the estimate is partial.
Tests: the new formatting pinned, the removed notes asserted absent;
stylesheet rebuilt; 664 frontend / 504 Python green; version 3.6.33;
sw simple-v306.
The RAM install form's toolkit parameters are the make-or-break field for
toolkit applets, and CA vs EA is easy to get wrong (the earlier live 6A80s).
- "SIM/UICC Toolkit parameters" is now an accented bordered fieldset (blue
border/legend, always visible in the cap / install / make-selectable
modes) with the checkbox in the legend and the fields inside.
- ramCapToolkitMode(imports) maps the CAP's linked libraries to the install
parameter family: sim.* (AID tree A0000000090003FFFFFFFF89...) -> SIM
Toolkit (Tag CA), uicc.* / uicc.usim.* (A0000000090005FFFFFFFF89...,
A0000000871005FFFFFFFF89...) -> UICC Toolkit (Tag EA) - family-based, so
access-only applets get the right field too. Applied when the block is
enabled and when the CAP is swapped while it is on, with a "detected from
the CAP" hint under the select; no CAP keeps the manual choice.
- Rebuilt style.css for the new accent classes; tests pin the fieldset and
the family mapping.
663 frontend / 504 Python green; version 3.6.32; sw simple-v305.
Even with the rebuilt style.css, three caching paths could still serve the
old file:
- the server sent Cache-Control: no-cache only for index.html/sw.js, so
browsers could heuristically cache style.css;
- the SW install precached with addAll(), which re-uses HTTP-cached copies,
so a new SW version could precache the stale file;
- the fetch handler is cache-first (fine once the cache is fresh).
Now every static shell file is served with Cache-Control: no-cache plus
Last-Modified and cheap 304 revalidation, and the SW install fetches each
precache URL with cache: 'reload' (CACHE simple-v304). A test pins the
install behaviour.
661 frontend / 504 Python green; version 3.6.31.
Note: the public demo simple.atroshin.ru is still v3.6.20 with the old
stylesheet - it needs a deploy of the current frontend/.
The compiled Tailwind stylesheet (frontend/style.css) had been stale since
2026-09-22: classes introduced later were missing from it, so the Install /
Make selectable chips had no background or text colour in light mode (dark
mode only looked green because an older dark:text-emerald-400 rule matched)
and the new w-28 action column did not apply.
- Rebuilt with `npm run build` (Tailwind + appended src/contrast.css);
verified the previously missing classes (bg-emerald-100, text-emerald-700,
hover:bg-emerald-200, bg-red-100, bg-blue-100, w-28, ...).
- Explorer rows are visible chips now: rounded bg-gray-50 dark:bg-slate-800/60
with a hover:bg-gray-100 highlight and the colored left accent kept (module
rows a subtler variant), so a right-column button is unambiguously tied to
its row; ISD/app/ELF/module rows all use the same treatment.
- AGENTS working conventions: an index.html change that introduces new
utility classes also requires `npm run build` in frontend/.
660 frontend / 504 Python green; version 3.6.30; sw simple-v303.
The explorer buttons sat inline after each AID/detail line, so their columns
moved with the text length. Now every entry (app / ELF) and every module
line is a row with a fixed-width action column (w-28):
- left cell: the text (module rows are siblings of their ELF entry, indented
pl-6, so their buttons share the same column as the entry buttons);
- right cell: the buttons, full width of the column, so Delete / Delete All /
Make selectable / Install line up vertically across rows.
All buttons go through ramActionBtn(label, action, tone): Danger (red) for
Delete / Delete All, Ok (emerald) for Make selectable and the **Install**
buttons, which were blue. Test pins the column width and the Install tone.
660 frontend / 504 Python green; version 3.6.29; sw simple-v302.
The RAM explorer's ramFmtPrivileges used a private table with wrong bits and
invented names ("Cum Deletion Ctr", "GSM Card Binding", "Global PIN",
"RFM"/"CFM") and no case for 0x80: a live ISD 9E printed four wrong names
and dropped Security Domain. Per GP Card Spec v2.3.1 11.1.2 Tables
11-7/8/9, 9E = Security Domain, Card Lock, Card Terminate, Card Reset, CVM
Management.
- ramFmtPrivileges now delegates to decodePrivileges (one spec table).
- decodePrivileges decodes byte 1's DAP pair per Table 11-7: b8+b7 = DAP
Verification, b8+b7+b1 = Mandated DAP Verification; the flag bits alone
are no privilege.
- The file had two decodePrivileges copies (the runtime used the later one,
the C-APDU parser test the earlier); the duplicate and its private table
are gone - the single implementation plus PRIVILEGE_NAMES live in the
parser section, which the test evaluates (converted to var like PARSE_INS).
- Tests: 9E/9A live values, the DAP pair, byte 2/3 spot checks; the
PRIVILEGE_NAMES spec pin moved to apdu_parse.
659 frontend / 504 Python green; version 3.6.28; sw simple-v301.
The explorer's FF21 block was titled "Memory (GET DATA FF21)" although the
tag is the Extended Card Resources Information (GP tag FF21, TS 102 226
8.2.1.7.2: installed applications, free non-volatile and free volatile
memory), and the detected RAM command format line sat between the Free NV
and Free Volatile lines.
- Heading is now "Card resources (GET DATA FF21)" with a tooltip naming the
tag and its three fields (EN/RU).
- The "RAM command format:" line moved after the resource lines.
- Help EN/RU spells out what FF21 returns; test asserts the format line
follows the resource lines.
657 frontend / 504 Python green; version 3.6.27; sw simple-v300.
GP 11.4.2.1 makes '10' the superset of '20' (Executable Load Files with
their modules), yet the Explore queried '20' plus a separate '10' modules
pass and merged the two. A live card answered 6A86 (incorrect P1-P2) to
'20' while '10' worked (2026-09-28) - all four P1 values "shall be
supported", so this is the guarded-fallback case, not a vendor branch.
- Explore now queries '10' (with module parsing) and falls back to '20'
(no module lists) only when it yields nothing; a successful fallback
drops the primary failure from the result, and the primary error surfaces
only when both fail.
- The separate P1=10 "modules" pass and ramMergeElfData are gone.
656 frontend / 504 Python green; version 3.6.26; sw simple-v299.
The E3 parsers existed since the vendor-trace work; what was missing was
issuing the query. Per GP 11.4.2.2 the compact listing format (P2.b2=0) is
deprecated and a card may reject it, while the expanded form (P2=02 + the
tag list 4F 9F70 C5 C4 CC CE EA) returns the richer registry data.
- ramExpandedQueryApdu(p1): the reference trace bytes
(80F2 <p1> 02 0C 4F005C084F9F70C5C4CCCEEA 00), pinned in a test.
- Explore: when the compact pass for a P1 yields nothing, the query is
retried in the expanded format and the result becomes the listing (the
compact error only surfaces if the retry also fails); the new "Query
expanded registry details (E3)" checkbox runs the expanded pass on top of
a working compact listing and merges the expanded-only fields
(privileges, ELF AID C4, SD AID CC, module lists) into the entries.
- The detected RAM command format (v3.6.24 ram_format) is now shown in the
Explore result + explorer view, the delete verdict and the plain-send
message.
- Tests parse the real trace payloads (NC ISD, ET application, NP ELF E3
templates); help EN/RU updated.
656 frontend / 504 Python green; version 3.6.25; sw simple-v298.
Comparing the new TCA vendor traces (Explore_EP/ET/NC/NP/SE) with our flow
showed why only one card works:
- The PoR capture was gated on SPI2 bit 0x20. The traced cards deliver the
PoR as a proactive SEND SHORT MESSAGE; with our ISD/memory queries
(SPI2=0x01) the fetched TPDU was thrown away - the Explore died with
"no data" and the RAM install steps silently degraded to an unverified
"no_por". The capture is now unconditional (inline ENVELOPE responses and
proactive submits both work), and the STATUS-poll fallback is enabled for
every card.
- `_ram_next_cntr` still wrapped at 32 bits (`% (2 ** 32)`): a 10-digit
counter like 10000AAAC8 came back as 00000AAAC9 after a RAM/Explore step -
the server-side half of the v3.6.23 counter report. It now advances the
full 40 bits.
- RAM command format (TS 102 226 5.2.1: compact C-APDU vs the expanded
`AA` > `22` form) is detected per operation and never stored:
/api/ram-install and /api/ram-install-app always probe, /api/send-ota
probes when the caller passes `ram_format`. The probe is a read-only
GET STATUS [ISD] as the first step (compact, then wrapped) and every
command of the operation uses the detected format. `_wrap_expanded_apdu`
is pinned to the reference bytes `AA0A220880F24000024F0000`; responses
carry `ram_format` and `final_cntr` (accepted probes consume counters, so
the views use the server's next counter). Applet-directed payloads stay
byte-exact; only RAM views (and RAM/GP chain packs) opt in.
504 Python / 653 frontend green; version 3.6.24; sw simple-v297.
spNextCntr coerced the 40-bit replay counter with `>>> 0`, so any counter
above FFFFFFFF lost its top byte after an accepted send: the live
10000AAAC8 came back as 00000AAAC9, and spCntrSyncPreset then stored the
broken value in the card preset - the RAM/Explore flows (ramIncrementCntr,
which is correct) inherited it.
- spNextCntr: 40-bit arithmetic (`% 0x10000000000`, wrap at FFFFFFFFFF
kept) instead of the 32-bit >>> 0 coercion plus slice(-10).
- spCntrAdjust (+/-): the same 40-bit wrap (an 11-digit value could be
produced at the maximum).
- tests: 10000AAAC8 -> 10000AAAC9, separator handling, FFFFFFFFF7 ->
FFFFFFFFF8, FFFFFFFFFFF -> 0000000000 wrap, out-of-range input clamps.
The server side (_increment_counter_hex, _ram_next_cntr, final_cntr) was
already exact. 653 frontend / 500 Python green; version 3.6.23;
sw simple-v296.
The event list tables carried labels like
"12 Reserved for 3GPP - Network rejection (TS 31.111 7.5.2)". The phrase is
an ETSI-to-3GPP document pointer, not a usage restriction, and it is not
what a user needs - the event name is.
- EVENT_NAMES (PWA + server mirror): the 3GPP-assigned codings carry their
plain TS 31.111 7.5 name ((I-)WLAN access status, Network rejection, CSG
cell selection, IMS registration, Incoming IMS data, Data connection
status change, CAG cell selection, Slices status change).
- 0x20-0x22: "Reserved (future usage)" (no attribution).
- 0x0B/0x14: the spec's full "(single access technology)" / "(multiple
access technologies)" wording instead of the cryptic "(single)/(multiple)".
- tests pin the exact names and that no name contains the cross-reference.
652 frontend / 500 Python green; version 3.6.22; sw simple-v295.
The plain "Send to Card" path refreshed the whole preset right before
sending (spRefreshFromPreset -> cardsApply -> cardsApplyFields) and
re-applied the preset's slot TAR - so a TAR typed for a push/link trigger,
an applet OTA message or the HTTP OTA admin params was silently replaced by
the preset's ISD TAR (the pack buttons for those sources deliberately leave
the TAR manual; the help even said so).
- spRefreshFromPreset(selId, keepTar): the plain send passes keepTar, so
the form's TAR survives; the counter, keys and SPI are still re-read.
- selecting a card preset, ramApplyCard and the chain packs still set the
TAR; the RAM operations keep the preset's ISD TAR by design.
- spParams()/getRamSpParams() normalise the TAR (strip/upper/pad to 6);
only the preview padded before, a short TAR went to the server raw.
- help EN/RU clarified, cards_counter/ram tests updated, sw simple-v294.
652 frontend / 499 Python green; version 3.6.21.
The v3.6.17 prefix built `C9` only when its field was filled, so filling the
C7/C8 quotas with an empty C9 produced `EF…`-only Install Parameters - and
GP Table 11-49 makes C9 mandatory, so the card rejected the data field with
6A80 (decrypted live packet: params started `EF08C7020000C8020000EA25…`).
- `ramParamsPrefix` always emits the C9 TLV (empty -> `C900`, like the
reference tools); the vendor form becomes `C900 EF08 C7020000 C8020000
EA…`.
- the raw "Install parameters (hex)" field is relabelled "Extra install
parameters (hex)" - it appends extra TLVs after the generated prefix.
- tests updated (empty prefix -> `C900`, quotas-only ->
`C900EF08C7020000C8020000`).
650 frontend / 499 Python green; version 3.6.20; sw simple-v293.
`_ram_send_gp_apdu` (the shared step sender of /api/ram-install and
/api/ram-install-app) now logs every step's plaintext APDU and packed
packet, like /api/send-ota's unconditional lines:
RAM C-APDU (INSTALL [for load]): 80E602000C07F0414C4641610100000000
RAM SECURED-PACKET (INSTALL [for load]): 00281516212525000000...
No flag - the step name keeps the sequence readable, and --apdu-trace stays
the card-level (ENVELOPE) trace. Tests: the logging pinned with stubbed
packet builders.
498 Python / 650 frontend green; version 3.6.19; sw simple-v292.
Iterating the final install step no longer needs a delete + re-load of the
.cap:
- `POST /api/ram-install-app` runs INSTALL [for install] (`mode: "install"`,
P1 0x0C/0x04 by make_selectable) or INSTALL [for make selectable]
(`mode: "make_selectable"`, GP 11.5.2.3.3 Table 11-44) for an already
loaded package. The APDU builders are shared with `_cap_apdu_sequence`
(`_cap_install_apdu`, `_cap_make_selectable_apdu` - one builder, no drift)
and the per-step send/PoR/counter logic is the extracted shared
`_ram_send_gp_apdu` (used by /api/ram-install too).
- PWA: the RAM op selector gains "INSTALL [for install] (loaded package)"
and "INSTALL [for make selectable]" with Load File / Module / Instance AID
inputs (instance defaults to the module AID); the result renders through
the shared `ramShowInstallResult` and `final_cntr` keeps the preset in
sync.
- Explore: an **Install** button on a package's module AIDs that have no
instance yet, and a **Make selectable** button on INSTALLED (0x03)
instances - both switch the op and pre-fill the AIDs.
- tests: the install APDU pinned to the live no-STK packet and to the
sequence tail, the make-selectable APDU, ramHasInstance, the Explore
buttons and the op wiring; docs/api.md updated.
650 frontend / 498 Python green; version 3.6.18; sw simple-v291.
The vendor's working install script (samples/uicc/applets/STK3) sends
`C9 22 <config> EF 08 C7 02 0000 C8 02 0000 EA 26 80 0A ... 81 18
<access>` - an applet config in C9 and memory quotas in EF. The RAM install
form now composes that prefix:
- "Applet-specific parameters (C9, hex)" -> `C9 <len> <value>`;
- "C7 / C8 memory quotas (bytes)" -> `EF <len> C7/C8 <2-byte int>` (4-byte
above 32767, GP 9.7);
- the prefix is prepended to the raw "Install parameters (hex)" field and
the server appends the STK part, giving `C9 EF EA`;
- `ramParamsPrefix`/`ramQuotaValue` are pure and tested (incl. the vendor's
`EF 08 C7 02 0000 C8 02 0000` form).
Also: `updateStkParamsHex` shows a hint naming the rejection reason (TAR not
a multiple of 3 bytes / menu ID > 7F / ADF AID not 5-16 bytes) instead of
silently emptying the field, and the UICC_SPECS.md install row notes that
`CA` sits inside `EF` and `CA`+`EA` together return 6A80.
647 frontend / 496 Python green; version 3.6.17; sw simple-v290.
- the ADF AID field gains a "From card" button: it reads the ADF.USIM FCI
via /api/select and extracts the DF name (tag 84) with the new
adfAidFromFci() - the AID is card-specific (the live card's is
A0000000871002FF49FF0589, 12 bytes; the base A0000000871002 is only a
prefix, and a shorter AID makes the card reject the access entry with
6A80).
- the priority field defaults to 255 (0xFF) and is labelled "(0 = RFU)":
TS 102 226 8.2.1.3.2.6 makes '00' RFU, which can break the toolkit
registration; the builder defaults an unset priority to 0xFF and the
chain rows follow.
- stkParamsBuild's formatted payload fields are uppercase now (the rest of
the tool's hex is; toString(16) emits lowercase).
- tests: adfAidFromFci pinned to the live FCI, the From-card wiring, the
updated form expectations.
643 frontend / 496 Python green; version 3.6.16; sw simple-v289.
The Explore delete showed "Failed: por_ok - remote SW 6700" while the delete
executed: ramDeleteApdu appended the trailing Le, and this card's SCP80
layer counts a trailing byte as a phantom second command whose SW 6700
masked the real result (count=2, last SW 6700) - the same quirk as
INSTALL [for load] before v3.6.8.
- ramDeleteApdu and the chain builder's DELETE row emit the case-3 APDU
(4F AID TLV, no Le); the SCP81 Delete-AID template follows (it reuses
ramDeleteApdu).
- tests updated (delete APDU expectations in ram/ram_counter_flow/scripts).
641 frontend / 496 Python green; version 3.6.15; sw simple-v288.
TS 102 226 8.2.1.3.2.2 defines the EA inner tags as 80 (toolkit params),
C3 (toolkit DAP), 81 (UICC Access Application specific parameters) and 82
(UICC *Administrative* Access) - the plain-text spec extract had shifted the
table columns, so the access list was emitted under 82. With the list under
82 the card rejected the ADF.USIM entry with 6A80 and the applet never got
its uicc.access.FileView rights (live 2026-09-28).
- stkParamsBuild emits the access list under 81 (the structure is unchanged:
the admin field is coded the same way per 8.2.1.3.2.2.4).
- tests updated (81 04 / 81 0F / 81 17 expectations).
641 frontend / 496 Python green; version 3.6.14; sw simple-v287.
"Failed: 9000" although the delete executed: the RAM helpers sent
`sp-spi2` - the base SPI2 select, whose default is "00 - No PoR" - so the
card answered the envelope 9000 with no PoR and the verdict treated the
missing PoR as a failure. `cardsApplyFields` also set `sp-spi2-hex` from
the preset and then `updateSp()` recomputed it from the selects (which the
preset never updated), clobbering the preset byte for anything reading it.
- `getRamSpParams()` reads the computed `sp-spi2-hex`; `cardsApplyFields`
syncs the SPI2 selects from the preset byte (base = low 5 bits, bit 0x20
= PoR via SMS-SUBMIT, dynamic option for unlisted bases) and the base
select gains the missing cipher + RC/CC/DS combinations (15/19/1D).
- a missing PoR is no longer a failure (the SPI may request none, and the
card sometimes refuses one): `OK` with the SW, or `OK (no PoR)`.
- a successful delete drops the object from the Explore result locally (no
re-explore): an applet row goes away; a cascade package delete also drops
the applets whose AID starts with the package AID. The consumed counter
is still saved.
- tests: ramRemoveFromExplorer (app/cascade/prefix), the no-PoR flow, the
SPI2 source guard, and the updated delete-flow stubs.
641 frontend / 496 Python green; version 3.6.13; sw simple-v286.
Live findings (2026-09-28): a browser-restored Receipt Generation privilege
bit (third byte, ISD-only per GP Table 6-2) was silently sent and produced
6985, and the '82' access entries were missing the mandatory "Length of
Access Domain DAP" byte, so the card rejected the ADF.USIM entry with 6A80.
- ramResetGrants() clears the privilege / toolkit-enable / file-access
checkboxes and refreshes the aggregates on load; ramInstallCap re-derives
the privileges from the checkboxes at send time - no stale or
browser-restored grant can be sent.
- the privileges aggregate emits 1 or 3 bytes, never the invalid 2-byte
form (GP Table 11-43), in both updateRcPriv and the chain's computePriv.
- '82' entries carry the DAP-length byte: '00 01 00 00' (shared FS) and
'<len> <ADF AID> 01 00 00' (ADF); the ADF AID is editable
(rc-tk-adfaid, default A0000000871002 = ADF.USIM, 5..16 bytes enforced).
- tests: ram_grants.test.js (aggregate forms, the send-time derivation, the
load-time reset) and the updated access-parameter shapes in
stk_params.test.js.
638 frontend / 496 Python green; version 3.6.12; sw simple-v285.
SIM toolkit applets installed while UICC-library applets failed at INSTALL
[for install] with 6F00 - including with the reference tool's exact install
parameters. The asymmetry: the SIM (CA) path grants file access via the
Access Domain field (default 00 = full access), while the UICC (EA) path
sent no '82' (UICC Access Application specific parameters) at all, and the
reference's '82 00' is empty. An applet importing uicc.access (the failing
CAP has 2 refs) can then fail inside its install().
- EA mode gains two checkboxes (RAM install form + the chain rows' toolkit
block): "File system access (full)" appends '82 03 00 01 00' (shared file
system + Access Domain Parameter 00 = full access, TS 102 226
8.2.1.3.2.2.2/8.2.1.3.2.5); "ADF.USIM access (full)" adds
'07 A0000000871002 01 00' to it. Both default off.
- tests: the access TLV shapes (with/without the ADF entry), the form -> hex
path with the checkbox, and the toolkit-field wiring count (16 fields).
635 frontend / 496 Python green; version 3.6.11; sw simple-v284.
The Apps query (P1=40) was sent with SPI2=0x01 while only the ELF queries
(P1=20/10) used SPI2=0x21. The Apps listing can exceed the ENVELOPE
response, and the card then answers the envelope PoR with
`actual_response_sms_submit` - the actual response would follow as an
SMS-SUBMIT, which is never sent unless the PoR is requested in submit mode.
The paginate only accepted `por_ok`, so the Explore reported the raw status
as an error ("Partial - Apps: actual_response_sms_submit") and the Apps list
never rendered (live 2026-09-28).
- `ramListingSpi2(p1)`: 40/20/10 -> '21', else '01'; the paginate uses it.
- the paginate accepts `actual_response_sms_submit` via `spPorAccepted` and,
when no data arrived, notes "response via SMS not captured" instead of
reporting the card status as a failure.
- the Explore delete verdict follows the same acceptance rule (an accepted
SMS-submit delete no longer shows "Failed" while its counter advanced) and
no longer prints a dangling " -> " without a SW.
- tests: ramListingSpi2 + the delete-accepted-via-sms-submit flow.
633 frontend / 496 Python green; version 3.6.10; sw simple-v283.
The RAM install form sent the `STK parameters (hex)` field, which was only
regenerated when the toolkit checkbox or the mode select changed - editing
the TAR (or any other toolkit field) afterwards was silently dropped. The
live install therefore carried the form defaults (TAR B00001, textLen 0,
menus 0, MSL 16, channels 0) instead of the entered AF4D01 / MSL 12 /
channels 1, and the card rejected the install parameters with 6A80
(TS 102 226 8.2.1.3.2.7: a TAR already assigned on the card).
- every `rc-tk-*` field regenerates the hex on input/change; the install
recomputes it at send time unless the user hand-edited the hex
(`dataset.manual`), so the edit order can no longer drop values;
- one pure `stkParamsBuild` serves the RAM form (`buildRcToolkitParams`) and
the RAM/GP chain rows (`buildTkParams`) - no drift between them (the chain
row also gains the menu-ID <= 7F check);
- applet TAR field: empty by default, no placeholder - B0 00 01 is the
allocated ADF RFM TAR (TS 101 220 Annex D), not an applet TAR; an empty
field is coded as TAR length 00 (the card may take a TAR from the AID only
when the AID carries one, PIX hex digits 15-20);
- the chain builder's INSTALL/LOAD rows drop the trailing Le (the v3.6.8
server form; a trailing Le made the card execute a phantom command);
- tests: stk_params.test.js (the decrypted live parameters, empty-TAR
coding, CA wrapper, long-form lengths, rejection cases, form wiring, and
the form -> hex path with the real builders) and ram_counter_flow.test.js
(the Explore delete persists the consumed counter through the real
ramSaveCntr); STK fixtures no longer use B00001.
631 frontend / 496 Python green; version 3.6.9; sw simple-v282.
INSTALL [for load]/LOAD/INSTALL [for install] carried a trailing Le and an
explicit ISD AID in the Security Domain field; the card executed the extra
byte as a second (phantom) command, so the compact response reported count=2
with SW 6700 - which the v3.6.2 remote-SW check correctly treated as a
failure, aborting at step 1.
Decrypted from the live trace (KIc/KID 25/25, 3DES): ours was
80E6020014 07F0414C46416101 08A000000003000000 00 00 00 00
the reference tool sends
80E602000C 07<aid> 00 00 00 00 (empty SD, no Le)
and its response is count=1 / 9000 while ours was count=2 / 6700.
- `_cap_apdu_sequence`: the SD AID is only sent when a custom one was
supplied (empty -> '00', the card defaults to the ISD; GP 11.5.2.3.1
Table 11-42) and all three RAM install APDUs are case 3 (no Le).
- tests: the expected INSTALL bytes updated, a no-Le assertion for every
APDU in the sequence, and a custom-SD-AID case.
615 frontend / 496 Python green; version 3.6.8; sw simple-v281.
Delete All worked but reset the preset counter: the handler never re-read
the preset (unlike ramExecute), saved N+1, then re-ran the Explore with the
OLD sp (counter N). Every page answered cntr_low, nothing was accepted, and
ramExplore's final ramSaveCntr wrote N back over the preset.
- ramDeleteFromExplorer re-reads the preset first (spRefreshFromPreset
'ram-card-sel'), advances and saves the counter only for a packet the card
accepted (spPorAccepted) - a rejected packet leaves it untouched - and
continues the follow-up Explore from the consumed counter instead of
replaying it.
- the delete result also checks the remote command's SW via a new
`ramRemoteSwOk` (9000 / 61xx / 62xx / 63xx / CAFE, mirroring the server's
`_ram_remote_sw_ok`): a por_ok packet whose DELETE was refused (e.g. 6A88)
no longer shows "OK" (the counter still advances, the card consumed it).
- ramExplore only saves the counter when it advanced, so a stale caller can
never lower a preset counter again.
- tests: flow tests for accepted / rejected / refused-remote-SW deletes
(refresh order, saved counter, the counter passed to the re-explore) and
the ramRemoteSwOk set.
615 frontend / 495 Python green; version 3.6.7; sw simple-v280.
ramDeleteFromExplorer() called an undefined `_ber_len()` helper, so clicking
Delete / Delete All raised a ReferenceError right after the confirm and no
APDU was ever sent. The inline builder also omitted the mandatory Le byte.
- new pure `ramDeleteApdu(aid, withCascade)` = GP Card Spec v2.3.1 Table
11-20/23 form: `80 E4 00 <p2> <Lc> 4F <len> <AID> 00` (P2 00 = object,
80 = object and related objects), used by the Explore handler.
- `scp81DeleteApdus()` (the "Delete AID" script template) now shares the
same builder: it used to send the raw AID without the mandatory '4F' TLV.
- tests: exact bytes for 7/16-byte AIDs and both P2 modes, the SCP81 script
expectations updated to the TLV form, and a wiring check that the undefined
helper call does not come back (`_ber_len(`).
611 frontend / 495 Python green; version 3.6.6; sw simple-v279.