Decode SCP80 Response Packet (PoR) in GET RESPONSE after SMS-PP DOWNLOAD
This commit is contained in:
1 parent
ef1ba00732
commit
582c42c5cd
3 files changed
+56
-11
No files matched your search
+11
-1
@@ -95,7 +95,7 @@ body { font: 14px/1.5 system-ui, sans-serif; background: var(--bg); color: var(-
|
|||||||
.trace-topbar select { padding: 8px 12px; background: var(--bg-input); border: 1px solid var(--border); border-radius: 4px; color: var(--text); }
|
.trace-topbar select { padding: 8px 12px; background: var(--bg-input); border: 1px solid var(--border); border-radius: 4px; color: var(--text); }
|
||||||
.trace-topbar button { padding: 8px 16px; background: var(--accent); color: #fff; border: none; border-radius: 4px; cursor: pointer; font-weight: 600; }
|
.trace-topbar button { padding: 8px 16px; background: var(--accent); color: #fff; border: none; border-radius: 4px; cursor: pointer; font-weight: 600; }
|
||||||
.trace-topbar button:hover { background: var(--accent-hover); }
|
.trace-topbar button:hover { background: var(--accent-hover); }
|
||||||
.trace-topbar button.stop { background: var(--bg-input); }
|
.trace-topbar button.stop { background: var(--bg-input); color: var(--text); }
|
||||||
.trace-topbar button.stop:hover { background: var(--bg-panel); }
|
.trace-topbar button.stop:hover { background: var(--bg-panel); }
|
||||||
.trace-topbar button.connect { background: var(--warn); color: #0f172a; }
|
.trace-topbar button.connect { background: var(--warn); color: #0f172a; }
|
||||||
.trace-topbar button.connect:hover { background: var(--warn-hover); }
|
.trace-topbar button.connect:hover { background: var(--warn-hover); }
|
||||||
@@ -591,6 +591,16 @@ function responseRows(resp) {
|
|||||||
rows.push([k, v]);
|
rows.push([k, v]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (resp.rpl !== undefined) {
|
||||||
|
rows.push(['Response packet', 'Response Packet Length ' + resp.rpl + ' · Response Header Length ' + resp.rhl]);
|
||||||
|
rows.push(['TAR', '0x' + resp.tar]);
|
||||||
|
rows.push(['CNTR', '0x' + resp.cntr]);
|
||||||
|
rows.push(['PCNTR', resp.pcntr]);
|
||||||
|
const st = resp.status || {};
|
||||||
|
rows.push(['Status', '0x' + st.code + (st.name ? ' — ' + st.name : '')]);
|
||||||
|
if (resp.rc_cc_ds) rows.push(['RC/CC/DS', resp.rc_cc_ds]);
|
||||||
|
if (resp.data) rows.push(['Response data', resp.data]);
|
||||||
|
}
|
||||||
|
|
||||||
if (!rows.length) return '';
|
if (!rows.length) return '';
|
||||||
return `<tr><td class="label" colspan="2" style="padding-top:6px; border-top:1px solid var(--border);">Response</td></tr>` +
|
return `<tr><td class="label" colspan="2" style="padding-top:6px; border-top:1px solid var(--border);">Response</td></tr>` +
|
||||||
|
|||||||
@@ -956,15 +956,18 @@ COMMAND_QUALIFIERS = {
|
|||||||
def _command_qualifier(cmd_type, qualifier):
|
def _command_qualifier(cmd_type, qualifier):
|
||||||
"""Decode a Command Qualifier (Command Details byte 3) into a string.
|
"""Decode a Command Qualifier (Command Details byte 3) into a string.
|
||||||
|
|
||||||
Returns None when no meaningful qualifier is present, a decoded name
|
Returns None when no meaningful qualifier is present, the raw '0xXX'
|
||||||
when a table exists (including a table's own '00' entry), or a raw
|
for a non-zero qualifier without a table, or '0xXX <name>' when a
|
||||||
'0xXX' for a non-zero qualifier without a table.
|
table entry exists.
|
||||||
"""
|
"""
|
||||||
if qualifier is None:
|
if qualifier is None:
|
||||||
return None
|
return None
|
||||||
table = COMMAND_QUALIFIERS.get(cmd_type)
|
table = COMMAND_QUALIFIERS.get(cmd_type)
|
||||||
if table:
|
if table:
|
||||||
return table.get(qualifier, f'0x{qualifier:02X}')
|
name = table.get(qualifier)
|
||||||
|
if name is None:
|
||||||
|
return f'0x{qualifier:02X}'
|
||||||
|
return f'0x{qualifier:02X} {name}'
|
||||||
if qualifier == 0x00:
|
if qualifier == 0x00:
|
||||||
return None
|
return None
|
||||||
return f'0x{qualifier:02X}'
|
return f'0x{qualifier:02X}'
|
||||||
@@ -3031,6 +3034,9 @@ def _build_summary(result):
|
|||||||
fd = response.get('file_descriptor')
|
fd = response.get('file_descriptor')
|
||||||
if fd:
|
if fd:
|
||||||
parts.append(_fcp_summary(result['response_for'], fd, response))
|
parts.append(_fcp_summary(result['response_for'], fd, response))
|
||||||
|
elif response.get('tar'):
|
||||||
|
st = response.get('status') or {}
|
||||||
|
parts.append(f"{st.get('name') or ('0x' + st.get('code', ''))} · TAR {response['tar']}")
|
||||||
else:
|
else:
|
||||||
parts.append('response for ' + result['response_for'])
|
parts.append('response for ' + result['response_for'])
|
||||||
if response and response.get('name'):
|
if response and response.get('name'):
|
||||||
@@ -3236,6 +3242,12 @@ def decode_message(raw_data, prev=None):
|
|||||||
'ef': KNOWN_FIDS.get(fid, fid.upper()),
|
'ef': KNOWN_FIDS.get(fid, fid.upper()),
|
||||||
'record_numbers': list(remaining[:cmd_body_len]),
|
'record_numbers': list(remaining[:cmd_body_len]),
|
||||||
}
|
}
|
||||||
|
elif prev_ins == 0xC2: # ENVELOPE → Response Packet (PoR)
|
||||||
|
data = remaining[:cmd_body_len]
|
||||||
|
# RPI mapping precedes the packet: UDHL='02', IEIa='71',
|
||||||
|
# IEIDLa='00' (TS 31.115 §4.4).
|
||||||
|
if data[:3] == b'\x02\x71\x00':
|
||||||
|
result['response'] = _decode_response_packet(data[3:])
|
||||||
else:
|
else:
|
||||||
response = _decode_response_for(prev_ins, remaining[:cmd_body_len])
|
response = _decode_response_for(prev_ins, remaining[:cmd_body_len])
|
||||||
if response:
|
if response:
|
||||||
|
|||||||
+29
-6
@@ -73,7 +73,7 @@ class TestCatDecoding(unittest.TestCase):
|
|||||||
'801200000BD0098103012600820281829000'))
|
'801200000BD0098103012600820281829000'))
|
||||||
self.assertEqual(r['cat_command'], 'PROVIDE LOCAL INFORMATION')
|
self.assertEqual(r['cat_command'], 'PROVIDE LOCAL INFORMATION')
|
||||||
self.assertEqual(r['cmd']['qualifier'],
|
self.assertEqual(r['cmd']['qualifier'],
|
||||||
'Location Info (MCC, MNC, LAC/TAC, Cell ID)')
|
'0x00 Location Info (MCC, MNC, LAC/TAC, Cell ID)')
|
||||||
|
|
||||||
def test_envelope_event_download(self):
|
def test_envelope_event_download(self):
|
||||||
r = decode_message(bytes.fromhex(
|
r = decode_message(bytes.fromhex(
|
||||||
@@ -186,20 +186,20 @@ class TestPliQualifier(unittest.TestCase):
|
|||||||
r = decode_message(bytes.fromhex(
|
r = decode_message(bytes.fromhex(
|
||||||
'8014000016810301260102028281030106130952F0991EC57A68009F'))
|
'8014000016810301260102028281030106130952F0991EC57A68009F'))
|
||||||
self.assertEqual(r['response_to'], 'PROVIDE LOCAL INFORMATION')
|
self.assertEqual(r['response_to'], 'PROVIDE LOCAL INFORMATION')
|
||||||
self.assertEqual(r['response']['qualifier'], 'IMEI')
|
self.assertEqual(r['response']['qualifier'], '0x01 IMEI')
|
||||||
|
|
||||||
def test_fetch_pli_location(self):
|
def test_fetch_pli_location(self):
|
||||||
r = decode_message(bytes.fromhex(
|
r = decode_message(bytes.fromhex(
|
||||||
'801200000BD0098103012600820281829000'))
|
'801200000BD0098103012600820281829000'))
|
||||||
self.assertEqual(r['cat_command'], 'PROVIDE LOCAL INFORMATION')
|
self.assertEqual(r['cat_command'], 'PROVIDE LOCAL INFORMATION')
|
||||||
self.assertEqual(r['cmd']['qualifier'],
|
self.assertEqual(r['cmd']['qualifier'],
|
||||||
'Location Info (MCC, MNC, LAC/TAC, Cell ID)')
|
'0x00 Location Info (MCC, MNC, LAC/TAC, Cell ID)')
|
||||||
|
|
||||||
def test_fetch_pli_date_time(self):
|
def test_fetch_pli_date_time(self):
|
||||||
r = decode_message(bytes.fromhex(
|
r = decode_message(bytes.fromhex(
|
||||||
'801200000BD0098103012603820281829000'))
|
'801200000BD0098103012603820281829000'))
|
||||||
self.assertEqual(r['cat_command'], 'PROVIDE LOCAL INFORMATION')
|
self.assertEqual(r['cat_command'], 'PROVIDE LOCAL INFORMATION')
|
||||||
self.assertEqual(r['cmd']['qualifier'], 'Date, time and time zone')
|
self.assertEqual(r['cmd']['qualifier'], '0x03 Date, time and time zone')
|
||||||
|
|
||||||
def test_non_pli_no_qualifier(self):
|
def test_non_pli_no_qualifier(self):
|
||||||
r = decode_message(bytes.fromhex(
|
r = decode_message(bytes.fromhex(
|
||||||
@@ -222,6 +222,29 @@ class TestGetResponseContext(unittest.TestCase):
|
|||||||
r = decode_message(self.GET_RESPONSE, prev=None)
|
r = decode_message(self.GET_RESPONSE, prev=None)
|
||||||
self.assertIsNone(r['response_for'])
|
self.assertIsNone(r['response_for'])
|
||||||
|
|
||||||
|
def test_get_response_por(self):
|
||||||
|
# GET RESPONSE after ENVELOPE SMS-PP DOWNLOAD → Response Packet (PoR).
|
||||||
|
r = decode_message(
|
||||||
|
bytes.fromhex('00C0000024') +
|
||||||
|
bytes.fromhex('027100001F0AB0000100000000C100000290000080041C043004400438044FD83CDF37FF') +
|
||||||
|
bytes.fromhex('9000'),
|
||||||
|
prev={'ins': 0xC2, 'ins_name': 'ENVELOPE', 'sw1': '61'})
|
||||||
|
self.assertEqual(r['response_for'], 'ENVELOPE')
|
||||||
|
self.assertEqual(r['response']['rpl'], 31)
|
||||||
|
self.assertEqual(r['response']['rhl'], 10)
|
||||||
|
self.assertEqual(r['response']['tar'], 'B00001')
|
||||||
|
self.assertEqual(r['response']['cntr'], '00000000C1')
|
||||||
|
self.assertEqual(r['response']['status'], {'code': '00', 'name': 'PoR OK'})
|
||||||
|
self.assertEqual(r['response']['data'], '0290000080041C043004400438044FD83CDF37FF')
|
||||||
|
self.assertIn('PoR OK · TAR B00001', r['summary'])
|
||||||
|
|
||||||
|
def test_get_response_non_por_not_parsed(self):
|
||||||
|
# A GET RESPONSE after ENVELOPE that is not a PoR must stay undecoded.
|
||||||
|
r = decode_message(bytes.fromhex('00C0000004') + bytes.fromhex('AABBCCDD') + bytes.fromhex('9000'),
|
||||||
|
prev={'ins': 0xC2, 'ins_name': 'ENVELOPE', 'sw1': '61'})
|
||||||
|
self.assertEqual(r['response_for'], 'ENVELOPE')
|
||||||
|
self.assertNotIn('response', r)
|
||||||
|
|
||||||
|
|
||||||
class TestFcpResponse(unittest.TestCase):
|
class TestFcpResponse(unittest.TestCase):
|
||||||
GET_RESPONSE = bytes.fromhex(
|
GET_RESPONSE = bytes.fromhex(
|
||||||
@@ -1512,13 +1535,13 @@ class TestRefresh(unittest.TestCase):
|
|||||||
def test_uicc_reset(self):
|
def test_uicc_reset(self):
|
||||||
r = decode_message(bytes.fromhex('801200000bd0098103010104820281829000'))
|
r = decode_message(bytes.fromhex('801200000bd0098103010104820281829000'))
|
||||||
self.assertEqual(r['cat_command'], 'REFRESH')
|
self.assertEqual(r['cat_command'], 'REFRESH')
|
||||||
self.assertEqual(r['cmd']['qualifier'], 'UICC Reset')
|
self.assertEqual(r['cmd']['qualifier'], '0x04 UICC Reset')
|
||||||
self.assertNotIn('file_list', r['cmd'])
|
self.assertNotIn('file_list', r['cmd'])
|
||||||
self.assertNotIn('aid', r['cmd'])
|
self.assertNotIn('aid', r['cmd'])
|
||||||
|
|
||||||
def test_file_change_notification(self):
|
def test_file_change_notification(self):
|
||||||
r = decode_message(bytes.fromhex('8012000011d00f81030101018202818292046f076f209000'))
|
r = decode_message(bytes.fromhex('8012000011d00f81030101018202818292046f076f209000'))
|
||||||
self.assertEqual(r['cmd']['qualifier'], 'File Change Notification')
|
self.assertEqual(r['cmd']['qualifier'], '0x01 File Change Notification')
|
||||||
self.assertEqual(r['cmd']['file_list'], ['6F07', '6F20'])
|
self.assertEqual(r['cmd']['file_list'], ['6F07', '6F20'])
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user