diff --git a/frontend/help-ru.html b/frontend/help-ru.html index 842a6aa..38e648e 100644 --- a/frontend/help-ru.html +++ b/frontend/help-ru.html @@ -504,7 +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
  • +
  • Data Connection Status Change (0x1D) — статус (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) — 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 4e654f0..e980793 100644 --- a/frontend/help.html +++ b/frontend/help.html @@ -503,7 +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
  • +
  • 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 date-time, 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 c4f7be0..3fc13d7 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -9354,6 +9354,17 @@ function ramExpandedGroups(report) { return groups; } +// The "more occurrences" flag of one expanded-registry page pass (pure): a +// page that promised more data ('6310', or an accepted chained retry) is +// fulfilled by the next accepted page; it stays set when the follow-up was +// refused/failed or the page cap stopped the loop - only then is the listing +// "first page only". +function ramExpandedMore(promised, sw, page) { + if (!promised) return false; + if (page >= 8) return true; + return !ramRemoteSwOk(sw); +} + // Summary of the expanded-registry pass (pure): which query form the card // accepted per listing and where the run fell back to the compact listing. // Shown in the explorer so a fallback (and the missing package versions) is @@ -9636,7 +9647,7 @@ async function ramExplore(sp) { return res.por.decoded ? (res.por.decoded.last_response_data || '') : ''; }; let page = 0; - let more = false; + let promised = false; while (accepted && page++ < 8) { const apdu = ramExpandedQueryApdu(p1, form, page > 1); const what = page > 1 ? 'P2=03 (' + form + ')' : 'P2=02 (' + form + ')'; @@ -9660,8 +9671,11 @@ async function ramExplore(sp) { // 6310 signal with the GET RESPONSE's own SW); a refused retry // must not cost another packet (v3.7.0) if (sw !== '6310' && !(pending && ramRemoteSwOk(sw))) break; - more = true; + promised = true; } + // An unfulfilled 6310 only: a follow-up page that was accepted and + // ended the listing fulfils the promise. + const more = ramExpandedMore(promised, sw, page); const parsed = data ? parser(data) : []; if (parsed.length) { expandedForm = form; @@ -11761,7 +11775,7 @@ function cardsRender() { const tbody = document.getElementById('cards-tbody'); if (!tbody) return; if (!cards.length) { - tbody.innerHTML = '' + t('No cards defined.') + ''; + tbody.innerHTML = '' + t('No cards defined.') + ''; return; } // The preset of the equipped card (normalised ICCID match): its row is @@ -13021,6 +13035,20 @@ const REJECTION_CAUSES = [ // 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. +// Date-Time and Time zone object (TS 102 223 8.39): seven semi-octet BCD +// bytes in the TS 23.040 9.2.3.11 order (units in the high nibble) - YY MM +// DD hh mm ss plus the time zone in quarters of an hour, its sign in bit 3 +// of the first semi-octet (1 = west of GMT). +function eventDateTimeTlv(d) { + const semi = v => ((v % 10) << 4) | Math.floor(v / 10); + const hx = b => b.toString(16).padStart(2, '0'); + const offMin = -d.getTimezoneOffset(); // minutes east of UTC + const tz = semi(Math.round(Math.abs(offMin) / 15)) | (offMin < 0 ? 0x08 : 0); + return '2607' + hx(semi(d.getFullYear() % 100)) + hx(semi(d.getMonth() + 1)) + + hx(semi(d.getDate())) + hx(semi(d.getHours())) + hx(semi(d.getMinutes())) + + hx(semi(d.getSeconds())) + hx(tz); +} + 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'}, @@ -13165,6 +13193,8 @@ const EVENT_FORMS = { {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: 'datetime', label: 'Date-Time and Time zone (optional)', type: 'select', + opts: [{v:'',l:'\u2014 not present \u2014'},{v:'now',l:'current date-time (host clock)'}] }, { 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' }, @@ -13189,6 +13219,7 @@ const EVENT_FORMS = { 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.datetime === 'now') d += eventDateTimeTlv(new Date()); if (v.mcc) d += '1307' + encPlmn(v.mcc, v.mnc || '01') + (v.lac || '0000').toUpperCase() + (v.cell || '0001').toUpperCase(); diff --git a/frontend/sw.js b/frontend/sw.js index 052a540..7886a98 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v346'; +const CACHE = 'simple-v347'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/event_forms.test.js b/frontend/tests/event_forms.test.js index 3c945fc..1fc4ecf 100644 --- a/frontend/tests/event_forms.test.js +++ b/frontend/tests/event_forms.test.js @@ -124,3 +124,28 @@ test('the access technology options follow the pinned 8.61 coding', () => { assert.strictEqual(opts[10].l, 'NG-RAN'); assert.strictEqual(opts[12].l, 'Satellite E-UTRAN'); }); + +test('eventDateTimeTlv builds the 8.39 date-time object', () => { + const d = o => ({ + getFullYear: () => o.year, getMonth: () => o.month - 1, getDate: () => o.day, + getHours: () => o.hour, getMinutes: () => o.min, getSeconds: () => o.sec, + getTimezoneOffset: () => o.tzMin, + }); + // UTC+3, 2026-09-30 21:55:00 -> swapped BCD + 12 quarters + assert.strictEqual( + eventDateTimeTlv(d({ year: 2026, month: 9, day: 30, hour: 21, min: 55, sec: 0, tzMin: -180 })), + '260762900312550021'); + // UTC-2:30 -> 10 quarters with the sign bit (bit 3 of the first semi-octet) + assert.strictEqual( + eventDateTimeTlv(d({ year: 2026, month: 1, day: 5, hour: 8, min: 7, sec: 9, tzMin: 150 })), + '260762105080709009'); +}); + +test('the data connection event can carry the current date-time', () => { + const build = EVENT_FORMS[0x1D].build; + assert.match(build({ status: '0', type: '0', ti: '00', loc_status: '0', datetime: 'now' }), + /^9D0100AA01001C01002607[0-9A-F]{14}9B0100$/); + // left out by default + assert.strictEqual(build({ status: '0', type: '0', ti: '00', loc_status: '0' }), + '9D0100AA01001C01009B0100'); +}); diff --git a/frontend/tests/ram.test.js b/frontend/tests/ram.test.js index 8f51870..bb0079a 100644 --- a/frontend/tests/ram.test.js +++ b/frontend/tests/ram.test.js @@ -25,7 +25,7 @@ function extractFunc(src, name) { const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamBuildRowHex', 'ramFmtLifecycle', 'ramFmtVersion', 'ramMenuState', 'ramEntryKind', 'ramMenuHtml', 'ramRawTlvsHtml', 'ramNvLine', 'ramFmtPrivileges', 'ramRenderExploreHtml', 'ramMergeExpanded', 'ramStepLine', 'ramStepComponents', 'ramInstallFailHint', 'ramProbeParse', 'ramCompatVerdict', 'ramCompatProbeNote', 'ramCompatImportRows', 'ramCompatRowsHtml', 'ramCompatFailureHtml', 'ramCompatDetailHtml', 'lookupSw', 'ramExpandedDetailsInit', 'ramExpandedDetailsChanged', 'ramGetStatusApdu', 'ramDeleteApdu', 'stkParamsBuild', 'ramRemoteSwOk', 'spPorAccepted', 'ramIncrementCntr', 'ramDeleteFromExplorer', 'ramListingSpi2', 'ramRemoveFromExplorer', 'ramHasInstance', 'ramExpandedQueryApdu', '_parseRawElfEntry', '_parseRawAppEntry', 'ramParseElfStatus', 'ramParseAppStatus', 'ramParseModuleAids', 'parseTLV', '_parseE3Entry', '_parseMenuEntries', - 'ramExpandedReport', 'ramExpandedTags', 'ramExpandedGroups', 'ramExpandedElfForm', 'ramElfVersionHint', 'ramChainGetResponse', 'ramDeriveElfVersions', 'ramElfAppletCandidate', + 'ramExpandedReport', 'ramExpandedMore', 'ramExpandedTags', 'ramExpandedGroups', 'ramExpandedElfForm', 'ramElfVersionHint', 'ramChainGetResponse', 'ramDeriveElfVersions', 'ramElfAppletCandidate', 'spCntrLow', 'ramCntrLowHtml', 'ramCntrLowPresetIdx', 'ramShowCntrLow', 'spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck', 'spKeysetOptionsHtml', 'spPresetIdx', 'tarPresetIdx', @@ -1489,3 +1489,16 @@ test('SIM toolkit mode hides the UICC file-access row', () => { assert.ok(src.includes('rc-tk-access-row'), 'the access row must follow the CA/EA visibility'); }); + +test('ramExpandedMore marks only an unfulfilled 6310', () => { + // no promise -> nothing to report + assert.strictEqual(ramExpandedMore(false, '9000', 1), false); + // the follow-up page was accepted and ended the listing + assert.strictEqual(ramExpandedMore(true, '9000', 2), false); + // the follow-up page was refused + assert.strictEqual(ramExpandedMore(true, '6A86', 2), true); + // the page cap stopped the paging + assert.strictEqual(ramExpandedMore(true, '6310', 8), true); + // a transport failure (no remote status word) leaves it unfulfilled + assert.strictEqual(ramExpandedMore(true, 'index out of range', 2), true); +});