diff --git a/docs/api.md b/docs/api.md index d7e85f4..5df9fd1 100644 --- a/docs/api.md +++ b/docs/api.md @@ -770,9 +770,12 @@ Hex, even number of digits, 1–255 bytes. Response is the same shape as ### `GET /api/poll-status` -Background STATUS polling state. `card_disabled` is true after the card sent -POLLING OFF (TS 102 223 §6.4.14): proactive polling stays suspended until a -POLL INTERVAL arrives, independent of the operator's `enabled` switch. +Background STATUS polling state. Polling is enabled by default (a CAT +terminal polls during idle, TS 102 221 §14.6.2; `--poll-interval 0` disables +it at startup) and `enabled` is the operator's switch. `card_disabled` is true +after the card sent POLLING OFF (TS 102 223 §6.4.14): proactive polling stays +suspended until a POLL INTERVAL arrives, independent of `enabled` - the PWA +shows the effective state (OFF while suspended). ```json {"enabled": true, "interval": 30, "card_disabled": false} diff --git a/frontend/help-ru.html b/frontend/help-ru.html index 196fad8..891be84 100644 --- a/frontend/help-ru.html +++ b/frontend/help-ru.html @@ -504,7 +504,7 @@
Блок TERMINAL PROFILE (рядом с «STATUS и опрос») содержит кнопки «Отправить» (повторная отправка, как «Спасение») и «Настроить». В диалоге настройки: селектор пресетов (модели устройств, например профиль BIP-совместимого аппарата из этого проекта), поле hex и форма с флажком на каждый бит профиля по TS 102 223 §5.2 / TS 31.111 §5.2 (байты 1–39; для битов 3GPP используются имена из TS 31.111); каждый байт — вертикальный список битов в порядке спецификации (b1 — младший бит, первая строка таблицы); биты, образующие числовое поле по спецификации (байт 11 — число soft keys, байт 13 — число BIP-каналов, байты 14/15 — высота/ширина экрана, байт 16 — уменьшение ширины в меню, байт 19 — версия TIA/EIA-136, байт 24 — максимум кадров), показываются одним числовым полем на весь диапазон битов, а в подсказке байта 11 отмечено зарезервированное значение 'FF'; блоки размещены фиксированными группами (байты 1–12 в 2 колонки, 13–16 в 4, 17–18 в 2, 19–21 в 3, 22–25 в 2, 26–28 в 3, 29–30 в 2, далее по одному в строке); переключение бита обновляет hex, а правка hex перерисовывает форму — поле hex основное, неизвестные байты и биты сохраняются. Кнопка «Применить» отправляет новое значение на сервер (и далее на карту), сбрасывая STK-сессию как «Спасение»; изменение хранится только в памяти (значение --terminal-profile — стартовое по умолчанию). Правила вывода характеристик устройства из TP (поколение связи, бездисплейные модемы, семейство стандартов) собраны в docs/TERMINAL_PROFILE.md.
Кнопка Отправить STATUS отправляет STATUS (F2) вручную. Переключатель Опрос включает фоновый опрос: после настраиваемого интервала бездействия (аргумент сервера --poll-interval, 1–255 с, по умолчанию 30 с, 0 отключает опрос) сервер отправляет STATUS и обрабатывает любую ожидающую проактивную команду. Команда карты POLL INTERVAL обновляет интервал (TS 102 223 §6.4.6; TERMINAL RESPONSE сообщает фактически используемый интервал), а POLLING OFF приостанавливает опрос до нового POLL INTERVAL — состояние показывается янтарным цветом, при этом ручная кнопка Отправить STATUS продолжает работать (на presence detection и ручной STATUS POLLING OFF не влияет). При извлечении карты опрос останавливается, а состояние карты сбрасывается.
Кнопка Отправить STATUS отправляет STATUS (F2) вручную. Фоновый опрос включён по умолчанию (CAT-терминал опрашивает карту в режиме ожидания, TS 102 221 §14.6.2): после интервала бездействия (аргумент сервера --poll-interval, 1–255 с, по умолчанию 30 с, 0 отключает опрос) сервер отправляет STATUS и обрабатывает любую ожидающую проактивную команду. Переключатель включает и выключает опрос; команда карты POLL INTERVAL обновляет интервал (TS 102 223 §6.4.6; TERMINAL RESPONSE сообщает фактически используемый интервал), а после POLLING OFF переключатель показывает OFF янтарным цветом до нового POLL INTERVAL — ручная кнопка Отправить STATUS продолжает работать (на presence detection и ручной STATUS POLLING OFF не влияет). При извлечении карты опрос отключается и включается снова при следующем equip.
Редактируемые hex-значения для всех 22 квалификаторов PLI (TS 102 223 §8.6 + TS 131 111). У десяти квалификаторов есть встроенные формы декодирования/кодирования:
diff --git a/frontend/help.html b/frontend/help.html index 3fe44fa..6cf999b 100644 --- a/frontend/help.html +++ b/frontend/help.html @@ -503,7 +503,7 @@The TERMINAL PROFILE block (next to STATUS and Polling) offers Send (re-sends it, like Rescue) and Configure. The Configure dialog has a preset selector (device models, e.g. this project's BIP-capable handset profile), a hex field and a form with one checkbox per profile bit decoded per TS 102 223 §5.2 / TS 31.111 §5.2 (bytes 1–39, 3GPP-defined bits use the TS 31.111 names), each byte is a vertical list of its bits in spec order (b1 is the least significant bit and the first table row); bits that form a numeric value field per the spec (byte 11 soft keys, byte 13 BIP channels, byte 14/15 screen height/width, byte 16 menu width reduction, byte 19 TIA/EIA-136 version, byte 24 max frames) are shown as a single number input for the whole bit range, with the byte 11 'FF' reserved value noted in the hint; placed in fixed column groups (bytes 1-12 in 2 columns, 13-16 in 4, 17-18 in 2, 19-21 in 3, 22-25 in 2, 26-28 in 3, 29-30 in 2, later bytes one per row); toggling a bit updates the hex and editing the hex re-renders the form — the hex field is authoritative and unknown bytes/bits are preserved. Apply sends the new value to the server (and on to the card), resetting the STK session like Rescue; the change is in-memory only (the --terminal-profile CLI value is the startup default). The rules for inferring device characteristics from a TP (radio generation, headless modems, standards family) are collected in docs/TERMINAL_PROFILE.md.
A Send STATUS button issues a manual STATUS (F2). A Polling toggle enables background polling: after a configurable idle interval (server CLI --poll-interval, 1–255 s, default 30 s, 0 disables polling) the server sends STATUS and handles any pending proactive command. The card's POLL INTERVAL command updates the interval (TS 102 223 §6.4.6; the TERMINAL RESPONSE reports the interval actually used), and a POLLING OFF suspends polling until a new POLL INTERVAL — the panel shows that state in amber while the manual Send STATUS button keeps working (presence detection and manual STATUS are not affected by POLLING OFF). Polling stops and card state resets if the card is removed.
A Send STATUS button issues a manual STATUS (F2). Background Polling is on by default (a CAT terminal polls during idle, TS 102 221 §14.6.2): after the idle interval (server CLI --poll-interval, 1–255 s, default 30 s, 0 disables polling) the server sends STATUS and handles any pending proactive command. The toggle turns it off/on; the card's POLL INTERVAL command updates the interval (TS 102 223 §6.4.6; the TERMINAL RESPONSE reports the interval actually used), and after a POLLING OFF the toggle shows OFF in amber until a new POLL INTERVAL arrives — the manual Send STATUS button keeps working (presence detection and manual STATUS are not affected by POLLING OFF). Polling is disabled when the card is removed and re-enabled by the next equip.
Editable hex values for all 22 PLI qualifiers (TS 102 223 §8.6 + TS 131 111). Ten qualifiers have inline decode/encode forms:
diff --git a/frontend/index.html b/frontend/index.html index ef6c8c2..104238f 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1560,7 +1560,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.5.9'; +const SIMPLE_VERSION = '3.5.10'; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; // ===== Tab switching ===== @@ -9986,7 +9986,12 @@ function pysimUpdatePollUI(enabled, interval, cardDisabled, warning) { const btn = document.getElementById('pli-pause-btn'); const stat = document.getElementById('pli-poll-stat'); if (!btn) return; - if (enabled) { + // The button shows whether STATUS is actually being polled: while the card + // has disabled proactive polling (POLLING OFF, TS 102 223 6.4.14) it shows + // OFF even though the operator's switch is still on - polling resumes + // automatically on the next POLL INTERVAL. + const effective = !!enabled && !cardDisabled; + if (effective) { btn.textContent = 'ON'; btn.classList.remove('bg-gray-200', 'dark:bg-slate-700', 'text-gray-700', 'dark:text-slate-300'); btn.classList.add('bg-emerald-600', 'text-white'); @@ -9997,9 +10002,9 @@ function pysimUpdatePollUI(enabled, interval, cardDisabled, warning) { } if (stat) { stat.className = 'text-xs ' + (cardDisabled ? 'text-amber-600 dark:text-amber-400' : 'text-gray-400'); - if (cardDisabled) stat.textContent = t('card disabled polling (POLLING OFF)'); + if (cardDisabled) stat.textContent = warning || t('card disabled polling (POLLING OFF)'); else if (warning) stat.textContent = warning; - else stat.textContent = enabled ? '(' + interval + 's)' : ''; + else stat.textContent = effective ? '(' + interval + 's)' : ''; } } @@ -10086,7 +10091,7 @@ function pysimStartBackendPoll() { pysimFetch('/api/stk-status'), pysimFetch('/api/poll-status'), ]); - pysimUpdatePollUI(ps.enabled, ps.interval); + pysimUpdatePollUI(ps.enabled, ps.interval, ps.card_disabled, ps.warning); if (pysimStkStatusChanged(stk) && isViewVisible('tab-phone')) stkCheckMenu(); } catch (e) { /* ignore */ } }, 5000); diff --git a/frontend/sw.js b/frontend/sw.js index 23e3ab9..729ee9c 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v262'; +const CACHE = 'simple-v263'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/poll_ui.test.js b/frontend/tests/poll_ui.test.js index 48f96de..4efe348 100644 --- a/frontend/tests/poll_ui.test.js +++ b/frontend/tests/poll_ui.test.js @@ -41,16 +41,18 @@ test('pysimUpdatePollUI shows the interval while polling is on', () => { assert.ok(!els['pli-poll-stat'].className.includes('amber')); }); -test('pysimUpdatePollUI shows the card-disabled state in amber', () => { +test('pysimUpdatePollUI shows the card-disabled state as OFF in amber', () => { const els = setup(); pysimUpdatePollUI(true, 30, true); + assert.strictEqual(els['pli-pause-btn'].textContent, 'OFF'); assert.ok(els['pli-poll-stat'].textContent.includes('POLLING OFF'), els['pli-poll-stat'].textContent); assert.ok(els['pli-poll-stat'].className.includes('text-amber-600')); }); -test('pysimUpdatePollUI shows a server warning', () => { +test('pysimUpdatePollUI shows the server warning while suspended', () => { const els = setup(); - pysimUpdatePollUI(true, 30, false, 'card disabled proactive polling'); + pysimUpdatePollUI(true, 30, true, 'card disabled proactive polling'); + assert.strictEqual(els['pli-pause-btn'].textContent, 'OFF'); assert.strictEqual(els['pli-poll-stat'].textContent, 'card disabled proactive polling'); }); diff --git a/pyproject.toml b/pyproject.toml index 740c892..2a04266 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pysim-simple-server" -version = "3.5.9" +version = "3.5.10" 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/__main__.py b/pysim_simple_server/__main__.py index 369e0e4..9941939 100644 --- a/pysim_simple_server/__main__.py +++ b/pysim_simple_server/__main__.py @@ -287,8 +287,13 @@ def main(): pysim_simple_server.server._CARD_CONNECTED = card is not None if opts.poll_interval is not None: pysim_simple_server.server._set_poll_interval(opts.poll_interval) - # Auto-enable polling if card initialized successfully (unless interval is 0) - if server.scc and server.card and opts.poll_interval != 0: + # Background STATUS polling is on by default: a CAT terminal polls during + # idle (TS 102 221 14.6.2). --poll-interval 0 disables it, and a card + # that wants no polling suspends it with POLLING OFF (TS 102 223 6.4.14) + # until a new POLL INTERVAL. + if opts.poll_interval == 0: + pysim_simple_server.server._poll_disable() + else: pysim_simple_server.server._poll_enable() # Start presence monitoring only after the startup init: pyscard reports an # already-present card as "added" on the first pass, and we must not diff --git a/pysim_simple_server/server.py b/pysim_simple_server/server.py index dcfe352..12c4c38 100644 --- a/pysim_simple_server/server.py +++ b/pysim_simple_server/server.py @@ -30,7 +30,7 @@ from osmocom.tlv import BER_TLV_IE from osmocom.utils import rpad -VERSION = '3.5.9' +VERSION = '3.5.10' MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE @@ -1324,7 +1324,11 @@ _SCP81_TARGET = None _SCP81_PSKS = {} _SCP81_PSK_LEGACY = None -_POLL_ENABLED = False +# Background STATUS polling is on by default: a CAT terminal shall poll +# during idle at the negotiated (or default) interval (TS 102 221 14.6.2). +# The operator can turn it off, --poll-interval 0 disables it and a card can +# suspend it with POLLING OFF (see _POLL_DISABLED_BY_CARD). +_POLL_ENABLED = True _POLL_INTERVAL = 30 _POLL_TIMER = None # Set when the card sent POLLING OFF (TS 102 223 6.4.14): proactive polling @@ -1418,15 +1422,17 @@ def _do_status_poll(): with _CARD_LOCK: try: scc = getattr(_server_ref, 'scc', None) if _server_ref else None - if not scc: - return - st_data, st_sw = _send_status(scc) - sys.stderr.write('AUTO-STATUS -> %s\n' % st_sw) - if st_sw.startswith('91'): - _handle_proactive_chain(scc, st_sw) + if scc: + st_data, st_sw = _send_status(scc) + sys.stderr.write('AUTO-STATUS -> %s\n' % st_sw) + if st_sw.startswith('91'): + _handle_proactive_chain(scc, st_sw) except Exception as e: sys.stderr.write('AUTO-STATUS error: %s\n' % e) _handle_card_disconnect(stale=_is_transport_fatal(e)) + # Keep ticking while enabled even without a session (a cardless start or + # the window between removal and the next equip); _handle_card_disconnect + # disables polling, which stops the chain. _reset_poll_timer() def _poll_enable(): @@ -2964,7 +2970,7 @@ def _ensure_transport(server): def _apply_equipped_card(server): """Common post-equip state refresh + TERMINAL PROFILE, shared by the /api/command equip branch and the auto-equip worker.""" - global _CARD_CONNECTED + global _CARD_CONNECTED, _POLL_DISABLED_BY_CARD server.stk_pending = None server.menu_active = False _cancel_menu_timeout() @@ -2990,6 +2996,9 @@ def _apply_equipped_card(server): else: server.net_state = None _tlog('equip: ICCID not readable - network state skipped') + # A new card session starts with polling allowed; a POLLING OFF from the + # previous card does not survive the swap. + _POLL_DISABLED_BY_CARD = False _poll_enable() sm, el = _send_terminal_profile(server.scc, server.terminal_profile) server.sim_menu = sm diff --git a/tests/test_poll.py b/tests/test_poll.py index 66e3d66..6f185ff 100644 --- a/tests/test_poll.py +++ b/tests/test_poll.py @@ -129,6 +129,26 @@ class TestCardDrivenPolling(unittest.TestCase): S._apply_poll_negotiation(S._decode_poll_negotiation('11010104020102')) self.assertEqual(S._POLL_INTERVAL, 30) + def test_poll_keeps_ticking_without_a_card(self): + S._POLL_ENABLED = True + S._POLL_DISABLED_BY_CARD = False + S._set_poll_interval(30) + saved_ref = S._server_ref + S._server_ref = None + try: + with mock.patch.object(S.threading, 'Timer') as timer: + S._do_status_poll() + timer.assert_called_once_with(30, S._do_status_poll) + finally: + S._server_ref = saved_ref + + def test_polling_is_enabled_by_default(self): + import subprocess + code = 'import pysim_simple_server.server as s; print(s._POLL_ENABLED)' + rv = subprocess.run([sys.executable, '-c', code], capture_output=True, text=True, + cwd=str(PROJECTS / 'simple')) + self.assertEqual(rv.stdout.strip(), 'True', rv.stderr) + def test_poll_interval_decoded_in_the_proactive_log(self): fields = S._decode_cmd(0x03, bytes.fromhex(MIN_2_TLV), 0) self.assertEqual(fields[0]['value'], '2 min (120 s)')