diff --git a/frontend/index.html b/frontend/index.html
index 8a969ab..2a46ed2 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -1436,7 +1436,7 @@
// ===== Version =====
// Single source of truth for the PWA version: shown in the header and used
// by the server version check in pysimConnect().
-const SIMPLE_VERSION = '2.8.0';
+const SIMPLE_VERSION = '2.8.1';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== Tab switching =====
diff --git a/frontend/sw.js b/frontend/sw.js
index 8c4756c..b60a1fe 100644
--- a/frontend/sw.js
+++ b/frontend/sw.js
@@ -1,4 +1,4 @@
-const CACHE = 'simple-v225';
+const CACHE = 'simple-v226';
const URLS = [
'index.html',
'help.html',
diff --git a/pyproject.toml b/pyproject.toml
index 2974e6f..e4dd4fd 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pysim-simple-server"
-version = "2.8.0"
+version = "2.8.1"
description = "HTTP REST server wrapping pysim for the SIMple PWA"
requires-python = ">=3.8"
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
diff --git a/pysim_simple_server/fastinit.py b/pysim_simple_server/fastinit.py
index 6801dae..1d962f7 100644
--- a/pysim_simple_server/fastinit.py
+++ b/pysim_simple_server/fastinit.py
@@ -80,6 +80,18 @@ def pick_profile_no_reset(scc):
scc.reset_card = original_reset
+def _restore_mf_after_probe(scc):
+ """Probing can leave an ADF (e.g. ISD-R on an eUICC) selected. The
+ RuntimeState construction selects MF by FID, which some cards refuse from
+ within an ADF (6A82), so restore it here: the cheap select keeps the
+ reset-free path for normal cards, the physical reset covers the rest."""
+ try:
+ scc.select_file('3f00')
+ except SwMatchError:
+ sys.stderr.write('FAST-INIT: MF restore after probing failed; physical reset\n')
+ scc.reset_card()
+
+
def init_card_fast(sl, skip_card_init=False, wait=True):
"""Replacement for pySim.app.init_card() that avoids redundant resets.
@@ -112,6 +124,11 @@ def _init_card_once(sl, skip_card_init, wait):
if profile is None:
return None, card
+ # A successful probe may leave an ADF selected (e.g. ISD-R on an eUICC);
+ # RuntimeState selects MF by FID and would fail on cards that refuse that
+ # from within an ADF.
+ _restore_mf_after_probe(scc)
+
if generic_card and isinstance(profile, CardProfileUICC):
card._adm_chv_num = 0x0A
@@ -137,7 +154,9 @@ def _init_card_once(sl, skip_card_init, wait):
except SwMatchError:
pass
finally:
- rs.soft_reset()
+ # rs.reset() tries the reset-free soft reset first and escalates
+ # to a physical reset when MF cannot be selected from the ADF.
+ rs.reset()
return rs, card
diff --git a/pysim_simple_server/server.py b/pysim_simple_server/server.py
index fc25ef1..6e618e3 100644
--- a/pysim_simple_server/server.py
+++ b/pysim_simple_server/server.py
@@ -27,7 +27,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad
-VERSION = '2.8.0'
+VERSION = '2.8.1'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
diff --git a/tests/test_fastinit.py b/tests/test_fastinit.py
index b46eb1e..914968b 100644
--- a/tests/test_fastinit.py
+++ b/tests/test_fastinit.py
@@ -24,17 +24,21 @@ from pysim_simple_server.fastinit import (
class FakeScc:
- def __init__(self):
+ def __init__(self, mf_select_error=False):
self.sel_ctrl = '0004'
self.cla_byte = '00'
self.resets = 0
self.selected = []
+ self.mf_select_error = mf_select_error
def reset_card(self):
self.resets += 1
def select_file(self, fid):
self.selected.append(fid)
+ if fid == '3f00' and self.mf_select_error:
+ # eUICC with the ISD-R ADF selected: MF is not selectable by FID
+ raise SwMatchError('6a82', '9000')
return ('', '9000')
def select_adf(self, aid):
@@ -56,6 +60,19 @@ class TestPickProfileNoReset(unittest.TestCase):
self.assertEqual(scc.resets, 1)
+class TestRestoreMfAfterProbe(unittest.TestCase):
+ def test_normal_card_keeps_the_reset_free_path(self):
+ scc = FakeScc()
+ fastinit._restore_mf_after_probe(scc)
+ self.assertEqual(scc.resets, 0)
+ self.assertEqual(scc.selected[-1], '3f00')
+
+ def test_adf_selected_card_falls_back_to_a_physical_reset(self):
+ scc = FakeScc(mf_select_error=True)
+ fastinit._restore_mf_after_probe(scc)
+ self.assertEqual(scc.resets, 1)
+
+
class FakeLchan:
def __init__(self):
self.scc = types.SimpleNamespace(scp=object())