Запрос GET STATUS для ISD, приложений, ELF и модулей ELF, а также GET DATA FF21 (расширенные ресурсы карты: число установленных приложений и свободная энергонезависимая/энергозависимая память). Компактный листинг содержит только AID, состояние жизненного цикла и привилегии (версии в устаревшем формате нет); если карта отвергает его (P2.b2=0, устаревший по GP), запросы повторяются в расширенном формате E3, а флажок Запрашивать расширенные данные реестра (связи load-файла и SD) дополнительно запрашивает их и объединяет расширенные поля: версию исполняемого load-файла, AID load-файла, AID Security Domain, списки модулей, пункты меню STK апплета (SCP Registry Data, TS 102 226 8.2.1.6) и любые другие объекты реестра, которые вернула карта (показываются как есть, без потерь) — он стоит несколько дополнительных защищённых пакетов на каждый обзор и включён по умолчанию (запоминается в браузере; снимите флажок, чтобы пропустить). Карты различаются в том, что принимают, поэтому расширенный запрос пробуется в трёх формах — эталонный список тегов (с EA по TS 102 226), тот же список без EA и вовсе без списка тегов (форма GlobalPlatformPro) — а обзор сообщает, какая форма сработала и где выполнен откат к компактному списку (версии пакетов тогда недоступны). Если строки ELF всё ещё без версий, формы со списком тегов повторяются для обоих P1=10 и P1=20, пропуская уже отклонённые комбинации, и обход останавливается, как только версия найдена. Результаты отображаются в обзоре (строка успеха минимальна, счётчики — в заголовках разделов) с кнопками Удалить для каждого элемента и определённым форматом команд RAM (подсказка при наведении объясняет compact и expanded).
Установка пакета (.cap файл)
Отправка .cap файла на карту через сервер: INSTALL[for load] → LOAD ×N → INSTALL[for install (+make selectable)]. Load-файл делится на LOAD APDU размера размер блока LOAD (1–240 байт полезной нагрузки, по умолчанию 240); каждый защищённый пакет доставляется одним SMS или, если он больше, конкатенированной SMS-PP загрузкой до 5 сегментов, так что большой .cap просто занимает несколько SMS. Сразу после выбора файл проверяется и в форме показывается оценка требований к памяти: требование NVRAM (образ кода + постоянные данные, сумма в стиле C6+C8 из GP Card Spec) и оценка RAM. Там же показаны библиотеки, с которыми слинкован CAP (его компонент Import: имя библиотеки, AID и минимальная версия ≥ x.y — имена стандартных библиотек и семейства подставляются, а где известно, даётся подсказка о версии Java Card), идентификаторы пакета и апплетов и разбивка по компонентам (размеры и доли в порядке load-файла) — всё сгруппировано по разделам Package, Requires, Memory и Components. Строка Platform показывает минимальный уровень Java Card, который должна предоставить карта (выводится из импорта javacard.framework; флаг int в заголовке дополнительно требует поддержки int — Java Card 2.2+), и релиз ETSI, из которого пришли импорты UICC API (версия uicc.toolkit → REL-N по корпусу sim-tools в рабочем каталоге; основание показано в скобках): JCRE проверяет каждый импорт по своим пакетам при верификации load-файла (JC VM spec 4.5.2), поэтому карта со старой платформой отклоняет LOAD сразу после завершения компонента Import (в строке шага виден remote SW карты и компонент, который завершил блок, например завершает Import): подсказка при ошибке и попап предлагают тест совместимости CAP (в один клик — тот же .cap уже выбран), чтобы найти отсутствующий пакет и версию; меньший размер блока LOAD помогает только если блок превышает буфер карты. Повреждённый файл не проходит анализ, и Execute остаётся недоступной до успешного анализа. Оценка носит справочный характер и никогда не используется при установке: NVRAM — это образ кода плюс постоянные данные (сумма в стиле C6+C8 по GP Table 11-48; управление памятью карты, округление и запись в реестре не учитываются, а образ статических полей входит в обе части); рекомендуемый C6 следует за load-файлом, а рекомендуемые квоты — это десятичные значения в байтах (шестнадцатеричное значение в скобках — это кодирование параметров установки C7/C8: C6 — энергонезависимый код, C7 — энергозависимая RAM, C8 — энергонезависимые данные).
-
Пакетная установка (.cap, по картам)
Массовый сценарий: один .cap и один набор параметров установки, применяемые карта за картой. Запустить пакетную установку не требует карты — цикл ведёт сервер: он ждёт карту, устанавливает сразу после завершения equip и затем ждёт следующую. Для каждой карты пресет находится по ICCID и задаёт keyset (по умолчанию из ролевой записи ISD TAR, v3.26.0), ключи и счётчик; карта без подходящего пресета или без пригодного keyset ISD TAR отмечается и пропускается. Каждая попытка попадает в список со временем, ICCID, пресетом, вердиктом и длительностью; при ошибке показаны шаг, на котором она произошла, и удалённый статусный байт — пакетная установка никогда не повторяет попытку и не останавливается при ошибке (ошибки остаются на ваше усмотрение). Карта обрабатывается один раз за запуск: повторная установка карты даёт «уже обработана» — для повтора перезапустите пакетную установку. Остановить завершает текущую карту (прерванная цепочка LOAD может оставить полузагруженный пакет), а затем завершает запуск; Очистить удаляет отчёт завершённого запуска. Цикл работает на сервере, поэтому закрытие вкладки, переключение представлений или перезагрузка страницы его не прерывают; перезапуск сервера завершает запуск. Пакетная установка и тест-скрипт исключают друг друга.
+
Пакетная установка (.cap на множество карт)
Массовый сценарий: один .cap и один набор параметров установки, применяемые карта за картой. Запустить пакетную установку не требует карты — цикл ведёт сервер: он ждёт карту, устанавливает сразу после завершения equip и затем ждёт следующую. Для каждой карты пресет находится по ICCID и задаёт keyset (по умолчанию из ролевой записи ISD TAR, v3.26.0), ключи и счётчик; карта без подходящего пресета или без пригодного keyset ISD TAR отмечается и пропускается. Каждая попытка попадает в список со временем, ICCID, пресетом, вердиктом и длительностью; при ошибке показаны шаг, на котором она произошла, и удалённый статусный байт — пакетная установка никогда не повторяет попытку и не останавливается при ошибке (ошибки остаются на ваше усмотрение). Карта обрабатывается один раз за запуск: повторная установка карты даёт «уже обработана» — для повтора перезапустите пакетную установку. Остановить завершает текущую карту (прерванная цепочка LOAD может оставить полузагруженный пакет), а затем завершает запуск; Очистить удаляет отчёт завершённого запуска. Цикл работает на сервере, поэтому закрытие вкладки, переключение представлений или перезагрузка страницы его не прерывают; перезапуск сервера завершает запуск. Пакетная установка и тест-скрипт исключают друг друга.
Проверка совместимости CAP
Отвечает на один вопрос, ничего не устанавливая: может ли эта карта удовлетворить список импортов CAP? Сервер выполняет проверку формата, INSTALL[for load] и блоки LOAD только до блока, завершающего компонент Import — именно там JCRE проверяет список импортов — и останавливается: без флага последнего блока и без INSTALL[for install], поэтому ничего не фиксируется и удалять после этого ничего не нужно. Список «Версии импортов для проверки» предзаполняется из анализа CAP: AID=version переписывает запрошенную версию импорта, +AID=version добавляет синтетический импорт (запрос возможностей карты: LOAD проходит, только если карта экспортирует этот пакет версии ≥ указанной), а «Все 0.0» снимает все требования. Использование в два шага: нажмите «Все 0.0» — если проверка проходит, причиной был список импортов; затем возвращайте по одной строке, чтобы найти пакет и версию, которых не хватает карте. Вердикт явно указывает, что проверяется только этап LOAD/импортов: этап связывания (наличие реально вызываемых API) проверяет настоящая установка на INSTALL[for install]. Запуск и результат показываются в окне (как при установке): строки шагов, вердикт и блок деталей по каждому AID — при отказе он называет раздел CAP, который покрывает неудачный блок (диапазон байтов load-файла и компоненты, и завершает ли этот блок Import), SW команды с его значением, список импортов ровно в том виде, в каком он проверялся (версии CAP, правки пробы, добавленные синтетические записи, строки без совпадений) и что означает состояние пробы; при успехе — список импортов, который прошёл проверку.
INSTALL [for install] (загруженный пакет)
Один APDU INSTALL [for install] для уже загруженного на карту пакета (его Load File AID и Module AID, привилегии, параметры установки/STK и необязательное make selectable) — параметры установки можно перебирать, не удаляя и не загружая .cap заново.
INSTALL [for make selectable]
Один APDU INSTALL [for make selectable] (AID экземпляра) для установленного, но невыбираемого экземпляра апплета.
diff --git a/frontend/index.html b/frontend/index.html
index 73d7dd2..6e4b127 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -918,6 +918,7 @@
+
@@ -1909,7 +1910,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 = '3.28.0';
+const SIMPLE_VERSION = '3.28.1';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== view history (the browser Back/Forward buttons) =====
@@ -10216,6 +10217,10 @@ function ramBatchRender() {
// the server refuses a clear while the run is active
const clearBtn = document.getElementById('ram-batch-clear');
if (clearBtn) clearBtn.disabled = active;
+ // A throbber while a card is actually being installed (`current` is set for
+ // the whole install, the stopping phase included).
+ const spinEl = document.getElementById('ram-batch-spin');
+ if (spinEl) spinEl.classList.toggle('hidden', !(active && st.current));
const stateEl = document.getElementById('ram-batch-state');
if (stateEl) {
if (!active) {
@@ -10228,7 +10233,7 @@ function ramBatchRender() {
} else if (phase === 'stopping') {
stateEl.textContent = t('Stopping after the current card...');
} else {
- stateEl.textContent = t('Waiting for a card...');
+ stateEl.textContent = t('Insert next card. Waiting for a card...');
}
}
const notice = document.getElementById('ram-batch-notice');
@@ -24405,14 +24410,14 @@ const LANG_RU = {
'Loading...': 'Загрузка...',
'Close': 'Закрыть',
'Installing package': 'Установка пакета',
- 'Batch install (.cap file, per card)': 'Пакетная установка (.cap, по картам)',
+ 'Batch install (.cap file, per card)': 'Пакетная установка (.cap на множество карт)',
'Start batch': 'Запустить пакетную установку',
'Stopping...': 'Остановка…',
'Batch stopped': 'Пакетная установка остановлена',
'Batch finished': 'Пакетная установка завершена',
'Installing...': 'Установка…',
'Stopping after the current card...': 'Остановка после текущей карты…',
- 'Waiting for a card...': 'Ожидание карты…',
+ 'Insert next card. Waiting for a card...': 'Вставьте следующую карту. Ожидание карты…',
'Installed': 'Установлено',
'Skipped': 'Пропущено',
'attempts': 'попыток',
diff --git a/frontend/sw.js b/frontend/sw.js
index ea18a98..b938eea 100644
--- a/frontend/sw.js
+++ b/frontend/sw.js
@@ -1,4 +1,4 @@
-const CACHE = 'simple-v438';
+const CACHE = 'simple-v439';
const URLS = [
'index.html',
'help.html',
diff --git a/frontend/tests/ram_batch.test.js b/frontend/tests/ram_batch.test.js
index 459d449..573d9cd 100644
--- a/frontend/tests/ram_batch.test.js
+++ b/frontend/tests/ram_batch.test.js
@@ -165,10 +165,12 @@ test('ramBatchRender shows the phase, the counts and the stop button', () => {
setEl('ram-batch-counts', '');
setEl('ram-batch-attempts', '');
setEl('ram-batch-clear', '');
- // waiting for a card
+ setEl('ram-batch-spin', '');
+ // waiting for a card: the explicit prompt, no throbber
app.setLast({ active: true, phase: 'waiting_card', attempts: [], counts: {}, attempts_total: 0, notice: '' });
app.ramBatchRender();
- assert.strictEqual(els['ram-batch-state'].textContent, 'Waiting for a card...');
+ assert.strictEqual(els['ram-batch-state'].textContent, 'Insert next card. Waiting for a card...');
+ assert.ok(els['ram-batch-spin'].classes['hidden']);
assert.ok(els['ram-batch-stop'].classes['hidden'] === undefined);
assert.ok(els['ram-batch-start'].classes['hidden']);
assert.ok(els['ram-batch-clear'].disabled); // the server refuses a clear while active
@@ -177,12 +179,24 @@ test('ramBatchRender shows the phase, the counts and the stop button', () => {
attempts_total: 3, notice: 'Card 897… installed.', current: { iccid: '8970119000004600098', preset: 'Alfa' } });
app.ramBatchRender();
assert.match(els['ram-batch-state'].textContent, /Installing\.\.\. 8970119000004600098 — Alfa/);
+ assert.ok(els['ram-batch-spin'].classes['hidden'] === undefined); // the install throbber
assert.match(els['ram-batch-counts'].textContent, /Installed: 2 · Failed: 1 · Skipped: 0 · 3 attempts/);
assert.strictEqual(els['ram-batch-notice'].textContent, 'Card 897… installed.');
- // stopping
- app.setLast({ active: true, phase: 'stopping', attempts: [], counts: {}, attempts_total: 0 });
+ // a new install clears the previous notice: an empty one hides the box
+ app.setLast({ active: true, phase: 'installing', attempts: [], counts: {}, attempts_total: 3,
+ notice: '', current: { iccid: '8970119000004600106' } });
app.ramBatchRender();
+ assert.ok(els['ram-batch-notice'].classes['hidden']);
+ // stopping while a card is still being installed: the throbber stays
+ app.setLast({ active: true, phase: 'stopping', attempts: [], counts: {}, attempts_total: 0,
+ current: { iccid: '8970119000004600098' } });
+ app.ramBatchRender();
+ assert.ok(els['ram-batch-spin'].classes['hidden'] === undefined);
assert.match(els['ram-batch-state'].textContent, /Stopping after the current card\.\.\./);
+ // ...but not once the install finished (stopping while waiting)
+ app.setLast({ active: true, phase: 'stopping', attempts: [], counts: {}, attempts_total: 0, current: null });
+ app.ramBatchRender();
+ assert.ok(els['ram-batch-spin'].classes['hidden']);
assert.strictEqual(els['ram-batch-stop'].textContent, 'Stopping...');
assert.ok(els['ram-batch-stop'].disabled);
// stopped
@@ -248,7 +262,7 @@ test('the panel headline follows the live summary (phase and step)', () => {
// ...and back to waiting when the card is gone
app.ramBatchApplyStatus({ ram_batch: { active: true, phase: 'waiting_card', attempts_total: 0,
counts: { installed: 0, failed: 0, skipped: 0 } } });
- assert.strictEqual(els['ram-batch-state'].textContent, 'Waiting for a card...');
+ assert.strictEqual(els['ram-batch-state'].textContent, 'Insert next card. Waiting for a card...');
});
test('the stop and clear calls hit their endpoints', async () => {
diff --git a/pyproject.toml b/pyproject.toml
index 301285b..584101d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pysim-simple-server"
-version = "3.28.0"
+version = "3.28.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/server.py b/pysim_simple_server/server.py
index 8a755b0..87f7ee5 100644
--- a/pysim_simple_server/server.py
+++ b/pysim_simple_server/server.py
@@ -37,7 +37,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad
-VERSION = '3.28.0'
+VERSION = '3.28.1'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
@@ -2637,6 +2637,10 @@ def _ram_batch_handle_card(state, server):
state['seen'][iccid] = 'pending'
state['current'] = {'iccid': iccid, 'preset': name, 'started': time.time()}
state['phase'] = 'installing'
+ # A new card's install supersedes the previous attempt's headline: the
+ # notice reports the *last finished* card, so it must not linger while
+ # the next card is being installed (v3.28.1).
+ state['notice'] = ''
started = time.time()
result = None
error = None
diff --git a/tests/test_ram_batch.py b/tests/test_ram_batch.py
index 57826ab..50d3a9c 100644
--- a/tests/test_ram_batch.py
+++ b/tests/test_ram_batch.py
@@ -205,6 +205,27 @@ class BatchCardTests(unittest.TestCase):
self.assertEqual(st['counts']['failed'], 1)
self.assertEqual(st['seen']['8970119000004600098'], 'failed')
+ def test_a_new_install_clears_the_previous_notice(self):
+ """The notice reports the last *finished* card: starting the next
+ card's install must clear it (v3.28.1)."""
+ st = srv._ram_batch_new_state(_plan())
+ srv._ram_batch_record(st, {'iccid': '8970119000004600000', 'result': 'installed'})
+ self.assertIn('installed', st['notice'])
+ preset = _preset('8970119000004600098', 'Alfa')
+ seen = {}
+
+ def fake_run(server, body, plan, scc):
+ seen['notice'] = st['notice'] # what the panel would render
+ seen['current'] = st['current']
+ return {'success': True}
+
+ with mock.patch.object(srv, '_ram_install_run', fake_run):
+ srv._ram_batch_handle_card(st, _fake_server('8970119000004600098',
+ _FakeStore([preset])))
+ self.assertEqual(seen['notice'], '') # cleared while installing
+ self.assertEqual(seen['current']['iccid'], '8970119000004600098')
+ self.assertIn('installed', st['notice']) # ...and set for the new outcome
+
def test_a_card_already_processed_is_reported_not_reinstalled(self):
st = srv._ram_batch_new_state(_plan())
preset = _preset('8970119000004600098', 'Alfa')