From cafec549fab44c62091a284511faa86bea4ddcb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Thu, 1 Oct 2026 21:35:23 +0300 Subject: [PATCH] feat: the Data Connection Status Change event gets its parameter form (0x1D) EVENT_FORMS[0x1D] was missing, so its Send button posted event_data: null - the ENVELOPE carried only the event list and device identities, none of the event's data objects. The form now builds the TS 31.111 7.5.25.2 object set (tags per TS 101 220 / TS 31.111 9.3): data connection status (9D), connection type (AA), optional (E/5G)SM cause (AE), transaction identifier (1C), optional location information (13, CR flag 0 as the spec requires for this command), optional access technology (BF), location status (9B), optional APN (C7) and PDP/PDN/PDU type (0B). Adjacent spec fixes found while verifying: - the Access Technology Change options (0x0B) did not match the pinned TS 102 223 V18.3.0 8.61 coding (GSM Compact / GSM w-EGPRS / UTRAN w-HSDPA ...); they now use the shared ACCESSTECH list, whose two labels were also corrected (TIA/EIA-553-A, TIA/EIA-136-270); - _send_event_download() wrote the D6 length as a single byte - a long APN would overflow it; 128..255 bytes now use the BER long form (81 ). Tests: the 0x1D build (defaults, full form, fallbacks), the 8.61 option list, the long-form D6. Help EN/RU and both AGENTS files; sw cache simple-v342. --- frontend/help-ru.html | 1 + frontend/help.html | 1 + frontend/index.html | 66 ++++++++++++++++++++++++++---- frontend/sw.js | 2 +- frontend/tests/event_forms.test.js | 27 ++++++++++++ pysim_simple_server/server.py | 8 +++- tests/test_ota_helpers.py | 11 +++++ 7 files changed, 107 insertions(+), 9 deletions(-) diff --git a/frontend/help-ru.html b/frontend/help-ru.html index ec28c32..3fad878 100644 --- a/frontend/help-ru.html +++ b/frontend/help-ru.html @@ -504,6 +504,7 @@
  • Location Status — выпадающий список: Normal / Limited / No service (тег 9B)
  • Access Technology Change — 13 типов RAT (тег BF)
  • Network Rejection — полная адаптивная форма: тип регистрации (LU / GPRS / EPS / 5GS), поля местоположения (MCC, MNC, LAC, RAC, TAC), технология доступа и единый выпадающий список из 53 кодов причин (EMM, GMM, 5GMM и LU)
  • +
  • Data Connection Status Change (0x1D) — статус (successful / rejected / dropped-deactivated), тип соединения (PDP / PDN / PDU), необязательная причина (E/5G)SM, transaction identifier и location status, а также необязательные access technology, APN, тип PDP/PDN/PDU и location information (MCC/MNC + LAC + Cell ID) — TS 131 111 §7.5.25
  • Poll Interval Negotiation (0x1C) — предлагает интервал опроса в виде Duration (единица + значение); после отправки показывается ответ UICC (принято / отклонено / изменено + длительность), а значение изменено становится интервалом фонового опроса (TS 102 223 §7.5.22). Форма появляется, когда карта подписана на это событие.
  • Отправка события использует ENVELOPE(Event Download) по TS 102 223 / TS 131 111.

    diff --git a/frontend/help.html b/frontend/help.html index 58ce750..271c3c4 100644 --- a/frontend/help.html +++ b/frontend/help.html @@ -503,6 +503,7 @@
  • Location Status — dropdown: Normal / Limited / No service (tag 9B)
  • Access Technology Change — 13 RAT types (tag BF)
  • Network Rejection — 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
  • +
  • Data Connection Status Change (0x1D) — status (successful / rejected / dropped-deactivated), connection type (PDP / PDN / PDU), optional (E/5G)SM cause, transaction identifier and location status, plus optional access technology, APN, PDP/PDN/PDU type and location information (MCC/MNC + LAC + Cell ID) — TS 131 111 §7.5.25
  • Poll Interval Negotiation (0x1C) — proposes a poll interval as a Duration (unit + value); after sending, the UICC's answer is shown (accepted / rejected / modified + duration), and a modified duration becomes the background poll interval (TS 102 223 §7.5.22). The form appears when the card subscribed to this event.
  • Sending an event uses ENVELOPE(Event Download) per TS 102 223 / TS 131 111.

    diff --git a/frontend/index.html b/frontend/index.html index cd182bd..b86a63d 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -12978,6 +12978,14 @@ const REJECTION_CAUSES = [ {v:111,l:'6F \u2014 Protocol error, unspecified'}, ]; +// Access Technology coding (TS 102 223 V18.3.0 8.61, tags '3F'/'BF'): the +// single/multiple technology objects and the PLI qualifier share this list. +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:4,l:'TETRA'},{v:5,l:'TIA/EIA-95-B'},{v:6,l:'CDMA2000 1x'},{v:7,l:'CDMA2000 HRPD'}, + {v:8,l:'E-UTRAN'},{v:9,l:'eHRPD'},{v:10,l:'NG-RAN'},{v:11,l:'Satellite NG-RAN'},{v:12,l:'Satellite E-UTRAN'}, +]; + const EVENT_FORMS = { 0x00: { title: 'MT call', note: 'not_yet' }, 0x01: { title: 'Call connected', note: 'not_yet' }, @@ -13028,7 +13036,7 @@ const EVENT_FORMS = { 0x0B: { title: 'Access Technology Change', fields: [ { id: 'tech', label: 'Access Technology', type: 'select', - opts: [{v:0,l:'GSM'},{v:1,l:'GSM Compact'},{v:3,l:'UTRAN'},{v:4,l:'GSM w/EGPRS'},{v:5,l:'UTRAN w/HSDPA'},{v:6,l:'UTRAN w/HSUPA'},{v:7,l:'UTRAN w/HSPA+'},{v:8,l:'E-UTRAN'},{v:9,l:'E-UTRAN (4G)'},{v:10,l:'5G NR'},{v:11,l:'5G NR (NSA)'},{v:12,l:'Satellite'}] } + opts: ACCESSTECH } ], build(v) { return 'BF01' + (v.tech|0).toString(16).padStart(2,'0'); } }, @@ -13105,6 +13113,56 @@ const EVENT_FORMS = { return ('0402' + unit.toString(16).padStart(2, '0') + iv.toString(16).padStart(2, '0')).toUpperCase(); } }, + 0x1D: { title: 'Data connection status change', + fields: [ + { id: 'status', label: 'Data connection status', type: 'select', + opts: [{v:0,l:'00 \u2014 Data connection successful'}, + {v:1,l:'01 \u2014 Data connection rejected'}, + {v:2,l:'02 \u2014 Data connection dropped / deactivated'}] }, + { id: 'type', label: 'Data connection type', type: 'select', + opts: [{v:0,l:'00 \u2014 PDP connection'},{v:1,l:'01 \u2014 PDN connection'}, + {v:2,l:'02 \u2014 PDU connection'}] }, + { id: 'cause', label: '(E/5G)SM cause (optional)', type: 'number', min: '0', max: '255', placeholder: '0\u2013255, e.g. 26' }, + { id: 'ti', label: 'Transaction identifier', type: 'text', pattern: '[0-9A-Fa-f]{2}', placeholder: 'hex byte, e.g. 00' }, + { id: 'mcc', label: 'MCC (Location Information, optional)', type: 'text', pattern: '\\d{3}', placeholder: 'e.g. 250' }, + { id: 'mnc', label: 'MNC', type: 'text', pattern: '\\d{2,3}', placeholder: 'e.g. 01' }, + { id: 'lac', label: 'LAC', type: 'text', pattern: '[0-9A-Fa-f]{4}', placeholder: 'hex, e.g. 00FF' }, + { id: 'cell', label: 'Cell ID', type: 'text', pattern: '[0-9A-Fa-f]{4}', placeholder: 'hex, e.g. 0001' }, + { id: 'tech', label: 'Access Technology (optional)', type: 'select', + opts: [{v:'',l:'\u2014 not present \u2014'}].concat(ACCESSTECH) }, + { id: 'loc_status', label: 'Location status', type: 'select', + opts: [{v:0,l:'Normal service'},{v:1,l:'Limited service'},{v:2,l:'No service'}] }, + { id: 'apn', label: 'Network Access Name / APN (optional)', type: 'text', placeholder: 'e.g. internet' }, + { id: 'pdp_type', label: 'PDP/PDN/PDU type (optional)', type: 'select', + opts: [{v:'',l:'\u2014 not present \u2014'}, + {v:0,l:'00 \u2014 IPv4'},{v:1,l:'01 \u2014 IPv6'},{v:3,l:'03 \u2014 IPv4v6'}, + {v:4,l:'04 \u2014 PPP (PDP/PDN) / Unstructured (PDU)'}, + {v:5,l:'05 \u2014 non-IP (PDP/PDN) / Ethernet (PDU)'}] }, + ], + build(v) { + // TS 131 111 7.5.25.2 object order; the conditional objects (cause, + // location information, access technology, APN, PDP/PDN/PDU type) are + // omitted when left empty. + let d = '9D01' + ((parseInt(v.status) || 0) & 0xFF).toString(16).padStart(2, '0'); + d += 'AA01' + ((parseInt(v.type) || 0) & 0xFF).toString(16).padStart(2, '0'); + if (v.cause !== '' && v.cause !== undefined) + d += 'AE01' + ((parseInt(v.cause) || 0) & 0xFF).toString(16).padStart(2, '0'); + d += '1C01' + (v.ti || '00').replace(/[^0-9a-fA-F]/g, '').padStart(2, '0').slice(0, 2); + if (v.mcc) + d += '1307' + encPlmn(v.mcc, v.mnc || '01') + + (v.lac || '0000').toUpperCase() + (v.cell || '0001').toUpperCase(); + if (v.tech !== '' && v.tech !== undefined) + d += 'BF01' + ((parseInt(v.tech) || 0) & 0xFF).toString(16).padStart(2, '0'); + d += '9B01' + ((parseInt(v.loc_status) || 0) & 0xFF).toString(16).padStart(2, '0'); + if (v.apn) { + const b = new TextEncoder().encode(v.apn).slice(0, 100); + d += 'C7' + b.length.toString(16).padStart(2, '0') + bytesToHex(Array.from(b)); + } + if (v.pdp_type !== '' && v.pdp_type !== undefined) + d += '0B01' + ((parseInt(v.pdp_type) || 0) & 0xFF).toString(16).padStart(2, '0'); + return d.toUpperCase(); + } + }, }; const PLI_QUALIFIERS = [ @@ -15042,12 +15100,6 @@ function encImeisvPli(digits) { return h; } -const ACCESSTECH = [ - {v:0,l:'GSM'},{v:1,l:'GSM Compact'},{v:2,l:'TIA/EIA-533'},{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:8,l:'E-UTRAN'},{v:9,l:'eHRPD'},{v:10,l:'NG-RAN'},{v:11,l:'Satellite NG-RAN'},{v:12,l:'Satellite E-UTRAN'}, -]; - const PLI_CODECS = { '00': { tlv: '9305', decode(hex) { diff --git a/frontend/sw.js b/frontend/sw.js index c04eece..6db5692 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v341'; +const CACHE = 'simple-v342'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/event_forms.test.js b/frontend/tests/event_forms.test.js index 475d363..3c945fc 100644 --- a/frontend/tests/event_forms.test.js +++ b/frontend/tests/event_forms.test.js @@ -34,6 +34,8 @@ 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 ')); +eval(extractFunc(html, 'encPlmn')); +eval(extractFunc(html, 'bytesToHex')); test('CMD_NAMES decodes timer management and the BIP commands', () => { assert.strictEqual(CMD_NAMES['01'], 'REFRESH'); @@ -97,3 +99,28 @@ test('channel status event builds the B8 channel status TLV', () => { assert.strictEqual(build({ channel: '1', state: '0', info: '0' }), 'B8020100'); assert.strictEqual(build({ channel: '0', state: '64', info: '0' }), 'B8024000'); }); + +test('the data connection status change event builds the 7.5.25 object set', () => { + const build = EVENT_FORMS[0x1D].build; + assert.strictEqual(EVENT_FORMS[0x1D].note, undefined); + // the four mandatory objects; every conditional object omitted + assert.strictEqual(build({ status: '0', type: '0', ti: '00', loc_status: '0' }), + '9D0100AA01001C01009B0100'); + // full form: cause, location information, access technology, APN, type + assert.strictEqual( + build({ status: '1', type: '2', cause: '26', ti: '85', mcc: '250', mnc: '01', + lac: '00FF', cell: '0001', tech: '8', loc_status: '1', apn: 'internet', pdp_type: '3' }), + '9D0101AA0102AE011A1C0185130752F01000FF0001BF01089B0101C708696E7465726E65740B0103'); + // an empty TI falls back to 00; the location defaults fill missing fields + assert.strictEqual(build({ status: '2', type: '1', ti: '', mcc: '250', loc_status: '2' }), + '9D0102AA01011C0100130752F010000000019B0102'); +}); + +test('the access technology options follow the pinned 8.61 coding', () => { + const opts = EVENT_FORMS[0x0B].fields[0].opts; + assert.deepStrictEqual(opts.map(o => o.v), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + assert.strictEqual(opts[1].l, 'TIA/EIA-553-A'); + assert.strictEqual(opts[2].l, 'TIA/EIA-136-270'); + assert.strictEqual(opts[10].l, 'NG-RAN'); + assert.strictEqual(opts[12].l, 'Satellite E-UTRAN'); +}); diff --git a/pysim_simple_server/server.py b/pysim_simple_server/server.py index 9ca9e10..89ce0b2 100644 --- a/pysim_simple_server/server.py +++ b/pysim_simple_server/server.py @@ -4284,7 +4284,13 @@ def _send_event_download(scc, event_type, event_data=None, drain=True): inner.extend([0x82, 0x02, 0x82, 0x81]) if event_data: inner.extend(event_data) - d6_tlv = bytes([0xD6, len(inner)]) + bytes(inner) + if len(inner) < 0x80: + d6_tlv = bytes([0xD6, len(inner)]) + bytes(inner) + else: + # BER long form for 128..255 bytes: the length field is "1 or 2" per + # the TS 102 223 7.5.x ENVELOPE tables (a raw byte above 0x7F would be + # read as a long-form indicator) + d6_tlv = bytes([0xD6, 0x81, len(inner)]) + bytes(inner) env_hex = '%sc20000%02x%s' % (scc.cat_cla, len(d6_tlv), d6_tlv.hex()) sys.stderr.write('ENVELOPE(Event Download): type=0x%02x data=%s\n' % (event_type, event_data.hex() if event_data else '(none)')) data, sw = scc._tp.send_apdu(env_hex) diff --git a/tests/test_ota_helpers.py b/tests/test_ota_helpers.py index f8cff93..5f4e653 100644 --- a/tests/test_ota_helpers.py +++ b/tests/test_ota_helpers.py @@ -817,6 +817,17 @@ class TestEventDownload(unittest.TestCase): self.assertEqual(sw, '9000') self.assertEqual(apdu, '80c2000009d60799010582028281') + def test_event_with_long_data_uses_the_ber_long_form(self): + # 128+ bytes of event data: the D6 length becomes '81' (the + # 7.5.x tables allow 1 or 2 length bytes; a raw byte above 0x7F would + # be read as a long-form indicator) + payload = bytes(range(200)) + apdu, sw = self._send(0x1D, payload) + self.assertEqual(sw, '9000') + inner = bytes([0x99, 0x01, 0x1D, 0x82, 0x02, 0x82, 0x81]) + payload + d6 = bytes([0xD6, 0x81, len(inner)]) + inner + self.assertEqual(apdu, '80c20000%02x%s' % (len(d6), d6.hex())) + class TestTimerManagement(unittest.TestCase): """Terminal side of TIMER MANAGEMENT (TS 102 223 6.6.21, 6.8.13/14, 7.4).