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);
+});