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).