Fix review findings: timer value nibble order, TIMER MGMT qualifier bits
Self-review of 174df8a/d0f8fb8 found two real bugs plus smaller items:
- _decode_timer_value read semi-octets high-nibble first; TS 23.040
coding (referenced by §8.38) puts the first digit in the LOW nibble
— "09:25:03" decoded as "90:25:03". Regression test added with an
asymmetric vector; the original symmetric vectors masked the bug.
- TIMER MANAGEMENT qualifier used bits b2-b3 instead of the spec's
bits 1-2 (q & 0x03): '01' deactivate was hidden, '02' printed
"deactivate timer", RFU '04' printed "get current timer value".
Decoder fixed and wrong expectations corrected.
- Stale text_note: a compliant 8D Text String arriving after the
non-standard '10' quirk TLV now clears the note.
- Proprietary type format unified to uppercase ("Proprietary (0xF2)").
- GET INKEY: Yes/No bit now suppresses the b1/b2 character-set parts,
matching §8.6 ("character sets ... are disabled").
- Frontend renders string timer_id values verbatim instead of NaN.
Tests: +4 regressions, expectations corrected — 320 total.
This commit is contained in:
1 parent
d0f8fb8f36
commit
0e99e73fda
3 files changed
+50
-12
No files matched your search
+2
-1
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
+34
-6
@@ -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)."""
|
||||
|
||||
Reference in new issue
Block a user