diff --git a/frontend/index.html b/frontend/index.html index 92d0d2a..d03f95a 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -682,7 +682,8 @@ function cmdRows(cmd) { if (cmd.response_length) rows.push(['Response length', cmd.response_length.min + '…' + cmd.response_length.max]); if (cmd.item_id !== undefined) rows.push(['Item id', '0x' + hx(cmd.item_id, 2)]); if (cmd.help) rows.push(['Help', 'requested']); - if (cmd.timer_id !== undefined) rows.push(['Timer id', '0x' + hx(cmd.timer_id, 2)]); + if (cmd.timer_id !== undefined) rows.push(['Timer id', + typeof cmd.timer_id === 'number' ? '0x' + hx(cmd.timer_id, 2) : cmd.timer_id]); if (cmd.timer_value) rows.push(['Timer value', cmd.timer_value]); if (cmd.ussd) rows.push(['USSD string', cmd.ussd]); if (cmd.encapsulated && cmd.encapsulated.type) { diff --git a/simtrace2_pysniff/server/decode.py b/simtrace2_pysniff/server/decode.py index 7158be3..d344cfb 100644 --- a/simtrace2_pysniff/server/decode.py +++ b/simtrace2_pysniff/server/decode.py @@ -1126,6 +1126,10 @@ def _display_text_qualifier(q): @_bitmask def _get_inkey_qualifier(q): + if q & 0x04: # Yes/No mode disables the b1/b2 character-set coding + return ['Yes/No response requested', + 'immediate digit response requested' if q & 0x08 else None, + _help_bit(q)] return [ 'alphabet set requested' if q & 0x01 else None, _alphabet_bit(q), @@ -1164,7 +1168,9 @@ def _set_up_menu_qualifier(q): @_bitmask def _timer_mgmt_qualifier(q): - op = (q >> 1) & 0x03 + # TS 102 223 §8.6: bits 1 to 2 — '00' start, '01' deactivate, + # '10' get current value, '11' RFU. + op = q & 0x03 return {1: 'deactivate timer', 2: 'get current timer value'}.get(op, []) @@ -1275,7 +1281,7 @@ def _command_details_name(value): if len(value) < 2: return None if 0xF0 <= value[1] <= 0xFE: # TS 102 223 §9.4 — reserved for proprietary use - return f'Proprietary ({value[1]:#04x})' + return f'Proprietary (0x{value[1]:02X})' return CAT_COMMAND_TYPES.get(value[1]) @@ -1754,11 +1760,13 @@ def _decode_device_ids(value): def _decode_timer_value(b): - """Decode a 3-byte Timer Value (TS 102 223 §8.38): semi-octet h/m/s.""" + """Decode a 3-byte Timer Value (TS 102 223 §8.38): TP-SCTS-style + semi-octets (first digit in the low nibble) for hour/minute/second.""" if len(b) != 3: return None def so(x): - return '%d%d' % ((x >> 4) & 0x0F, x & 0x0F) + # TS 23.040 semi-octet representation: digit 1 in bits 1-4 + return '%d%d' % (x & 0x0F, (x >> 4) & 0x0F) return f'{so(b[0])}:{so(b[1])}:{so(b[2])}' @@ -2586,6 +2594,7 @@ def _decode_proactive(body): result['title'] = _decode_annex_a(value) elif base == _P_TEXT_STRING: result['text'] = _decode_dcs_text(value) + result.pop('text_note', None) # compliant TLV wins over quirk note text_seen = True elif base == _P_ITEM and len(value) >= 2: items.append({'id': value[0], 'text': _decode_annex_a(value[1:])}) @@ -2624,7 +2633,7 @@ def _decode_proactive(body): if cmd_type is None: return None if 0xF0 <= cmd_type <= 0xFE: # TS 102 223 §9.4 — reserved for proprietary use - result['type'] = f'Proprietary ({cmd_type:#04x})' + result['type'] = f'Proprietary (0x{cmd_type:02X})' else: result['type'] = CAT_COMMAND_TYPES.get(cmd_type, f'0x{cmd_type:02X}') qualifier_desc = _command_qualifier(cmd_type, qualifier) diff --git a/tests/test_decode.py b/tests/test_decode.py index 16f5d63..491c0cc 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -1641,6 +1641,15 @@ class TestSatLegacyQuirks(unittest.TestCase): self.assertEqual(r['cmd']['text'], 'A') self.assertIn({'tag': '10', 'value': '010102'}, r['cmd']['raw_tlv']) + def test_compliant_text_clears_quirk_note(self): + # Quirk '10' text first, then a compliant 8D Text String: + # the compliant value wins and the stale note must disappear. + r = self._fetch('810301280082028182' + '1003043337' # non-standard '10': DCS+"37" + '8d060448656c6c6f') # compliant "Hello" + self.assertEqual(r['cmd']['text'], 'Hello') + self.assertNotIn('text_note', r['cmd']) + class TestSpecRegistry(unittest.TestCase): """Registries verified against TS 102 223 V18.3.0 / TS 31.111 V18.12.0.""" @@ -1703,8 +1712,8 @@ class TestSpecRegistry(unittest.TestCase): def test_proprietary_type_fallback(self): r = self._fetch('810301f20082028182') - self.assertEqual(r['cat_command'], 'Proprietary (0xf2)') - self.assertEqual(r['cmd']['type'], 'Proprietary (0xf2)') + self.assertEqual(r['cat_command'], 'Proprietary (0xF2)') + self.assertEqual(r['cmd']['type'], 'Proprietary (0xF2)') def test_pli_qualifiers_esn_and_supported_rat(self): from simtrace2_pysniff.server.decode import PLI_QUALIFIERS @@ -1731,7 +1740,7 @@ class TestEnvelopeDecoders(unittest.TestCase): def test_timer_expiration(self): # D7 | device ids | timer id 01 | timer value semi-octet h/m/s - r = self._envelope('D70C820282812401012503010203') + r = self._envelope('D70C820282812401012503102030') cmd = r['cmd'] self.assertEqual(cmd['type'], 'TIMER EXPIRATION') self.assertEqual(cmd['timer_id'], 1) @@ -1778,6 +1787,17 @@ class TestEnvelopeDecoders(unittest.TestCase): self.assertEqual(r['cmd']['device_ids'], {'src': 'Network', 'dst': 'UICC'}) + def test_timer_value_semi_octet_order(self): + # TS 23.040 semi-octets: first digit in the LOW nibble. + # 90 52 30 → "09:25:03" (pre-fix code read high-nibble first). + from simtrace2_pysniff.server.decode import _decode_timer_value + self.assertEqual(_decode_timer_value(bytes.fromhex('905230')), + '09:25:03') + # 01 02 03 means "10:20:30", not "01:02:03". + self.assertEqual(_decode_timer_value(bytes.fromhex('010203')), + '10:20:30') + + class TestQualifiers(unittest.TestCase): """Phase 3: §8.6 qualifier decoding for the remaining commands.""" @@ -1814,10 +1834,13 @@ class TestQualifiers(unittest.TestCase): 'selection using soft key preferred') def test_timer_management_ops(self): - self.assertEqual(self._fetch_qual(0x27, 0x02), 'deactivate timer') - self.assertEqual(self._fetch_qual(0x27, 0x04), - 'get current timer value') + # TS 102 223 §8.6: bits 1-2 — '00' start, '01' deactivate, + # '10' get current value, '11' RFU. self.assertIsNone(self._fetch_qual(0x27, 0x00)) # start = default + self.assertEqual(self._fetch_qual(0x27, 0x01), 'deactivate timer') + self.assertEqual(self._fetch_qual(0x27, 0x02), + 'get current timer value') + self.assertEqual(self._fetch_qual(0x27, 0x04), '0x04') # b1-b2=00 def test_send_sm_packing_bit(self): self.assertEqual(self._fetch_qual(0x13, 0x01), @@ -1839,6 +1862,11 @@ class TestQualifiers(unittest.TestCase): self.assertEqual(self._fetch_qual(0x02, 0xFF), '0xFF') # MORE TIME self.assertIsNone(self._fetch_qual(0x02, 0x00)) + def test_get_inkey_yes_no_suppresses_alphabet_bits(self): + # b3 (Yes/No) disables the b1/b2 character-set coding per §8.6 + self.assertEqual(self._fetch_qual(0x22, 0x04), + 'Yes/No response requested') + class TestSwUiccSpecific(unittest.TestCase): """TS 102 221 UICC-specific SWs (tables 10.7-10.15)."""