feat: the remaining six event types get their parameter forms

MT call, Call connected, Call disconnected, Browser termination, (I-)WLAN
access status and CSG cell selection no longer fall back to the generic
"send a bare notification" prompt:

- MT call (TS 31.111 7.5.1A.2): transaction identifier, calling party number
  + TON -> Address (EFADN BCD coding, encBcdDigits/eventAddressTlv),
  subaddress, IMS URI, media type; source network.
- Call connected (7.5.2A.2) / Call disconnected (7.5.3A.2): transaction
  identifier, near/far-end source select, media type, and a cause - absent,
  the zero-length radio-link-timeout form or custom bytes (the hex field shows
  only for "custom").
- Browser termination (TS 102 223 7.5.9.2): user / error cause.
- (I-)WLAN access status (TS 31.111 7.5.1.2): no coverage / coverage /
  connection on-going.
- CSG cell selection (TS 31.111 7.5.3.2): status + selection mechanism,
  optional access technology, CSG ID, HNB name and PLMN ID; source network.

The device-identities source (TS 102 223 8.7: 82 terminal / 83 network) is now
selectable: EVENT_FORMS carries a fixed `src` or a `srcField`, and
_send_event_download()/`/api/event-send` take an `event_src` override (an
invalid value falls back to the terminal).

