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:
+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': 'Сохранено',
|
||||
|
||||
Reference in New Issue
Block a user