feat: polling on by default and an effective-state toggle (v3.5.10)

- Background STATUS polling is enabled by default, per the spec's idle
  polling rule (TS 102 221 14.6.2): `_POLL_ENABLED = True`, __main__ runs
  `_poll_enable()` regardless of card presence (--poll-interval 0 still
  disables) and `_do_status_poll` keeps ticking while enabled even without
  a session, so a cardless start resumes as soon as a card appears.
- A new card session clears a POLLING OFF from the previous card in
  `_apply_equipped_card` before re-enabling polling.
- The Phone-tab toggle now shows the *effective* state: OFF in amber
  ("card disabled polling (POLLING OFF)") while the card suspended
  proactive polling, back to ON on the next POLL INTERVAL.  The 5 s
  background poll now passes `card_disabled` through (it was dropped, so
  an autonomously received POLLING OFF never reached the UI).
- Tests: poll_ui updated (button OFF while suspended, warning path),
  test_poll +2 (cardless ticking, module default via a subprocess check).
- Help EN/RU, docs/api.md, AGENTS; version 3.5.10; sw cache simple-v263.

573 frontend / 441 Python green.
This commit is contained in:
2026-09-26 13:09:44 +03:00
parent 25f001a778
commit 4c3fa64c95
10 changed files with 70 additions and 26 deletions
+6 -3
View File
@@ -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}
+1 -1
View File
@@ -504,7 +504,7 @@
<p class="text-sm mb-3">Блок <strong>TERMINAL PROFILE</strong> (рядом с «STATUS и опрос») содержит кнопки <strong>«Отправить»</strong> (повторная отправка, как «Спасение») и <strong>«Настроить»</strong>. В диалоге настройки: селектор пресетов (модели устройств, например профиль BIP-совместимого аппарата из этого проекта), поле hex и форма с флажком на каждый бит профиля по <strong>TS 102 223 §5.2</strong> / <strong>TS 31.111 §5.2</strong> (байты 1&ndash;39; для битов 3GPP используются имена из TS 31.111); каждый байт — вертикальный список битов в порядке спецификации (<strong>b1 — младший бит, первая строка таблицы</strong>); биты, образующие числовое поле по спецификации (байт 11 — число soft keys, байт 13 — число BIP-каналов, байты 14/15 — высота/ширина экрана, байт 16 — уменьшение ширины в меню, байт 19 — версия TIA/EIA-136, байт 24 — максимум кадров), показываются одним числовым полем на весь диапазон битов, а в подсказке байта 11 отмечено зарезервированное значение <code class="font-mono text-sm">'FF'</code>; блоки размещены фиксированными группами (байты 1–12 в 2 колонки, 13–16 в 4, 17–18 в 2, 19–21 в 3, 22–25 в 2, 26–28 в 3, 29–30 в 2, далее по одному в строке); переключение бита обновляет hex, а правка hex перерисовывает форму &mdash; поле hex основное, неизвестные байты и биты сохраняются. Кнопка <strong>«Применить»</strong> отправляет новое значение на сервер (и далее на карту), сбрасывая STK-сессию как «Спасение»; изменение хранится только в памяти (значение <code class="font-mono text-sm">--terminal-profile</code> — стартовое по умолчанию). Правила вывода характеристик устройства из TP (поколение связи, бездисплейные модемы, семейство стандартов) собраны в <code class="font-mono text-sm">docs/TERMINAL_PROFILE.md</code>.</p>
<h3 id="status-polling" class="text-lg font-medium mb-2">8.5 Опрос STATUS</h3>
<p class="text-sm mb-3">Кнопка <strong>Отправить STATUS</strong> отправляет STATUS (F2) вручную. Переключатель <strong>Опрос</strong> включает фоновый опрос: после настраиваемого интервала бездействия (аргумент сервера <code class="font-mono text-sm">--poll-interval</code>, 1&ndash;255&nbsp;с, по умолчанию 30&nbsp;с, <code class="font-mono text-sm">0</code> отключает опрос) сервер отправляет STATUS и обрабатывает любую ожидающую проактивную команду. Команда карты <strong>POLL INTERVAL</strong> обновляет интервал (TS 102 223 &sect;6.4.6; TERMINAL RESPONSE сообщает фактически используемый интервал), а <strong>POLLING OFF</strong> приостанавливает опрос до нового POLL INTERVAL &mdash; состояние показывается янтарным цветом, при этом ручная кнопка <strong>Отправить STATUS</strong> продолжает работать (на presence detection и ручной STATUS POLLING OFF не влияет). При извлечении карты опрос останавливается, а состояние карты сбрасывается.</p>
<p class="text-sm mb-3">Кнопка <strong>Отправить STATUS</strong> отправляет STATUS (F2) вручную. Фоновый <strong>опрос включён по умолчанию</strong> (CAT-терминал опрашивает карту в режиме ожидания, TS 102 221 &sect;14.6.2): после интервала бездействия (аргумент сервера <code class="font-mono text-sm">--poll-interval</code>, 1&ndash;255&nbsp;с, по умолчанию 30&nbsp;с, <code class="font-mono text-sm">0</code> отключает опрос) сервер отправляет STATUS и обрабатывает любую ожидающую проактивную команду. Переключатель включает и выключает опрос; команда карты <strong>POLL INTERVAL</strong> обновляет интервал (TS 102 223 &sect;6.4.6; TERMINAL RESPONSE сообщает фактически используемый интервал), а после <strong>POLLING OFF</strong> переключатель показывает <strong>OFF</strong> янтарным цветом до нового POLL INTERVAL &mdash; ручная кнопка <strong>Отправить STATUS</strong> продолжает работать (на presence detection и ручной STATUS POLLING OFF не влияет). При извлечении карты опрос отключается и включается снова при следующем equip.</p>
<h3 id="pli-dict" class="text-lg font-medium mb-2">8.6 &laquo;Конфигурация TR&raquo; &mdash; данные ответа PROVIDE LOCAL INFORMATION</h3>
<p class="text-sm mb-2">Редактируемые hex-значения для всех 22 квалификаторов PLI (TS 102 223 &sect;8.6 + TS 131 111). У десяти квалификаторов есть встроенные формы декодирования/кодирования:</p>
+1 -1
View File
@@ -503,7 +503,7 @@
<p class="text-sm mb-3">The <strong>TERMINAL PROFILE</strong> block (next to STATUS and Polling) offers <strong>Send</strong> (re-sends it, like Rescue) and <strong>Configure</strong>. 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 <strong>TS 102 223 &sect;5.2</strong> / <strong>TS 31.111 &sect;5.2</strong> (bytes 1&ndash;39, 3GPP-defined bits use the TS 31.111 names), each byte is a vertical list of its bits in spec order (<strong>b1 is the least significant bit and the first table row</strong>); 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 <code class="font-mono text-sm">'FF'</code> 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 &mdash; the hex field is authoritative and unknown bytes/bits are preserved. <strong>Apply</strong> 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 <code class="font-mono text-sm">--terminal-profile</code> CLI value is the startup default). The rules for inferring device characteristics from a TP (radio generation, headless modems, standards family) are collected in <code class="font-mono text-sm">docs/TERMINAL_PROFILE.md</code>.</p>
<h3 id="status-polling" class="text-lg font-medium mb-2">8.5 STATUS polling</h3>
<p class="text-sm mb-3">A <strong>Send STATUS</strong> button issues a manual STATUS (F2). A <strong>Polling</strong> toggle enables background polling: after a configurable idle interval (server CLI <code class="font-mono text-sm">--poll-interval</code>, 1&ndash;255&nbsp;s, default 30&nbsp;s, <code class="font-mono text-sm">0</code> disables polling) the server sends STATUS and handles any pending proactive command. The card's <strong>POLL INTERVAL</strong> command updates the interval (TS 102 223 &sect;6.4.6; the TERMINAL RESPONSE reports the interval actually used), and a <strong>POLLING OFF</strong> suspends polling until a new POLL INTERVAL &mdash; the panel shows that state in amber while the manual <strong>Send STATUS</strong> 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.</p>
<p class="text-sm mb-3">A <strong>Send STATUS</strong> button issues a manual STATUS (F2). Background <strong>Polling</strong> is on by default (a CAT terminal polls during idle, TS 102 221 &sect;14.6.2): after the idle interval (server CLI <code class="font-mono text-sm">--poll-interval</code>, 1&ndash;255&nbsp;s, default 30&nbsp;s, <code class="font-mono text-sm">0</code> disables polling) the server sends STATUS and handles any pending proactive command. The toggle turns it off/on; the card's <strong>POLL INTERVAL</strong> command updates the interval (TS 102 223 &sect;6.4.6; the TERMINAL RESPONSE reports the interval actually used), and after a <strong>POLLING OFF</strong> the toggle shows <strong>OFF</strong> in amber until a new POLL INTERVAL arrives &mdash; the manual <strong>Send STATUS</strong> 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.</p>
<h3 id="pli-dict" class="text-lg font-medium mb-2">8.6 TR Config &mdash; PROVIDE LOCAL INFORMATION response data</h3>
<p class="text-sm mb-2">Editable hex values for all 22 PLI qualifiers (TS 102 223 &sect;8.6 + TS 131 111). Ten qualifiers have inline decode/encode forms:</p>
+10 -5
View File
@@ -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);
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v262';
const CACHE = 'simple-v263';
const URLS = [
'index.html',
'help.html',
+5 -3
View File
@@ -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');
});
+1 -1
View File
@@ -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.
+7 -2
View File
@@ -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
+18 -9
View File
@@ -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
+20
View File
@@ -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)')