v3.23.1: the PLI dictionary defaults and the PLI codec tag fixes
An unconfigured PLI dictionary answered result-ok with no data object,
which confuses applications that expect data. The dictionary now has a
base layer of sensible defaults for the qualifiers the pinned ETSI specs
define (TS 102 223 8.x, tags per TS 101 220 V18.3.0 Table 7.23):
- location as the test network 001/01 + LAC 0000 (8.19),
- the classic test IMEI 490154203237518 as F-padded BCD (8.20) and its
IMEISV (8.74),
- language 'en' (8.45), Timing Advance (2E/AE), E-UTRAN (8.61; UTRAN +
E-UTRAN for the multiple-technology qualifier 0E),
- automatic search mode (8.75), a full battery (8.76).
An override set through /api/pli-dict wins, and clearing a field restores
its default (the PWA's Save re-renders the effective values; the GET returns
the override or the default for the full qualifier set). The date/time
qualifier stays empty so the host clock keeps answering it; the
3GPP/3GPP2-specific qualifiers (NMR, ESN, MEID, CSG/HNB, WLAN, slices,
CAG, ...) stay empty - the ETSI specs define no contents for them, and a
fabricated object would mislead the application more than no answer.
Verifying the values against the pinned tag table exposed pre-existing bugs
in the PWA's PLI inline codecs, fixed here:
- Timing Advance used C6 (a 3GPP2 tag) -> 2E/AE;
- IMEISV used 94 (the IMEI tag) -> 62/E2;
- Network Search Mode used AD (the Language tag) -> 65/E5, and its labels
were swapped ('00' Manual / '01' Automatic per 8.75);
- the battery field was a percentage where 8.76 is an enum ('00' very low
... '04' full, 'FE'/'FF');
- encImeiPli padded the odd 15th digit with 0 instead of F (TS 124 008).
Docs: help EN/RU, api.md, the guide. Tests: the defaults + the resolution
order (Python), the HTTP endpoint's defaults/override/clearing, the codec
tags/labels and a server-defaults drift guard (Node). 846 frontend +
829 Python; sw cache simple-v414.
This commit is contained in:
@@ -530,7 +530,7 @@
|
||||
<li><strong>06</strong> Access Technology · <strong>08</strong> IMEISV · <strong>09</strong> Search Mode</li>
|
||||
<li><strong>0A</strong> Battery · <strong>0E</strong> Multiple Access Technologies</li>
|
||||
</ul>
|
||||
<p class="text-sm mb-3">Значения хранятся на сервере до перезапуска. Когда карта выдаёт PLI, сервер вставляет значения словаря в TERMINAL RESPONSE — в том числе в ответ тест-скрипта.</p>
|
||||
<p class="text-sm mb-3">Значения хранятся на сервере до перезапуска. Для квалификаторов, определённых в спецификациях ETSI, заранее заданы разумные значения по умолчанию (местоположение — тестовая сеть 001/01, тестовый IMEI 490154203237518 и его IMEISV, язык «en», технология доступа E-UTRAN, автоматический поиск сети, полный заряд батареи): заданное значение переопределяет умолчание, а <strong>очистка поля возвращает умолчание</strong>; квалификатор даты/времени, пока пуст, отвечается часами хоста, а специфичные для 3GPP/3GPP2 квалификаторы (NMR, ESN, MEID, CSG/HNB, WLAN, slices, CAG, …) остаются пустыми — спецификации ETSI не определяют для них содержимое. Когда карта выдаёт PLI, сервер вставляет значения в TERMINAL RESPONSE — в том числе в ответ тест-скрипта.</p>
|
||||
|
||||
<h3 id="network-simulation" class="text-lg font-medium mb-2">8.7 Симуляция сети</h3>
|
||||
<p class="text-sm mb-2">Воспроизводит шаблоны записи реального телефона при смене сетевых условий (исследование трасс в <code class="font-mono text-sm">projects/UICC_NAA.md</code>): подключение EPS, потеря сервиса / ограниченный сервис, запрет роуминга, откат на 2G, приём SMS, перенастройка cell broadcast и AUTHENTICATE. По кнопке на сценарий; карта должна быть подключена.</p>
|
||||
|
||||
+1
-1
@@ -529,7 +529,7 @@
|
||||
<li><strong>06</strong> Access Technology · <strong>08</strong> IMEISV · <strong>09</strong> Search Mode</li>
|
||||
<li><strong>0A</strong> Battery · <strong>0E</strong> Multiple Access Technologies</li>
|
||||
</ul>
|
||||
<p class="text-sm mb-3">Values persist server-side until restart. When the card issues PLI, the server injects the dictionary values into the TERMINAL RESPONSE - also into a test-script expectation's answer.</p>
|
||||
<p class="text-sm mb-3">Values persist server-side until restart. Sensible defaults are pre-settled for the qualifiers the ETSI specs define (location as the test network 001/01, the test IMEI 490154203237518 and its IMEISV, language ‘en’, E-UTRAN access technology, automatic search mode, a full battery): a value you set overrides its default, and <strong>clearing the field restores it</strong>; the date/time qualifier is answered from the host clock while empty, and the 3GPP/3GPP2-specific qualifiers (NMR, ESN, MEID, CSG/HNB, WLAN, slices, CAG, …) stay empty - the ETSI specs define no contents for them. When the card issues PLI, the server injects the values into the TERMINAL RESPONSE - also into a test-script expectation's answer.</p>
|
||||
|
||||
<h3 id="network-simulation" class="text-lg font-medium mb-2">8.7 Network simulation</h3>
|
||||
<p class="text-sm mb-2">Replays the write patterns a real phone performs when the network condition changes (trace study in <code class="font-mono text-sm">projects/UICC_NAA.md</code>): EPS attach, service loss / limited service, roaming denial, 2G fallback, SMS delivery, cell-broadcast reconfiguration and AUTHENTICATE. One button per scenario; the card must be equipped.</p>
|
||||
|
||||
+21
-12
@@ -1904,7 +1904,7 @@
|
||||
// ===== Version =====
|
||||
// Single source of truth for the PWA version: shown in the header and used
|
||||
// by the server version check in pysimConnect().
|
||||
const SIMPLE_VERSION = '3.23.0';
|
||||
const SIMPLE_VERSION = '3.23.1';
|
||||
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||
|
||||
// ===== view history (the browser Back/Forward buttons) =====
|
||||
@@ -17753,8 +17753,10 @@ function encImeiPli(digits) {
|
||||
const d = digits.replace(/\D/g, '').slice(0, 15);
|
||||
if (d.length < 15) return '';
|
||||
let h = '';
|
||||
for (let i = 0; i < 15; i += 2)
|
||||
for (let i = 0; i < 14; i += 2)
|
||||
h += (parseInt(d[i+1])<<4 | parseInt(d[i])).toString(16).padStart(2, '0');
|
||||
// the odd 15th digit takes the low nibble, F pads the high one
|
||||
h += (0xF0 | parseInt(d[14])).toString(16).padStart(2, '0');
|
||||
return h;
|
||||
}
|
||||
|
||||
@@ -17851,12 +17853,12 @@ const PLI_CODECS = {
|
||||
{id:'lang',label:'Language',type:'text',pattern:'[a-zA-Z]{2,3}',placeholder:'en'},
|
||||
],
|
||||
},
|
||||
'05': { tlv: 'C602',
|
||||
'05': { tlv: 'AE02',
|
||||
decode(hex) {
|
||||
const v = hex.replace(/\s/g, '').slice(4); if (v.length < 4) return null;
|
||||
return { status: parseInt(v[0]+v[1],16).toString(), ta: parseInt(v[2]+v[3],16).toString() };
|
||||
},
|
||||
encode(v) { return 'C602' + (parseInt(v.status)||0).toString(16).padStart(2,'0') + (parseInt(v.ta)||0).toString(16).padStart(2,'0'); },
|
||||
encode(v) { return 'AE02' + (parseInt(v.status)||0).toString(16).padStart(2,'0') + (parseInt(v.ta)||0).toString(16).padStart(2,'0'); },
|
||||
fields: [
|
||||
{id:'status',label:'ME Status',type:'number',min:'0',max:'255',placeholder:'0'},
|
||||
{id:'ta',label:'Timing Advance',type:'number',min:'0',max:'255',placeholder:'0'},
|
||||
@@ -17870,7 +17872,7 @@ const PLI_CODECS = {
|
||||
encode(v) { return 'BF01' + (parseInt(v.tech)||0).toString(16).padStart(2,'0'); },
|
||||
fields: [{id:'tech',label:'Access Technology',type:'select',opts:ACCESSTECH}],
|
||||
},
|
||||
'08': { tlv: '9408',
|
||||
'08': { tlv: 'E208',
|
||||
decode(hex) {
|
||||
const v = hex.replace(/\s/g, '').slice(4); if (v.length < 16) return null;
|
||||
return { imeisv: decImei(v) + v.slice(14,16).toUpperCase() };
|
||||
@@ -17881,28 +17883,33 @@ const PLI_CODECS = {
|
||||
let h = '';
|
||||
for (let i = 0; i < 16; i += 2)
|
||||
h += (parseInt(raw[i+1])<<4 | parseInt(raw[i])).toString(16).padStart(2,'0');
|
||||
return '9408' + h;
|
||||
return 'E208' + h;
|
||||
},
|
||||
fields: [{id:'imeisv',label:'IMEISV',type:'text',pattern:'\\d{16}',placeholder:'16 digits'}],
|
||||
},
|
||||
'09': { tlv: 'AD01',
|
||||
'09': { tlv: 'E501',
|
||||
decode(hex) {
|
||||
const v = hex.replace(/\s/g, '').slice(4); if (v.length < 2) return null;
|
||||
return { mode: parseInt(v[0]+v[1],16).toString() };
|
||||
},
|
||||
encode(v) {
|
||||
return 'AD01' + (parseInt(v.mode)||0).toString(16).padStart(2,'0');
|
||||
return 'E501' + (parseInt(v.mode)||0).toString(16).padStart(2,'0');
|
||||
},
|
||||
fields: [{id:'mode',label:'Search Mode',type:'select',
|
||||
opts:[{v:0,l:'Automatic'},{v:1,l:'Manual'}]}],
|
||||
opts:[{v:0,l:'Manual'},{v:1,l:'Automatic'}]}],
|
||||
},
|
||||
'0A': { tlv: '6301',
|
||||
decode(hex) {
|
||||
const v = hex.replace(/\s/g, '').slice(4); if (v.length < 2) return null;
|
||||
return { charge: parseInt(v[0]+v[1],16).toString() };
|
||||
},
|
||||
encode(v) { return '6301' + (parseInt(v.charge)||50).toString(16).padStart(2,'0'); },
|
||||
fields: [{id:'charge',label:'Charge state (%)',type:'number',min:'0',max:'100',placeholder:'50'}],
|
||||
encode(v) {
|
||||
const n = (v.charge === undefined || v.charge === '') ? 4 : parseInt(v.charge, 10);
|
||||
return '6301' + (n & 0xFF).toString(16).padStart(2,'0').toUpperCase();
|
||||
},
|
||||
fields: [{id:'charge',label:'Battery state',type:'select',
|
||||
opts:[{v:0,l:'Very low'},{v:1,l:'Low'},{v:2,l:'Average'},{v:3,l:'Good'},
|
||||
{v:4,l:'Full'},{v:0xFE,l:'Not battery powered'},{v:0xFF,l:'Unknown / charging'}]}],
|
||||
},
|
||||
'0E': { tlv: 'BF',
|
||||
decode(hex) {
|
||||
@@ -17937,7 +17944,8 @@ async function pysimPliRender() {
|
||||
+ '<span class="flex-1 min-w-0 text-[13px] leading-snug">' + esc(q.name) + '</span>'
|
||||
+ '</div>'
|
||||
+ '<div class="flex items-center gap-1 mt-0.5 ml-8">'
|
||||
+ '<input type="text" id="pli-v-' + q.code + '" value="' + esc(val) + '" placeholder="hex"'
|
||||
+ '<input type="text" id="pli-v-' + q.code + '" value="' + esc(val) + '"'
|
||||
+ ' placeholder="' + (q.code === '03' ? '(host clock)' : 'hex') + '"'
|
||||
+ ' class="flex-1 min-w-0 border border-gray-300 dark:border-slate-600 text-xs rounded px-1.5 py-1 dark:bg-slate-800 font-mono">';
|
||||
if (hasCodec) {
|
||||
html += '<span onclick="pysimPliDecodeRow(\'' + q.code + '\')" id="pli-tog-' + q.code + '"'
|
||||
@@ -18041,6 +18049,7 @@ async function pysimPliSave() {
|
||||
});
|
||||
try {
|
||||
await pysimFetch('/api/pli-dict', data);
|
||||
pysimPliRender(); // a cleared field shows its default again
|
||||
btn.textContent = t('Saved');
|
||||
btn.classList.add('bg-emerald-700');
|
||||
setTimeout(() => { btn.textContent = t('Save'); btn.classList.remove('bg-emerald-700'); btn.disabled = false; }, 1500);
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v413';
|
||||
const CACHE = 'simple-v414';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -37,3 +37,44 @@ test('the PLI date/time codec uses the 8.39 TP-SCTS coding', () => {
|
||||
// a truncated value still returns null instead of throwing
|
||||
assert.strictEqual(c.decode('2607123'), null);
|
||||
});
|
||||
|
||||
test('the PLI codec tags follow TS 101 220 Table 7.23', () => {
|
||||
// 05 Timing Advance: 2E/AE (C6 is a 3GPP2 tag)
|
||||
assert.strictEqual(PLI_CODECS['05'].tlv, 'AE02');
|
||||
assert.strictEqual(PLI_CODECS['05'].encode({ status: '0', ta: '0' }), 'AE020000');
|
||||
// 08 IMEISV: 62/E2 (94 is the IMEI tag)
|
||||
assert.strictEqual(PLI_CODECS['08'].tlv, 'E208');
|
||||
assert.strictEqual(PLI_CODECS['08'].encode({ imeisv: '4901542032375108' }),
|
||||
'E2089410450223731580');
|
||||
// 09 Network search mode: 65/E5, '00' Manual / '01' Automatic (8.75)
|
||||
assert.strictEqual(PLI_CODECS['09'].tlv, 'E501');
|
||||
assert.strictEqual(PLI_CODECS['09'].fields[0].opts[0].l, 'Manual');
|
||||
assert.strictEqual(PLI_CODECS['09'].fields[0].opts[1].l, 'Automatic');
|
||||
// 0A Battery state: 63/E3 is an enum, not a percentage (8.76)
|
||||
assert.strictEqual(PLI_CODECS['0A'].tlv, '6301');
|
||||
assert.strictEqual(PLI_CODECS['0A'].encode({ charge: '4' }), '630104');
|
||||
assert.strictEqual(PLI_CODECS['0A'].encode({ charge: '0' }), '630100');
|
||||
});
|
||||
|
||||
test('the IMEI encoder pads the odd 15th digit with F (TS 124 008)', () => {
|
||||
// the encoder's digits are lowercase (the server accepts either case)
|
||||
assert.strictEqual(encImeiPli('490154203237518'), '94104502237315f8');
|
||||
assert.strictEqual(decImei('94104502237315F8'), '490154203237518');
|
||||
});
|
||||
|
||||
test('the server PLI defaults decode through their codecs', () => {
|
||||
// mirrors PLI_DEFAULTS in server.py (v3.23.1; a drift guard for the codecs)
|
||||
const defaults = {
|
||||
'00': '930500F1100000', '01': '940894104502237315F8',
|
||||
'04': 'AD02656E', '05': 'AE020000', '06': 'BF0108',
|
||||
'08': 'E2089410450223731580', '09': 'E50101',
|
||||
'0A': '630104', '0E': 'BF020308',
|
||||
};
|
||||
for (const [code, hex] of Object.entries(defaults)) {
|
||||
assert.ok(PLI_CODECS[code].decode(hex), code + ' must decode');
|
||||
}
|
||||
assert.strictEqual(PLI_CODECS['01'].decode('940894104502237315F8').imei,
|
||||
'490154203237518');
|
||||
assert.deepStrictEqual(PLI_CODECS['09'].decode('E50101'), { mode: '1' });
|
||||
assert.deepStrictEqual(PLI_CODECS['0A'].decode('630104'), { charge: '4' });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user