diff --git a/docs/api.md b/docs/api.md index 3b56438..162bc0b 100644 --- a/docs/api.md +++ b/docs/api.md @@ -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` diff --git a/docs/test-scripts.md b/docs/test-scripts.md index 324b744..19b73da 100644 --- a/docs/test-scripts.md +++ b/docs/test-scripts.md @@ -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. diff --git a/frontend/help-ru.html b/frontend/help-ru.html index abf773d..7bc5afc 100644 --- a/frontend/help-ru.html +++ b/frontend/help-ru.html @@ -530,7 +530,7 @@
  • 06 Access Technology · 08 IMEISV · 09 Search Mode
  • 0A Battery · 0E Multiple Access Technologies
  • -

    Значения хранятся на сервере до перезапуска. Когда карта выдаёт PLI, сервер вставляет значения словаря в TERMINAL RESPONSE — в том числе в ответ тест-скрипта.

    +

    Значения хранятся на сервере до перезапуска. Для квалификаторов, определённых в спецификациях ETSI, заранее заданы разумные значения по умолчанию (местоположение — тестовая сеть 001/01, тестовый IMEI 490154203237518 и его IMEISV, язык «en», технология доступа E-UTRAN, автоматический поиск сети, полный заряд батареи): заданное значение переопределяет умолчание, а очистка поля возвращает умолчание; квалификатор даты/времени, пока пуст, отвечается часами хоста, а специфичные для 3GPP/3GPP2 квалификаторы (NMR, ESN, MEID, CSG/HNB, WLAN, slices, CAG, …) остаются пустыми — спецификации ETSI не определяют для них содержимое. Когда карта выдаёт PLI, сервер вставляет значения в TERMINAL RESPONSE — в том числе в ответ тест-скрипта.

    8.7 Симуляция сети

    Воспроизводит шаблоны записи реального телефона при смене сетевых условий (исследование трасс в projects/UICC_NAA.md): подключение EPS, потеря сервиса / ограниченный сервис, запрет роуминга, откат на 2G, приём SMS, перенастройка cell broadcast и AUTHENTICATE. По кнопке на сценарий; карта должна быть подключена.

    diff --git a/frontend/help.html b/frontend/help.html index 9395de9..db1dd24 100644 --- a/frontend/help.html +++ b/frontend/help.html @@ -529,7 +529,7 @@
  • 06 Access Technology · 08 IMEISV · 09 Search Mode
  • 0A Battery · 0E Multiple Access Technologies
  • -

    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.

    +

    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 clearing the field restores it; 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.

    8.7 Network simulation

    Replays the write patterns a real phone performs when the network condition changes (trace study in projects/UICC_NAA.md): 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.

    diff --git a/frontend/index.html b/frontend/index.html index 8b35044..f83a653 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -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() { + '' + esc(q.name) + '' + '' + '
    ' - + ''; if (hasCodec) { html += ' { btn.textContent = t('Save'); btn.classList.remove('bg-emerald-700'); btn.disabled = false; }, 1500); diff --git a/frontend/sw.js b/frontend/sw.js index ef88c9d..84e7e97 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v413'; +const CACHE = 'simple-v414'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/pli_dict.test.js b/frontend/tests/pli_dict.test.js index ef27392..21649a7 100644 --- a/frontend/tests/pli_dict.test.js +++ b/frontend/tests/pli_dict.test.js @@ -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' }); +}); diff --git a/pyproject.toml b/pyproject.toml index 4bfd233..deab407 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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. diff --git a/pysim_simple_server/server.py b/pysim_simple_server/server.py index 586c41e..3415e58 100644 --- a/pysim_simple_server/server.py +++ b/pysim_simple_server/server.py @@ -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': diff --git a/tests/test_fetch_routes.py b/tests/test_fetch_routes.py index 29ad4ad..118ee52 100644 --- a/tests/test_fetch_routes.py +++ b/tests/test_fetch_routes.py @@ -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. diff --git a/tests/test_ota_helpers.py b/tests/test_ota_helpers.py index 22efa64..6ae76fc 100644 --- a/tests/test_ota_helpers.py +++ b/tests/test_ota_helpers.py @@ -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), [])