9ef1a59f2c
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.
81 lines
3.8 KiB
JavaScript
81 lines
3.8 KiB
JavaScript
const { test } = require('node:test');
|
|
const assert = require('node:assert');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
|
|
|
|
function extractBlock(startMarker, endMarker) {
|
|
const start = html.indexOf(startMarker);
|
|
const end = html.indexOf(endMarker, start);
|
|
if (start < 0 || end < 0) throw new Error('block not found: ' + startMarker);
|
|
return html.slice(start, end);
|
|
}
|
|
|
|
// The codec map's select fields reference ACCESSTECH; only the date/time
|
|
// codec is exercised here, but the map literal still evaluates that list.
|
|
eval(extractBlock('const ACCESSTECH = [', 'const EVENT_FORMS = {').replace(/^const /gm, 'var '));
|
|
eval(extractBlock('// ===== PLI data dictionary =====', 'async function pysimPliRender')
|
|
.replace(/^const /gm, 'var '));
|
|
|
|
test('the PLI date/time codec uses the 8.39 TP-SCTS coding', () => {
|
|
const c = PLI_CODECS['03'];
|
|
assert.strictEqual(c.tlv, '2607');
|
|
// +03:00: swapped semi-octet BCD digits, the time zone in quarters of an
|
|
// hour (0x21 = 12 quarters) - the eventDateTimeTlv / _encode_scts vectors
|
|
assert.strictEqual(c.encode({ dt: '2026-09-30', tm: '21:55:00', tz: '+03:00' }),
|
|
'260762900312550021');
|
|
assert.deepStrictEqual(c.decode('260762900312550021'),
|
|
{ dt: '2026-09-30', tm: '21:55:00', tz: '+03:00' });
|
|
// negative zones carry the sign bit 0x08: -2:30 = 10 quarters -> 0x09
|
|
assert.strictEqual(c.encode({ dt: '2026-01-05', tm: '08:07:09', tz: '-02:30' }),
|
|
'260762105080709009');
|
|
assert.deepStrictEqual(c.decode('260762105080709009'),
|
|
{ dt: '2026-01-05', tm: '08:07:09', tz: '-02:30' });
|
|
// 'FF' time zone = unknown (TS 123 040 9.2.3.11), never a bogus offset
|
|
assert.strictEqual(c.decode('2607629003125500FF').tz, 'unknown');
|
|
// 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' });
|
|
});
|