cards: read EF.ICCID at equip and auto-select the matching preset (v2.2.16)

- Server: _decode_iccid (nibble-swapped E.118 digits, trailing-F pad) and
  _read_iccid (best-effort MF/2FE2 read through the parent-scoped select
  helper; the previous selection is restored, the read never raises). The
  equip path records the digit string before the TERMINAL PROFILE, i.e.
  before any CAT session is active; a startup with a card does the same.
  The value is cleared on card removal and exposed as /api/status 'iccid'
  (only while connected).
- PWA: when a connected status update reports a *new* ICCID, the matching
  card preset is selected in both SCP80 views - Secured Packet (sp-card-sel
  + form fill) and RAM (ram-card-sel + _ramCardIdx). Matching normalizes
  digits and accepts the raw EF hex form, leading zeros ignored; a manual
  choice for the same card is kept until the next equip, and a card removal
  re-arms the auto-selection. The status line shows the ICCID.
- Tests: tests/test_iccid.py (decode variants, model + probe read, equip
  recording, disconnect clearing) and frontend/tests/cards_iccid.test.js
  (normalize, find, select, no-override, card swap); the card_state test
  harness stubs the new hook and covers the guard reset.
- Docs: api.md /api/status fields, help EN+RU (SCP80 intro + Cards tab),
  READMEs, AGENTS. SW cache otaman-v183.
This commit is contained in:
2026-09-18 21:05:25 +03:00
parent ab91fcff54
commit 6d5d23487a
13 changed files with 400 additions and 17 deletions
+8 -1
View File
@@ -12,7 +12,7 @@ from pySim.cards import UiccCardBase
from .shell import load_pysim_app
from . import fastinit
from .server import PysimHandler, StderrApduTracer, _LoggingApduTracer, VERSION, _send_terminal_profile, _DefaultProactiveHandler, _handle_proactive_chain, _send_status, _init_proactive_session, _timing_on, _tlog, _set_menu_timeout, start_card_monitor, set_auto_equip
from .server import PysimHandler, StderrApduTracer, _LoggingApduTracer, VERSION, _send_terminal_profile, _DefaultProactiveHandler, _handle_proactive_chain, _send_status, _init_proactive_session, _timing_on, _tlog, _set_menu_timeout, start_card_monitor, set_auto_equip, _read_iccid
_server_start = 0
@@ -128,10 +128,16 @@ def main():
_tlog('pysim_app: %.0fms' % ((time.time() - t_phase) * 1000))
if app is not None and opts.fast_init:
fastinit.install(app)
iccid = None
if scc and card is not None and hasattr(scc, '_tp'):
scc._tp.apdu_tracer = _LoggingApduTracer()
try:
_init_proactive_session()
# Read EF.ICCID before the TERMINAL PROFILE starts the CAT session
# (the PWA auto-selects the matching card preset from it).
iccid = _read_iccid(app)
if iccid:
sys.stderr.write('INIT: ICCID %s\n' % iccid)
t_phase = time.time()
sys.stderr.write('INIT: sending TERMINAL PROFILE %s (CLA=%s)\n' % (opts.terminal_profile, scc.cat_cla))
sm, el = _send_terminal_profile(scc, opts.terminal_profile)
@@ -187,6 +193,7 @@ def main():
server.stk_pending = None
server.card_present = card is not None
server.card_session = 1 if card is not None else 0
server.iccid = iccid
server.equipping = False
# Set server reference for polling timer and mark the card session state
import pysim_otaman_server.server
+46 -1
View File
@@ -21,7 +21,7 @@ from osmocom.construct import GsmOrUcs2Adapter
from osmocom.tlv import BER_TLV_IE
VERSION = '2.2.15'
VERSION = '2.2.16'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
@@ -355,6 +355,41 @@ def _select_path(lchan, path, app):
return _select_with_parent(lchan, parts[-1], None, app, parent_path=parts[:-1], allow_probe=True)
def _decode_iccid(data_hex):
"""Decode EF.ICCID content: nibble-swapped E.118 digits with an optional
trailing 'F' pad (TS 102 221 13.2 / TS 151 011 10.2). Returns the digit
string, or None when the bytes are not a plausible ICCID."""
h = re.sub(r'[^0-9a-fA-F]', '', data_hex or '').upper()
if len(h) < 2 or len(h) % 2:
return None
digits = ''.join(h[i + 1] + h[i] for i in range(0, len(h), 2))
digits = re.sub(r'F+$', '', digits)
if not digits or not digits.isdigit():
return None
return digits
def _read_iccid(app):
"""Best-effort EF.ICCID (MF/2FE2) read: the E.118 digit string or None.
EF.ICCID is a mandatory transparent EF, but a card may protect it or the
generic profile may lack it, so the read is optional and never raises.
The previous selection is restored by the _select_path cleanup."""
if not app or not getattr(app, 'rs', None):
return None
lchan = app.rs.lchan[0]
cleanup = None
try:
_, cleanup = _select_path(lchan, 'MF/2FE2', app)
data, _sw = lchan.read_binary()
return _decode_iccid(data)
except Exception:
return None
finally:
if cleanup:
cleanup()
def _parse_tree_output(output):
lines = (output or '').split('\n')
children = []
@@ -2213,6 +2248,7 @@ def _handle_card_disconnect():
_server_ref.menu_active = False
_server_ref.event_list = None
_server_ref.sim_menu = None
_server_ref.iccid = None
_server_ref.equipping = False
_server_ref.card_session = getattr(_server_ref, 'card_session', 0) + 1
_reset_proactive_log()
@@ -2234,6 +2270,14 @@ def _apply_equipped_card(server):
_CARD_CONNECTED = True
server.card_present = True
server.card_session = getattr(server, 'card_session', 0) + 1
server.iccid = None
# Read the ICCID before the TERMINAL PROFILE starts a CAT session: the
# PWA auto-selects the matching card preset (SCP80 views) from it.
server.iccid = _read_iccid(server.app)
if server.iccid:
_tlog('equip: ICCID %s' % server.iccid)
else:
_tlog('equip: ICCID not readable')
_poll_enable()
sm, el = _send_terminal_profile(server.scc, server.terminal_profile)
server.sim_menu = sm
@@ -3073,6 +3117,7 @@ class PysimHandler(BaseHTTPRequestHandler):
'auto_equip': bool(_AUTO_EQUIP),
'card': card.name if card else None,
'profile': str(rs.profile) if rs and rs.profile else None,
'iccid': getattr(self.server, 'iccid', None) if connected else None,
'app_ready': app is not None,
'adm_verified': rs.adm_verified if rs else False,
'atr': rs.identity.get('ATR') if rs and rs.identity else None,