ui: the Data Connection event gains the date-time object; the "more" flag is precise

- The 0x1D event form now offers the Date-Time and Time zone object
  (TS 102 223 8.39): an optional select that includes the host clock's
  date-time, built by eventDateTimeTlv() as seven semi-octet BCD bytes in the
  TS 23.040 9.2.3.11 order (units high, tens low) with the time zone in
  quarters of an hour and its sign in bit 3 of the first semi-octet.
- The Cards list's empty-state colspan is 8 now (the table has had 8 columns
  since the keyset refactor, not 10).
- The expanded-registry "(first page only)" note marks only an unfulfilled
  6310: ramExpandedMore() keeps the promise set when the follow-up page was
  refused/failed or the page cap stopped the paging, and clears it when an
  accepted page ended the listing (a completed multi-page listing was
  annotated wrongly before).

Tests: eventDateTimeTlv (fixed dates, both timezone signs), the 0x1D build
with and without the date-time, and ramExpandedMore's cases.  Help EN/RU and
both AGENTS files; sw cache simple-v347.
This commit is contained in:
2026-10-01 22:07:24 +03:00
parent 0cccd62230
commit 994ee6d2a3
6 changed files with 76 additions and 7 deletions
+1 -1
View File
@@ -504,7 +504,7 @@
<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>Data Connection Status Change</strong> (0x1D) &mdash; статус (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) &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>
<p class="text-sm mb-3">Отправка события использует <code class="font-mono text-sm">ENVELOPE(Event Download)</code> по TS 102 223 / TS 131 111.</p> <p class="text-sm mb-3">Отправка события использует <code class="font-mono text-sm">ENVELOPE(Event Download)</code> по TS 102 223 / TS 131 111.</p>
+1 -1
View File
@@ -503,7 +503,7 @@
<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>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 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>
<p class="text-sm mb-3">Sending an event uses <code class="font-mono text-sm">ENVELOPE(Event Download)</code> per TS 102 223 / TS 131 111.</p> <p class="text-sm mb-3">Sending an event uses <code class="font-mono text-sm">ENVELOPE(Event Download)</code> per TS 102 223 / TS 131 111.</p>
+34 -3
View File
@@ -9354,6 +9354,17 @@ function ramExpandedGroups(report) {
return groups; 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 // 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. // 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 // 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 || '') : ''; return res.por.decoded ? (res.por.decoded.last_response_data || '') : '';
}; };
let page = 0; let page = 0;
let more = false; let promised = false;
while (accepted && page++ < 8) { while (accepted && page++ < 8) {
const apdu = ramExpandedQueryApdu(p1, form, page > 1); const apdu = ramExpandedQueryApdu(p1, form, page > 1);
const what = page > 1 ? 'P2=03 (' + form + ')' : 'P2=02 (' + form + ')'; 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 // 6310 signal with the GET RESPONSE's own SW); a refused retry
// must not cost another packet (v3.7.0) // must not cost another packet (v3.7.0)
if (sw !== '6310' && !(pending && ramRemoteSwOk(sw))) break; 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) : []; const parsed = data ? parser(data) : [];
if (parsed.length) { if (parsed.length) {
expandedForm = form; expandedForm = form;
@@ -11761,7 +11775,7 @@ function cardsRender() {
const tbody = document.getElementById('cards-tbody'); const tbody = document.getElementById('cards-tbody');
if (!tbody) return; if (!tbody) return;
if (!cards.length) { if (!cards.length) {
tbody.innerHTML = '<tr><td colspan="10" class="py-2 text-gray-400">' + t('No cards defined.') + '</td></tr>'; tbody.innerHTML = '<tr><td colspan="8" class="py-2 text-gray-400">' + t('No cards defined.') + '</td></tr>';
return; return;
} }
// The preset of the equipped card (normalised ICCID match): its row is // 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 // 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. // 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 = [ 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'},
@@ -13165,6 +13193,8 @@ const EVENT_FORMS = {
{v:2,l:'02 \u2014 PDU 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: '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: '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: '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: '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: '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) if (v.cause !== '' && v.cause !== undefined)
d += 'AE01' + ((parseInt(v.cause) || 0) & 0xFF).toString(16).padStart(2, '0'); 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); 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) if (v.mcc)
d += '1307' + encPlmn(v.mcc, v.mnc || '01') + d += '1307' + encPlmn(v.mcc, v.mnc || '01') +
(v.lac || '0000').toUpperCase() + (v.cell || '0001').toUpperCase(); (v.lac || '0000').toUpperCase() + (v.cell || '0001').toUpperCase();
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v346'; const CACHE = 'simple-v347';
const URLS = [ const URLS = [
'index.html', 'index.html',
'help.html', 'help.html',
+25
View File
@@ -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[10].l, 'NG-RAN');
assert.strictEqual(opts[12].l, 'Satellite E-UTRAN'); 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');
});
+14 -1
View File
@@ -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', 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', 'stkParamsBuild', 'ramRemoteSwOk', 'spPorAccepted', 'ramIncrementCntr', 'ramDeleteFromExplorer', 'ramListingSpi2', 'ramRemoveFromExplorer', 'ramHasInstance', 'ramExpandedQueryApdu',
'_parseRawElfEntry', '_parseRawAppEntry', 'ramParseElfStatus', 'ramParseAppStatus', 'ramParseModuleAids', 'parseTLV', '_parseE3Entry', '_parseMenuEntries', '_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', 'spCntrLow', 'ramCntrLowHtml', 'ramCntrLowPresetIdx', 'ramShowCntrLow',
'spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck', 'spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck',
'spKeysetOptionsHtml', 'spPresetIdx', 'tarPresetIdx', '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'), assert.ok(src.includes('rc-tk-access-row'),
'the access row must follow the CA/EA visibility'); '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);
});