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:
+12
-5
@@ -1477,17 +1477,24 @@ Lists the PROVIDE LOCAL INFORMATION qualifier codes with their names.
|
||||
|
||||
### `GET /api/pli-dict`
|
||||
|
||||
Returns the current PLI data dictionary as a qualifier-code map.
|
||||
Returns the effective PLI data dictionary as a qualifier-code map: the
|
||||
operator's override, else the built-in default (the qualifiers whose objects
|
||||
the pinned ETSI specs define - location as the test network 001/01, the test
|
||||
IMEI/IMEISV, language, access technology, search mode, battery; v3.23.1).
|
||||
The date/time qualifier stays empty - the host clock answers it while no
|
||||
override is set.
|
||||
|
||||
```json
|
||||
{"00": "0291...", "0A": "64"}
|
||||
{"00": "930500F1100000", "0A": "630104", "03": ""}
|
||||
```
|
||||
|
||||
### `POST /api/pli-dict`
|
||||
|
||||
Updates dictionary entries. Body is a map of qualifier code to hex value; keys
|
||||
must be known qualifiers and values valid hex, otherwise they are ignored.
|
||||
Returns the updated dictionary.
|
||||
Updates dictionary entries. Body is a map of qualifier code to hex value;
|
||||
keys must be known qualifiers and values valid hex, otherwise they are
|
||||
ignored. A non-empty value becomes an override; an **empty value clears the
|
||||
override**, so the built-in default (if any) applies again. Returns the
|
||||
updated effective dictionary.
|
||||
|
||||
### `POST /api/scp81/bip`
|
||||
|
||||
|
||||
@@ -481,8 +481,9 @@ identities, **Result**, Duration, Text string, Item identifier, …):
|
||||
|
||||
The terminal's own command-specific objects ride with the scripted result
|
||||
unless `raw` overrides them: a successful PROVIDE LOCAL INFORMATION answer
|
||||
takes the TR Config **PLI dictionary** entry for its qualifier (the current
|
||||
date and time for the date/time qualifier `03`), and a POLL INTERVAL answer
|
||||
takes the TR Config **PLI dictionary** value for its qualifier - the entry
|
||||
or its pre-settled default (v3.23.1; the current date and time for the
|
||||
date/time qualifier `03`), and a POLL INTERVAL answer
|
||||
echoes the current interval (TS 102 223 §6.8.4) - the same objects the
|
||||
terminal's own answers carry.
|
||||
|
||||
|
||||
@@ -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' });
|
||||
});
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "pysim-simple-server"
|
||||
version = "3.23.0"
|
||||
version = "3.23.1"
|
||||
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
||||
requires-python = ">=3.8"
|
||||
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
||||
|
||||
@@ -36,7 +36,7 @@ from osmocom.tlv import BER_TLV_IE
|
||||
from osmocom.utils import rpad
|
||||
|
||||
|
||||
VERSION = '3.23.0'
|
||||
VERSION = '3.23.1'
|
||||
|
||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||
|
||||
@@ -2565,7 +2565,40 @@ PLI_QUALIFIER_NAMES = {
|
||||
0x1A: 'Supported Radio Access Technologies',
|
||||
}
|
||||
|
||||
_PLI_DATA = {q: '' for q in PLI_QUALIFIER_NAMES}
|
||||
# Sensible PLI answers for an unconfigured dictionary - the qualifiers whose
|
||||
# objects the pinned ETSI specs define (TS 102 223 8.x, tags per TS 101 220
|
||||
# V18.3.0 Table 7.23). They are the *base layer*: a value set through
|
||||
# /api/pli-dict overrides its default, and clearing the field restores it
|
||||
# (v3.23.1). Qualifier 03 stays empty - the date/time is answered from the
|
||||
# host clock (see _pli_data_hex). The 3GPP/3GPP2-specific qualifiers (02
|
||||
# NMR, 07 ESN, 0B MEID, 0C WSID, 0D Broadcast Network info, 0F/10 multi-RAT,
|
||||
# 11-17 CSG/HNB/WLAN/slices/CAG, 1A Supported RATs) stay empty: the ETSI
|
||||
# specs define no contents for them ("reserved for 3GPP/3GPP2") and a
|
||||
# fabricated object would mislead the application more than no answer.
|
||||
PLI_DEFAULTS = {
|
||||
# Location Information (8.19): PLMN 001/01 (the test network, TS 23.003)
|
||||
# + LAC 0000
|
||||
0x00: '9305' + '00F110' + '0000',
|
||||
# IMEI (8.20): the classic test IMEI 490154203237518, nibble-swapped
|
||||
# BCD with the odd 15th digit padded by F (TS 124 008 Mobile Identity)
|
||||
0x01: '9408' + '94104502237315F8',
|
||||
# Language (8.45): 'en' in the SMS default alphabet (bit 8 clear)
|
||||
0x04: 'AD02' + '656E',
|
||||
# Timing Advance (3GPP; tag 2E/AE): ME status 00 + TA 00
|
||||
0x05: 'AE02' + '0000',
|
||||
# Access technology (8.61): E-UTRAN
|
||||
0x06: 'BF01' + '08',
|
||||
# IMEISV (8.74, tag 62/E2): the test IMEI + SVN 08
|
||||
0x08: 'E208' + '9410450223731580',
|
||||
# Network search mode (8.75, tag 65/E5): automatic
|
||||
0x09: 'E501' + '01',
|
||||
# Battery state (8.76, tag 63/E3): full
|
||||
0x0A: '6301' + '04',
|
||||
# Access technologies (8.61): UTRAN + E-UTRAN
|
||||
0x0E: 'BF02' + '0308',
|
||||
}
|
||||
|
||||
_PLI_DATA = {q: '' for q in PLI_QUALIFIER_NAMES} # the operator overrides
|
||||
|
||||
_BIP = httpota.BipTerminal()
|
||||
# Active BIP session, shared by the SCP81 listener and the Simulator's
|
||||
@@ -4139,12 +4172,17 @@ def _pli_data_hex(cmd_qual, dt=None):
|
||||
"""Data object(s) for a PROVIDE LOCAL INFORMATION TERMINAL RESPONSE, as
|
||||
hex - or '' when there is nothing to send.
|
||||
|
||||
The TR Config dictionary entry (if any) wins. Otherwise the date/time
|
||||
qualifier is answered with the current date and time from the host clock
|
||||
(TS 102 223 6.4.15: "The terminal shall return the current date and time
|
||||
as set by the user"), coded as the Date-Time and Time zone object (8.39).
|
||||
`dt` is a test seam, the clock is read live otherwise."""
|
||||
The TR Config dictionary override (if any) wins, then the built-in
|
||||
default (PLI_DEFAULTS) - clearing a field restores its default. Without
|
||||
either, the date/time qualifier is answered with the current date and
|
||||
time from the host clock (TS 102 223 6.4.15: "The terminal shall return
|
||||
the current date and time as set by the user"), coded as the Date-Time
|
||||
and Time zone object (8.39). `dt` is a test seam, the clock is read live
|
||||
otherwise."""
|
||||
val = str(_PLI_DATA.get(cmd_qual, '') or '').strip()
|
||||
if val:
|
||||
return val
|
||||
val = PLI_DEFAULTS.get(cmd_qual, '')
|
||||
if val:
|
||||
return val
|
||||
if cmd_qual == 0x03:
|
||||
@@ -8097,7 +8135,10 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
self._send_json(qualifiers)
|
||||
self._log_resp(qualifiers)
|
||||
elif self.path == '/api/pli-dict':
|
||||
resp = {('%02X' % q): v for q, v in _PLI_DATA.items()}
|
||||
# the effective values: the override or the built-in default
|
||||
# (the date/time qualifier stays empty - the host clock answers it)
|
||||
resp = {('%02X' % q): (_PLI_DATA.get(q) or PLI_DEFAULTS.get(q, ''))
|
||||
for q in PLI_QUALIFIER_NAMES}
|
||||
self._send_json(resp)
|
||||
self._log_resp(resp)
|
||||
elif self.path == '/api/poll-status':
|
||||
@@ -8855,11 +8896,16 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
try:
|
||||
code = int(k, 16)
|
||||
if code in _PLI_DATA:
|
||||
bytes.fromhex('') if not v else bytes.fromhex(v)
|
||||
_PLI_DATA[code] = v
|
||||
if v:
|
||||
bytes.fromhex(v) # valid hex only
|
||||
_PLI_DATA[code] = v
|
||||
else:
|
||||
# clearing the field restores the default
|
||||
_PLI_DATA.pop(code, None)
|
||||
except (ValueError, KeyError):
|
||||
pass
|
||||
resp = {('%02X' % q): v for q, v in _PLI_DATA.items()}
|
||||
resp = {('%02X' % q): (_PLI_DATA.get(q) or PLI_DEFAULTS.get(q, ''))
|
||||
for q in PLI_QUALIFIER_NAMES}
|
||||
self._send_json(resp)
|
||||
self._log_resp(resp)
|
||||
elif self.path == '/api/poll-toggle':
|
||||
|
||||
@@ -115,6 +115,57 @@ if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
|
||||
class PliDictHttpTests(unittest.TestCase):
|
||||
"""The PLI dictionary's defaults, overrides and clearing (v3.23.1)."""
|
||||
|
||||
def setUp(self):
|
||||
self.server = _build_http_server('127.0.0.1', 0, PysimHandler)
|
||||
self.server.log_requests = False
|
||||
self.server.app = None
|
||||
threading.Thread(target=self.server.serve_forever, daemon=True).start()
|
||||
self.port = self.server.server_address[1]
|
||||
|
||||
def tearDown(self):
|
||||
self.server.shutdown()
|
||||
self.server.server_close()
|
||||
|
||||
def _req(self, method, path, body=None):
|
||||
data = json.dumps(body).encode() if body is not None else None
|
||||
req = urllib.request.Request(
|
||||
'http://127.0.0.1:%d%s' % (self.port, path), data=data,
|
||||
headers={'Content-Type': 'application/json'}, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=5) as res:
|
||||
return res.status, json.loads(res.read() or b'{}')
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, json.loads(e.read() or b'{}')
|
||||
|
||||
def test_defaults_overrides_and_clearing(self):
|
||||
import pysim_simple_server.server as srv
|
||||
try:
|
||||
status, resp = self._req('GET', '/api/pli-dict')
|
||||
self.assertEqual(status, 200, resp)
|
||||
self.assertEqual(resp['00'], '930500F1100000')
|
||||
self.assertEqual(resp['0A'], '630104')
|
||||
self.assertEqual(resp['03'], '') # the host clock answers it
|
||||
self.assertEqual(resp['02'], '') # no default
|
||||
status, resp = self._req('POST', '/api/pli-dict',
|
||||
{'01': '94080000000000000000'})
|
||||
self.assertEqual(status, 200, resp)
|
||||
self.assertEqual(resp['01'], '94080000000000000000')
|
||||
self.assertEqual(srv._PLI_DATA.get(0x01), '94080000000000000000')
|
||||
# clearing the field restores the default
|
||||
status, resp = self._req('POST', '/api/pli-dict', {'01': ''})
|
||||
self.assertEqual(status, 200, resp)
|
||||
self.assertEqual(resp['01'], srv.PLI_DEFAULTS[0x01])
|
||||
self.assertNotIn(0x01, srv._PLI_DATA)
|
||||
# a bad value is ignored
|
||||
status, resp = self._req('POST', '/api/pli-dict', {'01': 'ZZ'})
|
||||
self.assertEqual(resp['01'], srv.PLI_DEFAULTS[0x01])
|
||||
finally:
|
||||
srv._PLI_DATA.pop(0x01, None)
|
||||
|
||||
|
||||
class CardFreeGetTests(unittest.TestCase):
|
||||
"""Cached GETs must answer while the card lock is held.
|
||||
|
||||
|
||||
@@ -632,9 +632,17 @@ class TestProactiveDecode(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
import pysim_simple_server.server as srv
|
||||
# the class-wide location override must not leak into other tests
|
||||
# (the PLI dictionary is module state; v3.23.1)
|
||||
self._pli_saved = dict(srv._PLI_DATA)
|
||||
srv._PROACTIVE_SESSION_START = 1234.0
|
||||
srv._PLI_DATA[0x00] = '93055210011000'
|
||||
|
||||
def tearDown(self):
|
||||
import pysim_simple_server.server as srv
|
||||
srv._PLI_DATA.clear()
|
||||
srv._PLI_DATA.update(self._pli_saved)
|
||||
|
||||
@staticmethod
|
||||
def _cmd_raw(cmd_type, qualifier, extras=b''):
|
||||
"""A D0-wrapped proactive command (header TLVs + extras)."""
|
||||
@@ -895,17 +903,56 @@ class TestProactiveDecode(unittest.TestCase):
|
||||
self.assertEqual(tr.hex(), '810301260082028281030100130752f01000ff0001')
|
||||
|
||||
def test_build_tr_pli_empty_other_qualifier_sends_no_data(self):
|
||||
# a qualifier with neither an override nor a built-in default (02
|
||||
# NMR) sends no data object (v3.23.1: the common ones have defaults)
|
||||
import pysim_simple_server.server as srv
|
||||
saved = srv._PLI_DATA.get(0x0a)
|
||||
srv._PLI_DATA[0x0a] = ' '
|
||||
saved = srv._PLI_DATA.get(0x02)
|
||||
srv._PLI_DATA[0x02] = ' '
|
||||
try:
|
||||
tr = _build_tr(None, 1, 0x26, 0x81, 0x82, 0x0a)
|
||||
data = srv._pli_data_hex(0x0a)
|
||||
tr = _build_tr(None, 1, 0x26, 0x81, 0x82, 0x02)
|
||||
data = srv._pli_data_hex(0x02)
|
||||
finally:
|
||||
srv._PLI_DATA[0x0a] = saved
|
||||
srv._PLI_DATA[0x02] = saved
|
||||
self.assertEqual(tr.hex(), '810301260082028281030100')
|
||||
self.assertEqual(data, '')
|
||||
|
||||
def test_pli_defaults_cover_the_common_qualifiers(self):
|
||||
# v3.23.1: the dictionary's base layer - the objects the pinned specs
|
||||
# define (TS 102 223 8.x, tags per TS 101 220 V18.3.0 Table 7.23)
|
||||
import pysim_simple_server.server as srv
|
||||
d = srv.PLI_DEFAULTS
|
||||
self.assertEqual(d[0x00], '930500F1100000') # 001/01 + LAC 0000
|
||||
self.assertEqual(d[0x01], '940894104502237315F8') # IMEI, F pad
|
||||
self.assertEqual(d[0x04], 'AD02656E') # 'en'
|
||||
self.assertEqual(d[0x05], 'AE020000') # Timing Advance
|
||||
self.assertEqual(d[0x06], 'BF0108') # E-UTRAN
|
||||
self.assertEqual(d[0x08], 'E2089410450223731580') # IMEISV
|
||||
self.assertEqual(d[0x09], 'E50101') # automatic
|
||||
self.assertEqual(d[0x0A], '630104') # battery full
|
||||
self.assertEqual(d[0x0E], 'BF020308') # UTRAN + E-UTRAN
|
||||
# the date/time stays dynamic; the 3GPP/3GPP2 qualifiers stay empty
|
||||
self.assertNotIn(0x03, d)
|
||||
for q in (0x02, 0x07, 0x0B, 0x0C, 0x0D, 0x0F, 0x10, 0x11, 0x12,
|
||||
0x13, 0x14, 0x15, 0x16, 0x17, 0x1A):
|
||||
self.assertNotIn(q, d)
|
||||
# every default is a well-formed TLV (a short-form length that
|
||||
# matches the value)
|
||||
for q, v in d.items():
|
||||
self.assertEqual(len(v) % 2, 0, q)
|
||||
self.assertEqual(int(v[2:4], 16) * 2, len(v) - 4, q)
|
||||
# the resolution order: override wins, clearing restores the default,
|
||||
# the clock answers 03 while empty
|
||||
saved = dict(srv._PLI_DATA)
|
||||
try:
|
||||
srv._PLI_DATA[0x01] = '94080000000000000000'
|
||||
self.assertEqual(srv._pli_data_hex(0x01), '94080000000000000000')
|
||||
srv._PLI_DATA.pop(0x01, None)
|
||||
self.assertEqual(srv._pli_data_hex(0x01), d[0x01])
|
||||
self.assertRegex(srv._pli_data_hex(0x03), r'^2607[0-9A-F]{14}$')
|
||||
finally:
|
||||
srv._PLI_DATA.clear()
|
||||
srv._PLI_DATA.update(saved)
|
||||
|
||||
def test_decode_cmd_empty_raw(self):
|
||||
self.assertEqual(_decode_cmd(0x26, b'', None), [])
|
||||
self.assertEqual(_decode_cmd(0x03, None, None), [])
|
||||
|
||||
Reference in New Issue
Block a user