Tests: the BCD/Address builders, each event's build vectors, the "no not_yet
left" guard and the server source override.  Help EN/RU, both AGENTS files;
sw cache simple-v348.
This commit is contained in:
2026-10-01 22:19:55 +03:00
parent 994ee6d2a3
commit a26a2b6648
7 changed files with 219 additions and 14 deletions
+2
View File
@@ -504,6 +504,8 @@
<li><strong>Location Status</strong> — выпадающий список: Normal / Limited / No service (тег <code class="font-mono text-sm">9B</code>)</li> <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>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>Network Rejection</strong> — полная адаптивная форма: тип регистрации (LU / GPRS / EPS / 5GS), поля местоположения (MCC, MNC, LAC, RAC, TAC), технология доступа и единый выпадающий список из 53 кодов причин (EMM, GMM, 5GMM и LU)</li>
<li><strong>События звонка</strong> &mdash; MT call (transaction identifier, номер вызывающего + TON &rarr; Address, subaddress, IMS URI, media type; источник <em>network</em>), Call connected и Call disconnected (transaction identifier, источник near/far, media type и причина &mdash; отсутствует, нулевая форма radio link timeout или свои байты)</li>
<li><strong>Browser termination</strong> (причина user / error), <strong>(I-)WLAN access status</strong> (нет покрытия / покрытие / соединение) и <strong>CSG cell selection</strong> (статус + механизм выбора, access technology, CSG ID, HNB name, PLMN ID; источник <em>network</em>)</li>
<li><strong>Data Connection Status Change</strong> (0x1D) &mdash; статус (successful / rejected / dropped-deactivated), тип соединения (PDP / PDN / PDU), необязательная причина (E/5G)SM, transaction identifier и location status, а также необязательные date-time, access technology, APN, тип PDP/PDN/PDU и location information (MCC/MNC + LAC + Cell ID) &mdash; TS 131 111 &sect;7.5.25</li> <li><strong>Data Connection Status Change</strong> (0x1D) &mdash; статус (successful / rejected / dropped-deactivated), тип соединения (PDP / PDN / PDU), необязательная причина (E/5G)SM, transaction identifier и location status, а также необязательные date-time, access technology, APN, тип PDP/PDN/PDU и location information (MCC/MNC + LAC + Cell ID) &mdash; TS 131 111 &sect;7.5.25</li>
<li><strong>Poll Interval Negotiation</strong> (0x1C) — предлагает интервал опроса в виде Duration (единица + значение); после отправки показывается ответ UICC (принято / отклонено / изменено + длительность), а значение <em>изменено</em> становится интервалом фонового опроса (TS 102 223 &sect;7.5.22). Форма появляется, когда карта подписана на это событие.</li> <li><strong>Poll Interval Negotiation</strong> (0x1C) — предлагает интервал опроса в виде Duration (единица + значение); после отправки показывается ответ UICC (принято / отклонено / изменено + длительность), а значение <em>изменено</em> становится интервалом фонового опроса (TS 102 223 &sect;7.5.22). Форма появляется, когда карта подписана на это событие.</li>
</ul> </ul>
+2
View File
@@ -503,6 +503,8 @@
<li><strong>Location Status</strong> — dropdown: Normal / Limited / No service (tag <code class="font-mono text-sm">9B</code>)</li> <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>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>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>Call events</strong> &mdash; MT call (transaction identifier, calling party number + TON &rarr; Address, subaddress, IMS URI, media type; source <em>network</em>), Call connected and Call disconnected (transaction identifier, near/far-end source, media type, and a cause &mdash; absent, the zero-length radio-link-timeout form, or custom bytes)</li>
<li><strong>Browser termination</strong> (user / error cause), <strong>(I-)WLAN access status</strong> (no coverage / coverage / connection on-going) and <strong>CSG cell selection</strong> (status + selection mechanism, access technology, CSG ID, HNB name, PLMN ID; source <em>network</em>)</li>
<li><strong>Data Connection Status Change</strong> (0x1D) &mdash; status (successful / rejected / dropped-deactivated), connection type (PDP / PDN / PDU), optional (E/5G)SM cause, transaction identifier and location status, plus optional date-time, access technology, APN, PDP/PDN/PDU type and location information (MCC/MNC + LAC + Cell ID) &mdash; TS 131 111 &sect;7.5.25</li> <li><strong>Data Connection Status Change</strong> (0x1D) &mdash; status (successful / rejected / dropped-deactivated), connection type (PDP / PDN / PDU), optional (E/5G)SM cause, transaction identifier and location status, plus optional date-time, access technology, APN, PDP/PDN/PDU type and location information (MCC/MNC + LAC + Cell ID) &mdash; TS 131 111 &sect;7.5.25</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 &sect;7.5.22). The form appears when the card subscribed to this event.</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 &sect;7.5.22). The form appears when the card subscribed to this event.</li>
</ul> </ul>
+153 -7
View File
@@ -13049,6 +13049,27 @@ function eventDateTimeTlv(d) {
hx(semi(d.getSeconds())) + hx(tz); hx(semi(d.getSeconds())) + hx(tz);
} }
// EFADN number coding (TS 31.102 / TS 24.008 10.5.4.7): digit pairs in
// swapped semi-octets, an odd number of digits padded with 'F'.
function encBcdDigits(digits) {
const d = String(digits || '').replace(/\D/g, '');
let out = '';
for (let i = 0; i < d.length; i += 2) {
out += ((i + 1 < d.length ? d[i + 1] : 'F') + d[i]).toUpperCase();
}
return out;
}
// Address object (TS 102 223 8.1): TON/NPI byte + the number in the EFADN
// coding. NPI = ISDN/telephony (1), TON per the form (0 = unknown).
function eventAddressTlv(number, ton) {
const bcd = encBcdDigits(number);
if (!bcd) return '';
const tonNpi = (0x80 | ((parseInt(ton, 10) & 0x07) << 4) | 0x01);
const value = tonNpi.toString(16).padStart(2, '0') + bcd;
return '86' + (value.length / 2).toString(16).padStart(2, '0') + value;
}
const ACCESSTECH = [ const ACCESSTECH = [
{v:0,l:'GSM'},{v:1,l:'TIA/EIA-553-A'},{v:2,l:'TIA/EIA-136-270'},{v:3,l:'UTRAN'}, {v:0,l:'GSM'},{v:1,l:'TIA/EIA-553-A'},{v:2,l:'TIA/EIA-136-270'},{v:3,l:'UTRAN'},
{v:4,l:'TETRA'},{v:5,l:'TIA/EIA-95-B'},{v:6,l:'CDMA2000 1x'},{v:7,l:'CDMA2000 HRPD'}, {v:4,l:'TETRA'},{v:5,l:'TIA/EIA-95-B'},{v:6,l:'CDMA2000 1x'},{v:7,l:'CDMA2000 HRPD'},
@@ -13056,9 +13077,70 @@ const ACCESSTECH = [
]; ];
const EVENT_FORMS = { const EVENT_FORMS = {
0x00: { title: 'MT call', note: 'not_yet' }, 0x00: { title: 'MT call', src: '83',
0x01: { title: 'Call connected', note: 'not_yet' }, fields: [
0x02: { title: 'Call disconnected', note: 'not_yet' }, { id: 'ti', label: 'Transaction identifier', type: 'text', pattern: '[0-9A-Fa-f]{2}', placeholder: 'hex byte, e.g. 00' },
{ id: 'number', label: 'Calling party number (optional)', type: 'text', pattern: '\\d{1,20}', placeholder: 'digits, e.g. 79001234567' },
{ id: 'ton', label: 'Type of number', type: 'select',
opts: [{v:0,l:'Unknown'},{v:1,l:'International'},{v:2,l:'National'},{v:3,l:'Network specific'}] },
{ id: 'sub', label: 'Subaddress (optional, hex)', type: 'text', pattern: '[0-9A-Fa-f]{2,40}', placeholder: 'e.g. 1234' },
{ id: 'ims_uri', label: 'IMS URI (optional)', type: 'text', placeholder: 'e.g. sip:user@example.com' },
{ id: 'media', label: 'Media Type (optional)', type: 'select',
opts: [{v:'',l:'\u2014 not present \u2014'},{v:0,l:'00 \u2014 voice'},{v:1,l:'01 \u2014 video'}] },
],
build(v) {
let d = '1C01' + (v.ti || '00').replace(/[^0-9a-fA-F]/g, '').padStart(2, '0').slice(0, 2);
d += eventAddressTlv(v.number, v.ton);
if (v.sub) {
const h = v.sub.replace(/[^0-9a-fA-F]/g, '');
d += '88' + (h.length / 2).toString(16).padStart(2, '0') + h;
}
if (v.ims_uri) {
const b = new TextEncoder().encode(v.ims_uri).slice(0, 100);
d += 'B1' + b.length.toString(16).padStart(2, '0') + bytesToHex(Array.from(b));
}
if (v.media !== '' && v.media !== undefined)
d += 'FE01' + ((parseInt(v.media) || 0) & 0xFF).toString(16).padStart(2, '0');
return d.toUpperCase();
}
},
0x01: { title: 'Call connected', srcField: 'src',
fields: [
{ id: 'src', label: 'Source', type: 'select',
opts: [{v:'82',l:'terminal \u2014 near end (MT call)'},{v:'83',l:'network \u2014 far end (MO call)'}] },
{ id: 'ti', label: 'Transaction identifier', type: 'text', pattern: '[0-9A-Fa-f]{2}', placeholder: 'hex byte, e.g. 00' },
{ id: 'media', label: 'Media Type (optional)', type: 'select',
opts: [{v:'',l:'\u2014 not present \u2014'},{v:0,l:'00 \u2014 voice'},{v:1,l:'01 \u2014 video'}] },
],
build(v) {
let d = '1C01' + (v.ti || '00').replace(/[^0-9a-fA-F]/g, '').padStart(2, '0').slice(0, 2);
if (v.media !== '' && v.media !== undefined)
d += 'FE01' + ((parseInt(v.media) || 0) & 0xFF).toString(16).padStart(2, '0');
return d.toUpperCase();
}
},
0x02: { title: 'Call disconnected', srcField: 'src',
fields: [
{ id: 'src', label: 'Source', type: 'select',
opts: [{v:'82',l:'terminal \u2014 near end (terminal-initiated)'},{v:'83',l:'network \u2014 far end (network-initiated)'}] },
{ id: 'ti', label: 'Transaction identifier', type: 'text', pattern: '[0-9A-Fa-f]{2}', placeholder: 'hex byte, e.g. 00' },
{ id: 'cause_mode', label: 'Cause', type: 'select', onchange: 'updateCallCauseFields()',
opts: [{v:'',l:'\u2014 not present \u2014'},{v:'rlt',l:'radio link timeout (zero length)'},{v:'custom',l:'custom (hex)'}] },
{ id: 'cause', label: 'Cause bytes (hex)', type: 'text', pattern: '[0-9A-Fa-f]{2,60}', placeholder: 'e.g. 10', g: 'custom' },
{ id: 'media', label: 'Media Type (optional)', type: 'select',
opts: [{v:'',l:'\u2014 not present \u2014'},{v:0,l:'00 \u2014 voice'},{v:1,l:'01 \u2014 video'}] },
],
build(v) {
let d = '1C01' + (v.ti || '00').replace(/[^0-9a-fA-F]/g, '').padStart(2, '0').slice(0, 2);
const cause = (v.cause || '').replace(/[^0-9a-fA-F]/g, '');
if (v.cause_mode === 'rlt') d += '9A00';
else if (v.cause_mode === 'custom' && cause)
d += '9A' + (cause.length / 2).toString(16).padStart(2, '0') + cause;
if (v.media !== '' && v.media !== undefined)
d += 'FE01' + ((parseInt(v.media) || 0) & 0xFF).toString(16).padStart(2, '0');
return d.toUpperCase();
}
},
0x03: { title: 'Location status', 0x03: { title: 'Location status',
fields: [ fields: [
{ id: 'status', label: 'Service Status', type: 'select', { id: 'status', label: 'Service Status', type: 'select',
@@ -13081,7 +13163,13 @@ const EVENT_FORMS = {
], ],
build(v) { const b = new TextEncoder().encode(v.lang||'en'); return b.length.toString(16).padStart(2,'0') + bytesToHex(Array.from(b)); } build(v) { const b = new TextEncoder().encode(v.lang||'en'); return b.length.toString(16).padStart(2,'0') + bytesToHex(Array.from(b)); }
}, },
0x08: { title: 'Browser termination', note: 'not_yet' }, 0x08: { title: 'Browser termination',
fields: [
{ id: 'cause', label: 'Browser termination cause', type: 'select',
opts: [{v:0,l:'00 \u2014 user termination'},{v:1,l:'01 \u2014 error termination'}] },
],
build(v) { return 'B401' + ((parseInt(v.cause) || 0) & 0xFF).toString(16).padStart(2, '0'); }
},
0x09: null, 0x09: null,
0x0A: { title: 'Channel status', 0x0A: { title: 'Channel status',
fields: [ fields: [
@@ -13114,7 +13202,15 @@ const EVENT_FORMS = {
0x0E: null, 0x0E: null,
0x0F: null, 0x0F: null,
0x10: null, 0x10: null,
0x11: { title: 'I-WLAN Access Status', note: 'not_yet' }, 0x11: { title: 'I-WLAN Access Status',
fields: [
{ id: 'status', label: 'Access status', type: 'select',
opts: [{v:0,l:'00 \u2014 no current (I-)WLAN coverage'},
{v:1,l:'01 \u2014 coverage available, no current connection'},
{v:2,l:'02 \u2014 coverage available, connection on-going'}] },
],
build(v) { return 'CB01' + ((parseInt(v.status) || 0) & 0xFF).toString(16).padStart(2, '0'); }
},
0x12: { title: 'Network Rejection', 0x12: { title: 'Network Rejection',
fields: [ fields: [
{ id: 'reg_type', label: 'Registration type', type: 'select', onchange: 'updateNetRejectionFields()', { id: 'reg_type', label: 'Registration type', type: 'select', onchange: 'updateNetRejectionFields()',
@@ -13166,7 +13262,40 @@ const EVENT_FORMS = {
opts: [{v:0,l:'GSM'},{v:3,l:'UTRAN'},{v:8,l:'E-UTRAN'},{v:10,l:'NG-RAN'}] }], 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'); } build(v) { return 'BF01' + (v.tech|0).toString(16).padStart(2,'0'); }
}, },
0x15: { title: 'CSG cell selection', note: 'not_yet' }, 0x15: { title: 'CSG cell selection', src: '83',
fields: [
{ id: 'status', label: 'CSG cell selection status', type: 'select',
opts: [{v:0,l:'00 \u2014 not camped on an Allowed / Operator CSG cell'},
{v:1,l:'01 \u2014 camped on an Allowed / Operator CSG cell'}] },
{ id: 'mech', label: 'Selection mechanism', type: 'select',
opts: [{v:'',l:'\u2014 none \u2014'},{v:'manual',l:'manual CSG selection'},
{v:'other',l:'another CSG selection type'}] },
{ id: 'tech', label: 'Access Technology (optional)', type: 'select',
opts: [{v:'',l:'\u2014 not present \u2014'}].concat(ACCESSTECH) },
{ id: 'csg', label: 'CSG ID (optional, hex)', type: 'text', pattern: '[0-9A-Fa-f]{2,16}', placeholder: 'e.g. 00000001' },
{ id: 'hnb', label: 'HNB name (optional)', type: 'text', placeholder: 'e.g. MyCSG' },
{ id: 'mcc', label: 'MCC (PLMN ID, optional)', type: 'text', pattern: '\\d{3}', placeholder: 'e.g. 250' },
{ id: 'mnc', label: 'MNC', type: 'text', pattern: '\\d{2,3}', placeholder: 'e.g. 01' },
],
build(v) {
let d = '';
if (v.tech !== '' && v.tech !== undefined)
d += 'BF01' + ((parseInt(v.tech) || 0) & 0xFF).toString(16).padStart(2, '0');
const mech = v.mech === 'manual' ? 0x40 : (v.mech === 'other' ? 0x80 : 0x00);
d += 'D502' + ((parseInt(v.status) || 0) & 0xFF).toString(16).padStart(2, '0') +
(0x01 | mech).toString(16).padStart(2, '0');
if (v.csg) {
const h = v.csg.replace(/[^0-9a-fA-F]/g, '');
d += 'D6' + (h.length / 2).toString(16).padStart(2, '0') + h;
}
if (v.hnb) {
const b = new TextEncoder().encode(v.hnb).slice(0, 60);
d += 'D7' + b.length.toString(16).padStart(2, '0') + bytesToHex(Array.from(b));
}
if (v.mcc) d += '8903' + encPlmn(v.mcc, v.mnc || '01');
return d.toUpperCase();
}
},
0x16: null, 0x16: null,
0x17: null, 0x17: null,
0x1C: { title: 'Poll interval negotiation', 0x1C: { title: 'Poll interval negotiation',
@@ -13690,10 +13819,23 @@ function pysimEventSendForm(eventType) {
body.innerHTML = html; body.innerHTML = html;
btn.classList.remove('hidden'); btn.classList.remove('hidden');
if (eventType === 0x12) updateNetRejectionFields(); if (eventType === 0x12) updateNetRejectionFields();
if (eventType === 0x02) updateCallCauseFields();
} }
modal.classList.remove('hidden'); modal.classList.remove('hidden');
} }
// The call-disconnected form's Cause bytes are shown only for a custom cause
// (the radio-link-timeout form is the zero-length object).
function updateCallCauseFields() {
const sel = document.getElementById('ev-f-cause_mode');
if (!sel) return;
const g = sel.value;
document.querySelectorAll('#event-send-body .event-field').forEach(div => {
const groups = div.getAttribute('data-groups') || '';
div.style.display = (groups === '' || groups.indexOf(g) >= 0) ? '' : 'none';
});
}
function updateNetRejectionFields() { function updateNetRejectionFields() {
const sel = document.getElementById('ev-f-reg_type'); const sel = document.getElementById('ev-f-reg_type');
if (!sel) return; if (!sel) return;
@@ -13715,6 +13857,9 @@ async function pysimEventSend() {
const cfg = EVENT_FORMS[_eventFormType]; const cfg = EVENT_FORMS[_eventFormType];
let eventData = null; let eventData = null;
// The device-identities source (TS 102 223 8.7): a fixed per-event value
// or a form field (the call events differ near/far end).
let eventSrc = (cfg && cfg.src) || null;
if (cfg && cfg.build) { if (cfg && cfg.build) {
const values = {}; const values = {};
@@ -13733,6 +13878,7 @@ async function pysimEventSend() {
if (!valid) return; if (!valid) return;
try { try {
eventData = cfg.build(values); eventData = cfg.build(values);
if (cfg.srcField && values[cfg.srcField]) eventSrc = values[cfg.srcField];
} catch (e) { } catch (e) {
err.textContent = 'Error: ' + e.message; err.textContent = 'Error: ' + e.message;
err.classList.remove('hidden'); err.classList.remove('hidden');
@@ -13743,7 +13889,7 @@ async function pysimEventSend() {
btn.disabled = true; btn.disabled = true;
btn.textContent = t('Sending...'); btn.textContent = t('Sending...');
try { try {
const resp = await pysimFetch('/api/event-send', { event_type: _eventFormType, event_data: eventData }); const resp = await pysimFetch('/api/event-send', { event_type: _eventFormType, event_data: eventData, event_src: eventSrc });
if (resp.sw === '9000') { if (resp.sw === '9000') {
result.textContent = (eventData ? t('Event data sent.') : t('Event sent.')) + ' SW: ' + resp.sw; result.textContent = (eventData ? t('Event data sent.') : t('Event sent.')) + ' SW: ' + resp.sw;
if (resp.negotiation) { if (resp.negotiation) {
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v347'; const CACHE = 'simple-v348';
const URLS = [ const URLS = [
'index.html', 'index.html',
'help.html', 'help.html',
+37
View File
@@ -149,3 +149,40 @@ test('the data connection event can carry the current date-time', () => {
assert.strictEqual(build({ status: '0', type: '0', ti: '00', loc_status: '0' }), assert.strictEqual(build({ status: '0', type: '0', ti: '00', loc_status: '0' }),
'9D0100AA01001C01009B0100'); '9D0100AA01001C01009B0100');
}); });
test('encBcdDigits and eventAddressTlv build the EFADN number coding', () => {
assert.strictEqual(encBcdDigits('12345'), '2143F5');
assert.strictEqual(encBcdDigits('79001234567'), '9700214365F7');
// international number -> TON/NPI 0x91, length = 1 + 6 bytes
assert.strictEqual(eventAddressTlv('79001234567', '1'), '8607919700214365F7');
assert.strictEqual(eventAddressTlv('', '1'), '');
});
test('the call events build their 31.111 object sets', () => {
// MT call: source network, TI + Address + Subaddress + IMS URI + Media Type
assert.strictEqual(EVENT_FORMS[0x00].src, '83');
assert.strictEqual(EVENT_FORMS[0x00].build({ ti: '85', number: '79001234567', ton: '1',
sub: '12', ims_uri: 'sip:a@b', media: '1' }),
'1C01858607919700214365F7880112B1077369703A614062FE0101');
// call connected: source is the form's field, TI + optional Media Type
assert.strictEqual(EVENT_FORMS[0x01].srcField, 'src');
assert.strictEqual(EVENT_FORMS[0x01].build({ ti: '00', media: '0' }), '1C0100FE0100');
assert.strictEqual(EVENT_FORMS[0x01].build({ ti: '00', media: '' }), '1C0100');
// call disconnected: radio link timeout = zero-length cause, custom = bytes
assert.strictEqual(EVENT_FORMS[0x02].build({ ti: '00', cause_mode: 'rlt', media: '' }), '1C01009A00');
assert.strictEqual(EVENT_FORMS[0x02].build({ ti: '00', cause_mode: 'custom', cause: '10', media: '' }), '1C01009A0110');
});
test('the browser, WLAN and CSG events build their object sets', () => {
assert.strictEqual(EVENT_FORMS[0x08].build({ cause: '1' }), 'B40101');
assert.strictEqual(EVENT_FORMS[0x11].build({ status: '2' }), 'CB0102');
// CSG: source network, access tech + status/mechanism + ID + name + PLMN
assert.strictEqual(EVENT_FORMS[0x15].src, '83');
assert.strictEqual(EVENT_FORMS[0x15].build({ status: '1', mech: 'manual', tech: '8',
csg: '00000001', hnb: 'MyCSG', mcc: '250', mnc: '01' }),
'BF0108D5020141D60400000001D7054D79435347890352F010');
});
test('every event type has a parameter form now', () => {
assert.ok(!html.includes("note: 'not_yet'"), 'no event may stay unimplemented');
});
+13 -4
View File
@@ -4276,12 +4276,20 @@ def _verify_adm(scc, app, adm_hex):
'error': 'Security status not satisfied' if sw == '6982' else 'Error'} 'error': 'Security status not satisfied' if sw == '6982' else 'Error'}
def _send_event_download(scc, event_type, event_data=None, drain=True): def _send_event_download(scc, event_type, event_data=None, drain=True, src=None):
"""Send ENVELOPE(Event Download) for the given event type. """Send ENVELOPE(Event Download) for the given event type.
Builds: CLA C2 0000 Lc D6 [len] (99 01 [type] 82 02 82 81 [extra])""" Builds: CLA C2 0000 Lc D6 [len] (99 01 [type] 82 02 <src> 81 [extra]).
`src` is the source device identity (TS 102 223 8.7: '82' terminal,
'83' network); the default is the terminal."""
src_byte = 0x82
if src:
try:
src_byte = int(str(src), 16) & 0xFF
except (TypeError, ValueError):
src_byte = 0x82
inner = bytearray() inner = bytearray()
inner.extend([0x99, 0x01, event_type]) inner.extend([0x99, 0x01, event_type])
inner.extend([0x82, 0x02, 0x82, 0x81]) inner.extend([0x82, 0x02, src_byte, 0x81])
if event_data: if event_data:
inner.extend(event_data) inner.extend(event_data)
if len(inner) < 0x80: if len(inner) < 0x80:
@@ -6849,8 +6857,9 @@ class PysimHandler(BaseHTTPRequestHandler):
self._send_json({'error': 'event_type is required'}, 400) self._send_json({'error': 'event_type is required'}, 400)
return return
event_data = bytes.fromhex(event_data_hex) if event_data_hex else None event_data = bytes.fromhex(event_data_hex) if event_data_hex else None
event_src = body.get('event_src')
try: try:
data, sw = _send_event_download(scc, event_type, event_data) data, sw = _send_event_download(scc, event_type, event_data, src=event_src)
try: try:
ev_type = int(event_type) ev_type = int(event_type)
except (TypeError, ValueError): except (TypeError, ValueError):
+11 -2
View File
@@ -789,7 +789,7 @@ class TestProactiveDecode(unittest.TestCase):
class TestEventDownload(unittest.TestCase): class TestEventDownload(unittest.TestCase):
"""ENVELOPE (EVENT DOWNLOAD) assembly, TS 102 223 7.5.11.""" """ENVELOPE (EVENT DOWNLOAD) assembly, TS 102 223 7.5.11."""
def _send(self, event_type, event_data): def _send(self, event_type, event_data, src=None):
import pysim_simple_server.server as srv import pysim_simple_server.server as srv
calls = [] calls = []
@@ -802,7 +802,7 @@ class TestEventDownload(unittest.TestCase):
cat_cla = '80' cat_cla = '80'
_tp = Tp() _tp = Tp()
data, sw = srv._send_event_download(Scc(), event_type, event_data) data, sw = srv._send_event_download(Scc(), event_type, event_data, src=src)
return calls[0], sw return calls[0], sw
def test_channel_status_event(self): def test_channel_status_event(self):
@@ -817,6 +817,15 @@ class TestEventDownload(unittest.TestCase):
self.assertEqual(sw, '9000') self.assertEqual(sw, '9000')
self.assertEqual(apdu, '80c2000009d60799010582028281') self.assertEqual(apdu, '80c2000009d60799010582028281')
def test_event_source_device_identity_can_be_overridden(self):
# MT call / CSG cell selection come from the network (TS 102 223 8.7)
apdu, sw = self._send(0x00, None, src='83')
self.assertEqual(sw, '9000')
self.assertEqual(apdu, '80c2000009d60799010082028381')
# an invalid source falls back to the terminal
apdu, sw = self._send(0x00, None, src='zz')
self.assertEqual(apdu, '80c2000009d60799010082028281')
def test_event_with_long_data_uses_the_ber_long_form(self): def test_event_with_long_data_uses_the_ber_long_form(self):
# 128+ bytes of event data: the D6 length becomes '81' <len> (the # 128+ bytes of event data: the D6 length becomes '81' <len> (the
# 7.5.x tables allow 1 or 2 length bytes; a raw byte above 0x7F would # 7.5.x tables allow 1 or 2 length bytes; a raw byte above 0x7F would