feat: card-driven STATUS polling and the Poll Interval Negotiation event (v3.5.9)
- EVENT_NAMES (server + PWA) corrected against TS 102 223 v18.3.0 8.25: 0x14 is "Access technology change (multiple)" (not Change of UICC Access), 0x19 Profile container, 0x1A Void, 0x1B Secured profile container, 0x1C Poll interval negotiation, 0x20-0x22 reserved. Values the CAT spec leaves "Reserved for 3GPP" now carry the concrete TS 31.111 event name + clause (0x11 (I-)WLAN access status, 0x12 Network rejection, 0x15 CSG cell selection, 0x17 IMS registration, 0x18 Incoming IMS data, 0x1D Data connection status change, 0x1E CAG cell selection, 0x1F Slices status change). - Poll Interval Negotiation (0x1C) in the Phone tab: the form proposes a Duration (unit + interval) and the UICC's answer (TS 102 223 8.97 accepted / rejected / modified + optional Duration) is decoded and shown; a "modified" duration becomes the background poll interval. The form appears when the card subscribed to the event, like every other. - Polling emulation is card-driven: a POLL INTERVAL adopts its Duration (minutes/seconds/tenths -> 1..255 s, logged) for the background poll, is echoed in the TERMINAL RESPONSE (6.8.4) and clears a POLLING OFF suspension; POLLING OFF (6.4.14) suspends proactive polling until a new POLL INTERVAL - the manual Send STATUS button and presence detection are unaffected. /api/poll-status and /api/poll-toggle report card_disabled (+ a warning when enabling while suspended); the PWA shows it in amber. - Tests: tests/test_poll.py +7, test_proactive_names.py +3, frontend event_forms +2 and poll_ui (4). Help EN/RU, docs/api.md, AGENTS. 573 frontend / 439 Python green; version 3.5.9; sw cache simple-v262.
This commit is contained in:
+22
-3
@@ -616,6 +616,21 @@ optional hex for events that carry data. Returns the SW and any response data:
|
||||
{"sw": "9000", "data": "..."}
|
||||
```
|
||||
|
||||
For **Poll Interval Negotiation** (event `0x1C`, TS 102 223 §7.5.22) the
|
||||
proposal rides in `event_data` as the Duration TLV `04 02 <unit> <interval>`
|
||||
(unit `00` minutes / `01` seconds / `02` tenths of seconds) and the UICC's
|
||||
response is decoded into `negotiation`:
|
||||
|
||||
```json
|
||||
{"sw": "9000", "data": "1101020402011E",
|
||||
"negotiation": {"result": 2, "result_name": "modified",
|
||||
"unit": 1, "interval": 30, "seconds": 30}}
|
||||
```
|
||||
|
||||
`result` is `00` accepted / `01` rejected / `02` modified (§8.97); no response
|
||||
data means *accepted*. A *modified* answer sets the background poll interval,
|
||||
and a *rejected* answer may carry the closest acceptable duration.
|
||||
|
||||
Channel status (event `0x0A`, TS 102 223 §8.56) carries the Channel status TLV
|
||||
`B8 02 <status> <info>`, where the status byte is the channel id (1–7) OR-ed
|
||||
with the state bits (0x00 link not established / 0x40 TCP LISTEN / 0x80 link
|
||||
@@ -755,10 +770,12 @@ Hex, even number of digits, 1–255 bytes. Response is the same shape as
|
||||
|
||||
### `GET /api/poll-status`
|
||||
|
||||
Background STATUS polling state.
|
||||
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.
|
||||
|
||||
```json
|
||||
{"enabled": true, "interval": 300}
|
||||
{"enabled": true, "interval": 30, "card_disabled": false}
|
||||
```
|
||||
|
||||
### `POST /api/poll-toggle`
|
||||
@@ -769,7 +786,9 @@ Turns background STATUS polling on or off.
|
||||
{"enabled": true}
|
||||
```
|
||||
|
||||
Returns the new state (`{"enabled": ..., "interval": ...}`).
|
||||
Returns the new state (`{"enabled": ..., "interval": ..., "card_disabled": ...}`);
|
||||
while the card has disabled polling, enabling adds a `warning` explaining
|
||||
that the timer stays suspended until the card sends a new POLL INTERVAL.
|
||||
|
||||
### `GET /api/pli-qualifiers`
|
||||
|
||||
|
||||
@@ -493,6 +493,7 @@
|
||||
<li><strong>Location Status</strong> — выпадающий список: Normal / Limited / No service (тег <code class="font-mono text-sm">9B</code>)</li>
|
||||
<li><strong>Access Technology Change</strong> — 13 типов RAT (тег <code class="font-mono text-sm">BF</code>)</li>
|
||||
<li><strong>Network Rejection</strong> — полная адаптивная форма: тип регистрации (LU / GPRS / EPS / 5GS), поля местоположения (MCC, MNC, LAC, RAC, TAC), технология доступа и единый выпадающий список из 53 кодов причин (EMM, GMM, 5GMM и LU)</li>
|
||||
<li><strong>Poll Interval Negotiation</strong> (0x1C) — предлагает интервал опроса в виде Duration (единица + значение); после отправки показывается ответ UICC (принято / отклонено / изменено + длительность), а значение <em>изменено</em> становится интервалом фонового опроса (TS 102 223 §7.5.22). Форма появляется, когда карта подписана на это событие.</li>
|
||||
</ul>
|
||||
<p class="text-sm mb-3">Отправка события использует <code class="font-mono text-sm">ENVELOPE(Event Download)</code> по TS 102 223 / TS 131 111.</p>
|
||||
|
||||
@@ -503,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–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 перерисовывает форму — поле 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–255 с, по умолчанию 30 с, <code class="font-mono text-sm">0</code> отключает опрос) сервер отправляет STATUS и обрабатывает любую ожидающую проактивную команду. При извлечении карты опрос останавливается, а состояние карты сбрасывается.</p>
|
||||
<p class="text-sm mb-3">Кнопка <strong>Отправить STATUS</strong> отправляет STATUS (F2) вручную. Переключатель <strong>Опрос</strong> включает фоновый опрос: после настраиваемого интервала бездействия (аргумент сервера <code class="font-mono text-sm">--poll-interval</code>, 1–255 с, по умолчанию 30 с, <code class="font-mono text-sm">0</code> отключает опрос) сервер отправляет STATUS и обрабатывает любую ожидающую проактивную команду. Команда карты <strong>POLL INTERVAL</strong> обновляет интервал (TS 102 223 §6.4.6; TERMINAL RESPONSE сообщает фактически используемый интервал), а <strong>POLLING OFF</strong> приостанавливает опрос до нового POLL INTERVAL — состояние показывается янтарным цветом, при этом ручная кнопка <strong>Отправить STATUS</strong> продолжает работать (на presence detection и ручной STATUS POLLING OFF не влияет). При извлечении карты опрос останавливается, а состояние карты сбрасывается.</p>
|
||||
|
||||
<h3 id="pli-dict" class="text-lg font-medium mb-2">8.6 «Конфигурация TR» — данные ответа PROVIDE LOCAL INFORMATION</h3>
|
||||
<p class="text-sm mb-2">Редактируемые hex-значения для всех 22 квалификаторов PLI (TS 102 223 §8.6 + TS 131 111). У десяти квалификаторов есть встроенные формы декодирования/кодирования:</p>
|
||||
|
||||
+2
-1
@@ -492,6 +492,7 @@
|
||||
<li><strong>Location Status</strong> — dropdown: Normal / Limited / No service (tag <code class="font-mono text-sm">9B</code>)</li>
|
||||
<li><strong>Access Technology Change</strong> — 13 RAT types (tag <code class="font-mono text-sm">BF</code>)</li>
|
||||
<li><strong>Network Rejection</strong> — full adaptive form: registration type (LU / GPRS / EPS / 5GS), location fields (MCC, MNC, LAC, RAC, TAC), access technology, and a 53-cause unified rejection cause dropdown covering EMM, GMM, 5GMM and LU causes</li>
|
||||
<li><strong>Poll Interval Negotiation</strong> (0x1C) — proposes a poll interval as a Duration (unit + value); after sending, the UICC's answer is shown (accepted / rejected / modified + duration), and a <em>modified</em> duration becomes the background poll interval (TS 102 223 §7.5.22). The form appears when the card subscribed to this event.</li>
|
||||
</ul>
|
||||
<p class="text-sm mb-3">Sending an event uses <code class="font-mono text-sm">ENVELOPE(Event Download)</code> per TS 102 223 / TS 131 111.</p>
|
||||
|
||||
@@ -502,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 §5.2</strong> / <strong>TS 31.111 §5.2</strong> (bytes 1–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 — 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–255 s, default 30 s, <code class="font-mono text-sm">0</code> disables polling) the server sends STATUS and handles any pending proactive command. 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). 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–255 s, default 30 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 §6.4.6; the TERMINAL RESPONSE reports the interval actually used), and a <strong>POLLING OFF</strong> suspends polling until a new POLL INTERVAL — 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>
|
||||
|
||||
<h3 id="pli-dict" class="text-lg font-medium mb-2">8.6 TR Config — PROVIDE LOCAL INFORMATION response data</h3>
|
||||
<p class="text-sm mb-2">Editable hex values for all 22 PLI qualifiers (TS 102 223 §8.6 + TS 131 111). Ten qualifiers have inline decode/encode forms:</p>
|
||||
|
||||
+59
-18
@@ -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.8';
|
||||
const SIMPLE_VERSION = '3.5.9';
|
||||
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||
|
||||
// ===== Tab switching =====
|
||||
@@ -9208,23 +9208,34 @@ function pysimFsResetEdit() {
|
||||
}
|
||||
|
||||
// ===== proactive UICC events =====
|
||||
// Event list codings (TS 102 223 v18.3.0 8.25). Values the CAT spec leaves
|
||||
// "Reserved for 3GPP" carry the concrete 3GPP event name where TS 31.111
|
||||
// defines one (clause in parentheses), so the list stays informative.
|
||||
const EVENT_NAMES = {
|
||||
0x00: 'MT call', 0x01: 'Call connected', 0x02: 'Call disconnected',
|
||||
0x03: 'Location status', 0x04: 'User activity', 0x05: 'Idle screen available',
|
||||
0x06: 'Card reader status', 0x07: 'Language selection',
|
||||
0x08: 'Browser termination', 0x09: 'Data available',
|
||||
0x0A: 'Channel status', 0x0B: 'Access Technology Change',
|
||||
0x0A: 'Channel status', 0x0B: 'Access technology change (single)',
|
||||
0x0C: 'Display parameters changed', 0x0D: 'Local connection',
|
||||
0x0E: 'Network Search Mode Change', 0x0F: 'Browsing status',
|
||||
0x10: 'Frames Information Change', 0x11: 'I-WLAN Access Status',
|
||||
0x12: 'Network Rejection', 0x13: 'HCI Connectivity',
|
||||
0x14: 'Change of UICC Access', 0x15: 'CSG Cell Change',
|
||||
0x16: 'Contactless state request', 0x17: 'Profile Container',
|
||||
0x18: 'LTE D2D Discovery Monitoring', 0x19: 'LTE D2D Communication Monitoring',
|
||||
0x1A: 'LTE D2D Announcement Response', 0x1B: 'LTE D2D Revocation',
|
||||
0x1C: 'LTE D2D Application Port', 0x1D: 'LTE D2D Security Recovery',
|
||||
0x1E: 'Off-net Emergency Call', 0x1F: 'ECall Over IMS',
|
||||
0x20: 'EARFCN Update', 0x21: 'SCEF Channel Status',
|
||||
0x0E: 'Network search mode change', 0x0F: 'Browsing status',
|
||||
0x10: 'Frames information change',
|
||||
0x11: 'Reserved for 3GPP — (I-)WLAN access status (TS 31.111 7.5.1)',
|
||||
0x12: 'Reserved for 3GPP — Network rejection (TS 31.111 7.5.2)',
|
||||
0x13: 'HCI connectivity',
|
||||
0x14: 'Access technology change (multiple)',
|
||||
0x15: 'Reserved for 3GPP — CSG cell selection (TS 31.111 7.5.3)',
|
||||
0x16: 'Contactless state request',
|
||||
0x17: 'Reserved for 3GPP — IMS registration (TS 31.111 7.5.21)',
|
||||
0x18: 'Reserved for 3GPP — Incoming IMS data (TS 31.111 7.5.20)',
|
||||
0x19: 'Profile container', 0x1A: 'Void', 0x1B: 'Secured profile container',
|
||||
0x1C: 'Poll interval negotiation',
|
||||
0x1D: 'Reserved for 3GPP — Data connection status change (TS 31.111 7.5.25)',
|
||||
0x1E: 'Reserved for 3GPP — CAG cell selection (TS 31.111 7.5.26)',
|
||||
0x1F: 'Reserved for 3GPP — Slices status change (TS 31.111 7.5.27)',
|
||||
0x20: 'Reserved for 3GPP (future usage)',
|
||||
0x21: 'Reserved for 3GPP (future usage)',
|
||||
0x22: 'Reserved for 3GPP (future usage)',
|
||||
};
|
||||
|
||||
const REJECTION_CAUSES = [
|
||||
@@ -9380,14 +9391,27 @@ const EVENT_FORMS = {
|
||||
}
|
||||
},
|
||||
0x13: null,
|
||||
0x14: { title: 'Change of UICC Access',
|
||||
0x14: { title: 'Access technology change (multiple)',
|
||||
fields: [{ id: 'tech', label: 'Access Technology', type: 'select',
|
||||
opts: [{v:0,l:'GSM'},{v:3,l:'UTRAN'},{v:8,l:'E-UTRAN'},{v:10,l:'NG-RAN'}] }],
|
||||
build(v) { return 'BF01' + (v.tech|0).toString(16).padStart(2,'0'); }
|
||||
},
|
||||
0x15: { title: 'CSG Cell Change', note: 'not_yet' },
|
||||
0x15: { title: 'CSG cell selection', note: 'not_yet' },
|
||||
0x16: null,
|
||||
0x17: null,
|
||||
0x1C: { title: 'Poll interval negotiation',
|
||||
fields: [
|
||||
{ id: 'unit', label: 'Time unit', type: 'select',
|
||||
opts: [{v:1,l:'Seconds'},{v:0,l:'Minutes'},{v:2,l:'Tenths of seconds'}] },
|
||||
{ id: 'interval', label: 'Time interval', type: 'number', min: '1', max: '255', placeholder: '1–255 (default 30 s)' },
|
||||
],
|
||||
build(v) {
|
||||
const unit = parseInt(v.unit, 10) || 0;
|
||||
let iv = parseInt(v.interval, 10);
|
||||
if (!(iv >= 1 && iv <= 255)) iv = 30;
|
||||
return ('0402' + unit.toString(16).padStart(2, '0') + iv.toString(16).padStart(2, '0')).toUpperCase();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const PLI_QUALIFIERS = [
|
||||
@@ -9899,6 +9923,11 @@ async function pysimEventSend() {
|
||||
const resp = await pysimFetch('/api/event-send', { event_type: _eventFormType, event_data: eventData });
|
||||
if (resp.sw === '9000') {
|
||||
result.textContent = (eventData ? t('Event data sent.') : t('Event sent.')) + ' SW: ' + resp.sw;
|
||||
if (resp.negotiation) {
|
||||
let neg = t('Poll interval negotiation:') + ' ' + t(resp.negotiation.result_name);
|
||||
if (resp.negotiation.seconds) neg += ' (' + resp.negotiation.seconds + ' s)';
|
||||
result.textContent += ' · ' + neg;
|
||||
}
|
||||
result.classList.remove('hidden');
|
||||
} else {
|
||||
result.textContent = 'SW: ' + resp.sw;
|
||||
@@ -9949,11 +9978,11 @@ async function pysimPollToggle() {
|
||||
const status = await pysimFetch('/api/poll-status');
|
||||
const newEnabled = !status.enabled;
|
||||
const resp = await pysimFetch('/api/poll-toggle', { enabled: newEnabled });
|
||||
pysimUpdatePollUI(resp.enabled, resp.interval);
|
||||
pysimUpdatePollUI(resp.enabled, resp.interval, resp.card_disabled, resp.warning);
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
function pysimUpdatePollUI(enabled, interval) {
|
||||
function pysimUpdatePollUI(enabled, interval, cardDisabled, warning) {
|
||||
const btn = document.getElementById('pli-pause-btn');
|
||||
const stat = document.getElementById('pli-poll-stat');
|
||||
if (!btn) return;
|
||||
@@ -9966,13 +9995,18 @@ function pysimUpdatePollUI(enabled, interval) {
|
||||
btn.classList.remove('bg-emerald-600', 'text-white');
|
||||
btn.classList.add('bg-gray-200', 'dark:bg-slate-700', 'text-gray-700', 'dark:text-slate-300');
|
||||
}
|
||||
if (stat) stat.textContent = enabled ? '(' + interval + 's)' : '';
|
||||
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)');
|
||||
else if (warning) stat.textContent = warning;
|
||||
else stat.textContent = enabled ? '(' + interval + 's)' : '';
|
||||
}
|
||||
}
|
||||
|
||||
async function pysimPollStatusInit() {
|
||||
try {
|
||||
const status = await pysimFetch('/api/poll-status');
|
||||
pysimUpdatePollUI(status.enabled, status.interval);
|
||||
pysimUpdatePollUI(status.enabled, status.interval, status.card_disabled);
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
@@ -15387,6 +15421,13 @@ const LANG_RU = {
|
||||
'STK menu': 'Меню STK',
|
||||
'No menu set by the card': 'Меню не задано картой',
|
||||
'Polling': 'Опрос',
|
||||
'card disabled polling (POLLING OFF)': 'карта отключила опрос (POLLING OFF)',
|
||||
'Time unit': 'Единица времени',
|
||||
'Time interval': 'Интервал времени',
|
||||
'Poll interval negotiation:': 'Согласование интервала опроса:',
|
||||
'accepted': 'принято',
|
||||
'rejected': 'отклонено',
|
||||
'modified': 'изменено',
|
||||
'STATUS...': 'STATUS...',
|
||||
'Saving...': 'Сохранение...',
|
||||
'Saved': 'Сохранено',
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v261';
|
||||
const CACHE = 'simple-v262';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -31,6 +31,7 @@ function extractFunc(src, name) {
|
||||
// Rewrite top-level const -> var so the maps leak out of sloppy-mode eval.
|
||||
eval(extractBlock('const CMD_NAMES = {', 'function cmdQualifierShort').replace(/^const /gm, 'var '));
|
||||
eval(extractFunc(html, 'cmdQualifierShort'));
|
||||
eval(extractBlock('const EVENT_NAMES = {', 'const REJECTION_CAUSES = [').replace(/^const /gm, 'var '));
|
||||
eval(extractBlock('const REJECTION_CAUSES = [', 'const EVENT_FORMS = {').replace(/^const /gm, 'var '));
|
||||
eval(extractBlock('const EVENT_FORMS = {', 'const PLI_QUALIFIERS = [').replace(/^const /gm, 'var '));
|
||||
|
||||
@@ -64,6 +65,27 @@ test('cmdQualifierShort returns empty for unknown types', () => {
|
||||
assert.strictEqual(cmdQualifierShort('99', 0x01), '');
|
||||
});
|
||||
|
||||
test('EVENT_NAMES follows the pinned CAT spec and names the 3GPP events', () => {
|
||||
assert.strictEqual(EVENT_NAMES[0x0B], 'Access technology change (single)');
|
||||
assert.strictEqual(EVENT_NAMES[0x14], 'Access technology change (multiple)');
|
||||
assert.strictEqual(EVENT_NAMES[0x19], 'Profile container');
|
||||
assert.strictEqual(EVENT_NAMES[0x1C], 'Poll interval negotiation');
|
||||
for (const [v, frag] of [[0x11, '(I-)WLAN access status'], [0x12, 'Network rejection'],
|
||||
[0x15, 'CSG cell selection'], [0x17, 'IMS registration'], [0x18, 'Incoming IMS data'],
|
||||
[0x1D, 'Data connection status change'], [0x1E, 'CAG cell selection'],
|
||||
[0x1F, 'Slices status change']]) {
|
||||
assert.ok(EVENT_NAMES[v].includes(frag), '0x' + v.toString(16) + ': ' + EVENT_NAMES[v]);
|
||||
}
|
||||
});
|
||||
|
||||
test('the poll interval negotiation event builds a Duration TLV', () => {
|
||||
const build = EVENT_FORMS[0x1C].build;
|
||||
assert.strictEqual(build({ unit: '1', interval: '45' }), '0402012D');
|
||||
assert.strictEqual(build({ unit: '0', interval: '2' }), '04020002');
|
||||
assert.strictEqual(build({ unit: '2', interval: '5' }), '04020205');
|
||||
assert.strictEqual(build({ unit: '1', interval: '' }), '0402011E'); // default 30 s
|
||||
});
|
||||
|
||||
test('channel status event builds the B8 channel status TLV', () => {
|
||||
const build = EVENT_FORMS[0x0A].build;
|
||||
assert.strictEqual(EVENT_FORMS[0x0A].note, undefined);
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
|
||||
|
||||
function extractFunc(src, name, asyncFn) {
|
||||
const re = new RegExp('function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
|
||||
const m = re.exec(src);
|
||||
if (!m) throw new Error('function ' + name + ' not found');
|
||||
let i = m.index + m[0].length - 1;
|
||||
let depth = 0;
|
||||
for (; i < src.length; i++) {
|
||||
if (src[i] === '{') depth++;
|
||||
else if (src[i] === '}') {
|
||||
depth--;
|
||||
if (depth === 0) break;
|
||||
}
|
||||
}
|
||||
return (asyncFn ? 'async ' : '') + src.slice(m.index, i + 1);
|
||||
}
|
||||
|
||||
eval(extractFunc(html, 'pysimUpdatePollUI'));
|
||||
globalThis.t = s => s;
|
||||
|
||||
function setup() {
|
||||
const els = {
|
||||
'pli-pause-btn': { textContent: '', classList: { add: () => {}, remove: () => {} } },
|
||||
'pli-poll-stat': { textContent: '', className: 'text-xs text-gray-400' },
|
||||
};
|
||||
globalThis.document = { getElementById: id => els[id] || null };
|
||||
return els;
|
||||
}
|
||||
|
||||
test('pysimUpdatePollUI shows the interval while polling is on', () => {
|
||||
const els = setup();
|
||||
pysimUpdatePollUI(true, 30);
|
||||
assert.strictEqual(els['pli-pause-btn'].textContent, 'ON');
|
||||
assert.strictEqual(els['pli-poll-stat'].textContent, '(30s)');
|
||||
assert.ok(!els['pli-poll-stat'].className.includes('amber'));
|
||||
});
|
||||
|
||||
test('pysimUpdatePollUI shows the card-disabled state in amber', () => {
|
||||
const els = setup();
|
||||
pysimUpdatePollUI(true, 30, true);
|
||||
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', () => {
|
||||
const els = setup();
|
||||
pysimUpdatePollUI(true, 30, false, 'card disabled proactive polling');
|
||||
assert.strictEqual(els['pli-poll-stat'].textContent, 'card disabled proactive polling');
|
||||
});
|
||||
|
||||
test('pysimUpdatePollUI clears the status line when off', () => {
|
||||
const els = setup();
|
||||
pysimUpdatePollUI(false, 30);
|
||||
assert.strictEqual(els['pli-pause-btn'].textContent, 'OFF');
|
||||
assert.strictEqual(els['pli-poll-stat'].textContent, '');
|
||||
});
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "pysim-simple-server"
|
||||
version = "3.5.8"
|
||||
version = "3.5.9"
|
||||
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.
|
||||
|
||||
+160
-21
@@ -30,7 +30,7 @@ from osmocom.tlv import BER_TLV_IE
|
||||
from osmocom.utils import rpad
|
||||
|
||||
|
||||
VERSION = '3.5.8'
|
||||
VERSION = '3.5.9'
|
||||
|
||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||
|
||||
@@ -1327,6 +1327,10 @@ _SCP81_PSK_LEGACY = None
|
||||
_POLL_ENABLED = False
|
||||
_POLL_INTERVAL = 30
|
||||
_POLL_TIMER = None
|
||||
# Set when the card sent POLLING OFF (TS 102 223 6.4.14): proactive polling
|
||||
# stays suspended until a POLL INTERVAL re-enables it. Presence detection and
|
||||
# the manual "Send STATUS" button are not affected.
|
||||
_POLL_DISABLED_BY_CARD = False
|
||||
_CARD_LOCK = threading.RLock()
|
||||
_CARD_CONNECTED = False
|
||||
# True when the current PC/SC transport is unusable (service failure): the
|
||||
@@ -1337,12 +1341,71 @@ def _set_poll_interval(seconds):
|
||||
global _POLL_INTERVAL
|
||||
_POLL_INTERVAL = max(0, min(255, int(seconds)))
|
||||
|
||||
def _duration_seconds(unit, interval):
|
||||
"""Duration TLV value (TS 102 223 8.8) as seconds for the background poll,
|
||||
clamped to 1..255 s (the TERMINAL RESPONSE Duration carries seconds)."""
|
||||
if unit == 0x00: # minutes
|
||||
secs = interval * 60
|
||||
elif unit == 0x02: # tenths of seconds
|
||||
secs = int(round(interval / 10.0))
|
||||
else: # seconds
|
||||
secs = interval
|
||||
return max(1, min(255, secs))
|
||||
|
||||
|
||||
def _duration_text(unit, interval):
|
||||
if unit == 0x00:
|
||||
return '%d min' % interval
|
||||
if unit == 0x02:
|
||||
return '%.1f s' % (interval / 10.0)
|
||||
return '%d s' % interval
|
||||
|
||||
|
||||
def _find_duration(raw):
|
||||
"""(unit, interval) of the Duration TLV in a proactive command (tag '04'
|
||||
or its CR-set variant '84', TS 102 223 8.8), or None."""
|
||||
if not raw:
|
||||
return None
|
||||
off = _skip_ber_len(raw, 1) if raw[0] == 0xD0 else 0
|
||||
while off + 1 < len(raw):
|
||||
tag, tlen = raw[off], raw[off + 1]
|
||||
val = raw[off + 2: off + 2 + tlen]
|
||||
if tag in (0x04, 0x84) and len(val) >= 2:
|
||||
return val[0], val[1]
|
||||
off += 2 + tlen
|
||||
return None
|
||||
|
||||
|
||||
def _handle_card_poll_command(cmd_type, raw):
|
||||
"""Adopt the card's polling wishes before its TERMINAL RESPONSE is built
|
||||
(TS 102 223 6.4.6 POLL INTERVAL / 6.4.14 POLLING OFF)."""
|
||||
global _POLL_DISABLED_BY_CARD, _POLL_TIMER
|
||||
if cmd_type == 0x03:
|
||||
dur = _find_duration(raw)
|
||||
if dur:
|
||||
unit, interval = dur
|
||||
secs = _duration_seconds(unit, interval)
|
||||
_set_poll_interval(secs)
|
||||
sys.stderr.write('POLL INTERVAL: card requests %s -> background poll %d s\n'
|
||||
% (_duration_text(unit, interval), secs))
|
||||
_POLL_DISABLED_BY_CARD = False
|
||||
if _POLL_ENABLED:
|
||||
_reset_poll_timer()
|
||||
elif cmd_type == 0x04:
|
||||
if not _POLL_DISABLED_BY_CARD:
|
||||
sys.stderr.write('POLLING OFF: card disabled proactive polling\n')
|
||||
_POLL_DISABLED_BY_CARD = True
|
||||
if _POLL_TIMER is not None:
|
||||
_POLL_TIMER.cancel()
|
||||
_POLL_TIMER = None
|
||||
|
||||
|
||||
def _reset_poll_timer():
|
||||
global _POLL_TIMER
|
||||
if _POLL_TIMER is not None:
|
||||
_POLL_TIMER.cancel()
|
||||
_POLL_TIMER = None
|
||||
if _POLL_ENABLED and _POLL_INTERVAL > 0:
|
||||
if _POLL_ENABLED and _POLL_INTERVAL > 0 and not _POLL_DISABLED_BY_CARD:
|
||||
_POLL_TIMER = threading.Timer(_POLL_INTERVAL, _do_status_poll)
|
||||
_POLL_TIMER.daemon = True
|
||||
_POLL_TIMER.start()
|
||||
@@ -1350,7 +1413,7 @@ def _reset_poll_timer():
|
||||
def _do_status_poll():
|
||||
global _POLL_TIMER
|
||||
_POLL_TIMER = None
|
||||
if not _POLL_ENABLED:
|
||||
if not _POLL_ENABLED or _POLL_DISABLED_BY_CARD:
|
||||
return
|
||||
with _CARD_LOCK:
|
||||
try:
|
||||
@@ -1420,6 +1483,53 @@ def _poll_disable():
|
||||
_POLL_TIMER.cancel()
|
||||
_POLL_TIMER = None
|
||||
|
||||
|
||||
POLL_NEGOTIATION_RESULTS = {0: 'accepted', 1: 'rejected', 2: 'modified'}
|
||||
|
||||
|
||||
def _decode_poll_negotiation(data_hex):
|
||||
"""Decode the ENVELOPE response data of a Poll Interval Negotiation event
|
||||
(TS 102 223 7.5.22.2 / 8.97). No response data means 'accepted'."""
|
||||
s = re.sub(r'\s', '', data_hex or '')
|
||||
if not s:
|
||||
return {'result': 0, 'result_name': 'accepted', 'unit': None,
|
||||
'interval': None, 'seconds': None}
|
||||
try:
|
||||
data = bytes.fromhex(s)
|
||||
except ValueError:
|
||||
return None
|
||||
result = None
|
||||
unit = interval = None
|
||||
off = 0
|
||||
while off + 1 < len(data):
|
||||
tag, tlen = data[off], data[off + 1]
|
||||
val = data[off + 2: off + 2 + tlen]
|
||||
off += 2 + tlen
|
||||
if tag in (0x11, 0x91) and len(val) >= 1:
|
||||
result = val[0]
|
||||
elif tag in (0x04, 0x84) and len(val) >= 2:
|
||||
unit, interval = val[0], val[1]
|
||||
if result is None:
|
||||
return None
|
||||
out = {'result': result, 'result_name': POLL_NEGOTIATION_RESULTS.get(result, 'unknown'),
|
||||
'unit': unit, 'interval': interval, 'seconds': None}
|
||||
if unit is not None:
|
||||
out['seconds'] = _duration_seconds(unit, interval)
|
||||
return out
|
||||
|
||||
|
||||
def _apply_poll_negotiation(neg):
|
||||
"""Act on the card's answer to our poll interval proposal: a 'modified'
|
||||
result carries the duration the terminal shall use from now on."""
|
||||
global _POLL_DISABLED_BY_CARD
|
||||
if not neg or neg.get('result') != 2 or not neg.get('seconds'):
|
||||
return
|
||||
_set_poll_interval(neg['seconds'])
|
||||
_POLL_DISABLED_BY_CARD = False
|
||||
sys.stderr.write('POLL NEGOTIATION: modified -> background poll %d s\n' % neg['seconds'])
|
||||
if _POLL_ENABLED:
|
||||
_reset_poll_timer()
|
||||
|
||||
_server_ref = None
|
||||
|
||||
|
||||
@@ -1438,23 +1548,34 @@ def _tr_data_only(tr_tlv):
|
||||
return bytes(data)
|
||||
|
||||
|
||||
# Event list codings (TS 102 223 v18.3.0 8.25). Values the CAT spec leaves
|
||||
# "Reserved for 3GPP" carry the concrete 3GPP event name where TS 31.111
|
||||
# defines one (clauses in parentheses), so the list stays informative.
|
||||
EVENT_NAMES = {
|
||||
0x00: 'MT call', 0x01: 'Call connected', 0x02: 'Call disconnected',
|
||||
0x03: 'Location status', 0x04: 'User activity', 0x05: 'Idle screen available',
|
||||
0x06: 'Card reader status', 0x07: 'Language selection',
|
||||
0x08: 'Browser termination', 0x09: 'Data available',
|
||||
0x0A: 'Channel status', 0x0B: 'Access Technology Change',
|
||||
0x0A: 'Channel status', 0x0B: 'Access technology change (single)',
|
||||
0x0C: 'Display parameters changed', 0x0D: 'Local connection',
|
||||
0x0E: 'Network Search Mode Change', 0x0F: 'Browsing status',
|
||||
0x10: 'Frames Information Change', 0x11: 'I-WLAN Access Status',
|
||||
0x12: 'Network Rejection', 0x13: 'HCI Connectivity',
|
||||
0x14: 'Change of UICC Access', 0x15: 'CSG Cell Change',
|
||||
0x16: 'Contactless state request', 0x17: 'Profile Container',
|
||||
0x18: 'LTE D2D Discovery Monitoring', 0x19: 'LTE D2D Communication Monitoring',
|
||||
0x1A: 'LTE D2D Announcement Response', 0x1B: 'LTE D2D Revocation',
|
||||
0x1C: 'LTE D2D Application Port', 0x1D: 'LTE D2D Security Recovery',
|
||||
0x1E: 'Off-net Emergency Call', 0x1F: 'ECall Over IMS',
|
||||
0x20: 'EARFCN Update', 0x21: 'SCEF Channel Status',
|
||||
0x0E: 'Network search mode change', 0x0F: 'Browsing status',
|
||||
0x10: 'Frames information change',
|
||||
0x11: 'Reserved for 3GPP \u2014 (I-)WLAN access status (TS 31.111 7.5.1)',
|
||||
0x12: 'Reserved for 3GPP \u2014 Network rejection (TS 31.111 7.5.2)',
|
||||
0x13: 'HCI connectivity',
|
||||
0x14: 'Access technology change (multiple)',
|
||||
0x15: 'Reserved for 3GPP \u2014 CSG cell selection (TS 31.111 7.5.3)',
|
||||
0x16: 'Contactless state request',
|
||||
0x17: 'Reserved for 3GPP \u2014 IMS registration (TS 31.111 7.5.21)',
|
||||
0x18: 'Reserved for 3GPP \u2014 Incoming IMS data (TS 31.111 7.5.20)',
|
||||
0x19: 'Profile container', 0x1A: 'Void', 0x1B: 'Secured profile container',
|
||||
0x1C: 'Poll interval negotiation',
|
||||
0x1D: 'Reserved for 3GPP \u2014 Data connection status change (TS 31.111 7.5.25)',
|
||||
0x1E: 'Reserved for 3GPP \u2014 CAG cell selection (TS 31.111 7.5.26)',
|
||||
0x1F: 'Reserved for 3GPP \u2014 Slices status change (TS 31.111 7.5.27)',
|
||||
0x20: 'Reserved for 3GPP (future usage)',
|
||||
0x21: 'Reserved for 3GPP (future usage)',
|
||||
0x22: 'Reserved for 3GPP (future usage)',
|
||||
}
|
||||
|
||||
ACCESSTECH_NAMES = {
|
||||
@@ -1672,9 +1793,12 @@ def _decode_cmd(cmd_type, raw, qualifier):
|
||||
if not raw:
|
||||
return []
|
||||
if cmd_type == 0x03:
|
||||
idx = raw.find(b'\x84\x02\x01')
|
||||
if idx >= 0 and idx + 3 < len(raw):
|
||||
return [{'label': 'Interval', 'value': '%d s' % raw[idx + 3]}]
|
||||
dur = _find_duration(raw)
|
||||
if dur:
|
||||
text = _duration_text(dur[0], dur[1])
|
||||
if dur[0] != 0x01:
|
||||
text += ' (%d s)' % _duration_seconds(dur[0], dur[1])
|
||||
return [{'label': 'Interval', 'value': text}]
|
||||
return []
|
||||
if cmd_type == 0x05:
|
||||
for tag in (0x99, 0x19):
|
||||
@@ -3758,6 +3882,8 @@ def _run_proactive_chain(scc, sw91, on_fetch=None, status_poll=True):
|
||||
else:
|
||||
cmd_num, cmd_type, dev_src, dev_dst, cmd_qual = 1, 0, 0x83, 0x81, None
|
||||
entry = None
|
||||
if raw and cmd_type in (0x03, 0x04):
|
||||
_handle_card_poll_command(cmd_type, raw)
|
||||
if on_fetch:
|
||||
action = on_fetch(raw, cmd_num, cmd_type, dev_src, dev_dst)
|
||||
paused = action == 'pause'
|
||||
@@ -3836,6 +3962,8 @@ def _send_terminal_profile(scc, tp_hex):
|
||||
if menu:
|
||||
_attach_nai(items, nai)
|
||||
sim_menu = menu
|
||||
if fdata and cmd_type in (0x03, 0x04):
|
||||
_handle_card_poll_command(cmd_type, raw)
|
||||
if fdata and cmd_type:
|
||||
entry = _log_proactive(cmd_type, raw, cmd_qual, cmd_num)
|
||||
else:
|
||||
@@ -4267,7 +4395,8 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
self._send_json(resp)
|
||||
self._log_resp(resp)
|
||||
elif self.path == '/api/poll-status':
|
||||
resp = {'enabled': _POLL_ENABLED, 'interval': _POLL_INTERVAL}
|
||||
resp = {'enabled': _POLL_ENABLED, 'interval': _POLL_INTERVAL,
|
||||
'card_disabled': _POLL_DISABLED_BY_CARD}
|
||||
self._send_json(resp)
|
||||
self._log_resp(resp)
|
||||
elif self.path == '/api/proactive-log':
|
||||
@@ -4933,14 +5062,20 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
try:
|
||||
data, sw = _send_event_download(scc, event_type, event_data)
|
||||
try:
|
||||
is_location = int(event_type) == netsim.EVENT_LOCATION_STATUS
|
||||
ev_type = int(event_type)
|
||||
except (TypeError, ValueError):
|
||||
is_location = False
|
||||
ev_type = -1
|
||||
is_location = ev_type == netsim.EVENT_LOCATION_STATUS
|
||||
if is_location:
|
||||
_netstate_after_event(self.server, event_type, event_data)
|
||||
resp = {'sw': sw}
|
||||
if data:
|
||||
resp['data'] = data
|
||||
if ev_type == 0x1C:
|
||||
negotiation = _decode_poll_negotiation(data)
|
||||
if negotiation:
|
||||
resp['negotiation'] = negotiation
|
||||
_apply_poll_negotiation(negotiation)
|
||||
if getattr(self.server, 'net_state', None) is not None:
|
||||
resp['net_state'] = self.server.net_state
|
||||
self._send_json(resp)
|
||||
@@ -4973,7 +5108,11 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
_poll_enable()
|
||||
else:
|
||||
_poll_disable()
|
||||
resp = {'enabled': _POLL_ENABLED, 'interval': _POLL_INTERVAL}
|
||||
resp = {'enabled': _POLL_ENABLED, 'interval': _POLL_INTERVAL,
|
||||
'card_disabled': _POLL_DISABLED_BY_CARD}
|
||||
if _POLL_ENABLED and _POLL_DISABLED_BY_CARD:
|
||||
resp['warning'] = ('card disabled proactive polling with POLLING OFF '
|
||||
'(TS 102 223 6.4.14); it resumes after a POLL INTERVAL')
|
||||
self._send_json(resp)
|
||||
self._log_resp(resp)
|
||||
elif self.path == '/api/send-ota':
|
||||
|
||||
@@ -51,5 +51,90 @@ class TestPollInterval(unittest.TestCase):
|
||||
self.assertIsNone(S._POLL_TIMER)
|
||||
|
||||
|
||||
POLL_CMD = 'D00D810301030082028182' # Command Details + Device Identities
|
||||
MIN_2_TLV = POLL_CMD + '04020002' # Duration: 2 minutes (plain tag)
|
||||
SEC_30_TLV = POLL_CMD + '8402011E' # Duration: 30 seconds (CR tag)
|
||||
|
||||
|
||||
class TestCardDrivenPolling(unittest.TestCase):
|
||||
"""TS 102 223 6.4.6/6.4.14: adopt the card's POLL INTERVAL duration and
|
||||
honour POLLING OFF; TS 102 223 7.5.22/8.97: act on a negotiation answer."""
|
||||
|
||||
def setUp(self):
|
||||
self.saved = (S._POLL_ENABLED, S._POLL_INTERVAL, S._POLL_TIMER,
|
||||
S._POLL_DISABLED_BY_CARD)
|
||||
|
||||
def tearDown(self):
|
||||
S._poll_disable()
|
||||
(S._POLL_ENABLED, S._POLL_INTERVAL, S._POLL_TIMER,
|
||||
S._POLL_DISABLED_BY_CARD) = self.saved
|
||||
|
||||
def test_duration_units_convert_to_seconds(self):
|
||||
self.assertEqual(S._duration_seconds(0x01, 30), 30)
|
||||
self.assertEqual(S._duration_seconds(0x00, 2), 120)
|
||||
self.assertEqual(S._duration_seconds(0x02, 5), 1) # tenths, min 1 s
|
||||
self.assertEqual(S._duration_seconds(0x00, 200), 255) # clamped
|
||||
self.assertEqual(S._duration_text(0x00, 2), '2 min')
|
||||
self.assertEqual(S._duration_text(0x01, 30), '30 s')
|
||||
|
||||
def test_find_duration_accepts_both_tag_styles(self):
|
||||
self.assertEqual(S._find_duration(bytes.fromhex(SEC_30_TLV)), (1, 30))
|
||||
self.assertEqual(S._find_duration(bytes.fromhex(MIN_2_TLV)), (0, 2))
|
||||
self.assertIsNone(S._find_duration(bytes.fromhex(POLL_CMD)))
|
||||
|
||||
def test_poll_interval_command_is_adopted(self):
|
||||
S._set_poll_interval(30)
|
||||
S._handle_card_poll_command(0x03, bytes.fromhex(MIN_2_TLV))
|
||||
self.assertEqual(S._POLL_INTERVAL, 120)
|
||||
self.assertFalse(S._POLL_DISABLED_BY_CARD)
|
||||
|
||||
def test_polling_off_suspends_until_a_new_interval(self):
|
||||
S._POLL_ENABLED = True
|
||||
S._set_poll_interval(30)
|
||||
S._handle_card_poll_command(0x04, b'')
|
||||
self.assertTrue(S._POLL_DISABLED_BY_CARD)
|
||||
with mock.patch.object(S.threading, 'Timer') as timer:
|
||||
S._reset_poll_timer()
|
||||
timer.assert_not_called()
|
||||
with mock.patch.object(S.threading, 'Timer') as timer:
|
||||
S._handle_card_poll_command(0x03, bytes.fromhex(SEC_30_TLV))
|
||||
timer.assert_called_once_with(30, S._do_status_poll)
|
||||
self.assertFalse(S._POLL_DISABLED_BY_CARD)
|
||||
|
||||
def test_status_poll_does_not_run_when_card_disabled(self):
|
||||
S._POLL_ENABLED = True
|
||||
S._POLL_DISABLED_BY_CARD = True
|
||||
with mock.patch.object(S, '_send_status') as send, \
|
||||
mock.patch.object(S.threading, 'Timer') as timer:
|
||||
S._do_status_poll()
|
||||
send.assert_not_called()
|
||||
timer.assert_not_called()
|
||||
|
||||
def test_negotiation_decode_and_modified_apply(self):
|
||||
neg = S._decode_poll_negotiation('1101020402011E') # modified, 30 s
|
||||
self.assertEqual(neg['result_name'], 'modified')
|
||||
self.assertEqual(neg['seconds'], 30)
|
||||
self.assertEqual(S._decode_poll_negotiation('')['result_name'], 'accepted')
|
||||
self.assertEqual(S._decode_poll_negotiation('110100')['result_name'], 'accepted')
|
||||
self.assertEqual(S._decode_poll_negotiation('11010104020102')['result_name'], 'rejected')
|
||||
self.assertIsNone(S._decode_poll_negotiation('1102')) # truncated result
|
||||
# a 'modified' answer becomes the new poll interval
|
||||
S._set_poll_interval(50)
|
||||
S._POLL_ENABLED = True
|
||||
with mock.patch.object(S.threading, 'Timer') as timer:
|
||||
S._apply_poll_negotiation(neg)
|
||||
timer.assert_called_once_with(30, S._do_status_poll)
|
||||
self.assertEqual(S._POLL_INTERVAL, 30)
|
||||
# a 'rejected' answer leaves the current interval alone
|
||||
S._apply_poll_negotiation(S._decode_poll_negotiation('11010104020102'))
|
||||
self.assertEqual(S._POLL_INTERVAL, 30)
|
||||
|
||||
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)')
|
||||
fields = S._decode_cmd(0x03, bytes.fromhex(SEC_30_TLV), 0)
|
||||
self.assertEqual(fields[0]['value'], '30 s')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -7,7 +7,7 @@ missing entry makes the proactive log show the generic 'Cmd 0x01' fallback.
|
||||
|
||||
import unittest
|
||||
|
||||
from pysim_simple_server.server import PROACTIVE_TYPE_NAMES
|
||||
from pysim_simple_server.server import EVENT_NAMES, PROACTIVE_TYPE_NAMES
|
||||
|
||||
|
||||
class ProactiveTypeNamesTests(unittest.TestCase):
|
||||
@@ -34,5 +34,43 @@ class ProactiveTypeNamesTests(unittest.TestCase):
|
||||
self.assertTrue(isinstance(name, str) and name, hex(value))
|
||||
|
||||
|
||||
class EventNamesTests(unittest.TestCase):
|
||||
"""Event list names follow TS 102 223 v18.3.0 8.25; the values the CAT
|
||||
spec leaves "Reserved for 3GPP" carry the TS 31.111 event name."""
|
||||
|
||||
def test_reassigned_values_match_the_pinned_spec(self):
|
||||
for value, name in [
|
||||
(0x0B, 'Access technology change (single)'),
|
||||
(0x14, 'Access technology change (multiple)'),
|
||||
(0x19, 'Profile container'),
|
||||
(0x1A, 'Void'),
|
||||
(0x1B, 'Secured profile container'),
|
||||
(0x1C, 'Poll interval negotiation'),
|
||||
(0x20, 'Reserved for 3GPP (future usage)'),
|
||||
]:
|
||||
self.assertEqual(EVENT_NAMES[value], name, hex(value))
|
||||
|
||||
def test_3gpp_reserved_events_carry_their_3gpp_name(self):
|
||||
for value, fragment, clause in [
|
||||
(0x11, '(I-)WLAN access status', 'TS 31.111 7.5.1'),
|
||||
(0x12, 'Network rejection', 'TS 31.111 7.5.2'),
|
||||
(0x15, 'CSG cell selection', 'TS 31.111 7.5.3'),
|
||||
(0x17, 'IMS registration', 'TS 31.111 7.5.21'),
|
||||
(0x18, 'Incoming IMS data', 'TS 31.111 7.5.20'),
|
||||
(0x1D, 'Data connection status change', 'TS 31.111 7.5.25'),
|
||||
(0x1E, 'CAG cell selection', 'TS 31.111 7.5.26'),
|
||||
(0x1F, 'Slices status change', 'TS 31.111 7.5.27'),
|
||||
]:
|
||||
name = EVENT_NAMES[value]
|
||||
self.assertIn('Reserved for 3GPP', name, hex(value))
|
||||
self.assertIn(fragment, name, hex(value))
|
||||
self.assertIn(clause, name, hex(value))
|
||||
|
||||
def test_names_are_nonempty_strings(self):
|
||||
for value, name in EVENT_NAMES.items():
|
||||
self.assertIsInstance(value, int)
|
||||
self.assertTrue(isinstance(name, str) and name, hex(value))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user