v1.8.0: proactive log overhaul, Expanded Script rename, proactive view sizing

Server:
- Track all TERMINAL RESPONSEs (chain, TP, pySim auto-handler, menu-respond
  link to paused entry) with shared _build_tr/_record_tr
- Log entry ids + cmd_num; server-side decode of fetched commands and TRs
  (compact PLI port); Result CTLV extracted as tr_result/tr_result_name
- _LoggingApduTracer captures TR SW for pySim auto-handler responses
- _DefaultProactiveHandler answers PLI with editor data (no re-request)
- VERSION 1.8.0

Frontend:
- Expandable proactive log rows (click to expand, state survives re-render)
- Response section: Result name + hex, decoded fields, width-restrained
  readonly hex inputs; no SW display
- Rename Expanded Script tab/pill/button/docs; proactive UICC view font
  sizing increase; card reader pills text-sm
This commit is contained in:
2026-08-19 18:04:08 +03:00
parent a916e8a255
commit 80a860549a
11 changed files with 563 additions and 124 deletions
+122
View File
@@ -20,9 +20,15 @@ if str(PY_SIM) not in sys.path:
from pysim_otaman_server.server import (
_build_sms_tpdu,
_build_tr,
_decode_cmd,
_decode_por,
_decode_tr,
_log_proactive,
_ota_reference,
_record_tr,
_spi_from_bytes,
_tr_data_only,
)
# Synthetic dummy key material (no real card keys).
@@ -214,5 +220,121 @@ class TestDecodePor(unittest.TestCase):
msg='expected None for %r' % bad)
class TestProactiveDecode(unittest.TestCase):
"""Server-side proactive command/TR decode helpers (v1.8.0 log feature)."""
def setUp(self):
import pysim_otaman_server.server as srv
srv._PROACTIVE_SESSION_START = 1234.0
srv._PLI_DATA[0x00] = '93055210011000'
def test_decode_cmd_poll_interval(self):
r = _decode_cmd(0x03, bytes.fromhex('d00d8103010300820283818402011e'), None)
self.assertEqual(r, [{'label': 'Interval', 'value': '30 s'}])
def test_decode_cmd_setup_event_list(self):
r = _decode_cmd(0x05, bytes.fromhex('d00c810301050082028381990101'), None)
self.assertEqual(r, [{'label': 'Events', 'value': 'Call connected'}])
def test_decode_cmd_send_short_message(self):
r = _decode_cmd(0x13, bytes.fromhex('d0158103011300820283818b0b916106152670f900a35f020101'), None)
self.assertEqual(r, [{'label': 'SMS TPDU', 'value': '916106152670f900a35f02'}])
def test_decode_cmd_pli_qualifier_name(self):
r = _decode_cmd(0x26, b'\xd0', 0x00)
self.assertTrue(r[0]['value'].startswith('Location Information (MCC, MNC, LAC/TAC, Cell ID)'))
def test_decode_cmd_empty_raw(self):
self.assertEqual(_decode_cmd(0x26, b'', None), [])
self.assertEqual(_decode_cmd(0x03, None, None), [])
def test_decode_tr_pli_location(self):
r = _decode_tr('26', '00', '93055210011000')
self.assertEqual(r, [
{'label': 'MCC', 'value': '250'},
{'label': 'MNC', 'value': '11'},
{'label': 'LAC/TAC', 'value': '1000'},
])
def test_decode_tr_pli_imei(self):
r = _decode_tr('26', '01', '94082143658709214305')
self.assertEqual(r[0], {'label': 'IMEI', 'value': '123456789012345'})
def test_decode_tr_pli_access_technology(self):
r = _decode_tr('26', '06', 'bf0103')
self.assertEqual(r, [{'label': 'Access Technology', 'value': 'UTRAN (3)'}])
def test_decode_tr_pli_search_mode(self):
r = _decode_tr('26', '09', 'ad0101')
self.assertEqual(r, [{'label': 'Search Mode', 'value': 'Manual'}])
def test_decode_tr_poll_interval(self):
r = _decode_tr('03', None, '8402011e')
self.assertEqual(r, [{'label': 'Interval', 'value': '30 s'}])
def test_decode_tr_empty(self):
self.assertEqual(_decode_tr('26', '00', ''), [])
def test_tr_data_only_strips_boilerplate(self):
tr = bytes.fromhex('81030326008202818393055210011000030100')
self.assertEqual(_tr_data_only(tr).hex(), '93055210011000')
def test_build_and_record_tr(self):
entry = {'type_hex': '26', 'qualifier': '00'}
tr = _build_tr(None, 3, 0x26, 0x83, 0x81, 0x00)
_record_tr(entry, tr, '9000')
self.assertEqual(entry['tr_hex'], '93055210011000')
self.assertEqual(entry['tr_sw'], '9000')
self.assertEqual([f['label'] for f in entry['tr_decoded']], ['MCC', 'MNC', 'LAC/TAC'])
def test_record_tr_without_sw(self):
entry = {'type_hex': '26', 'qualifier': '00'}
_record_tr(entry, bytes.fromhex('93055210011000'))
self.assertNotIn('tr_sw', entry)
self.assertEqual(entry['tr_hex'], '93055210011000')
def test_log_proactive_fields(self):
entry = _log_proactive(0x26, b'\xd0', 0x00, 7)
self.assertEqual(entry['cmd_num'], 7)
self.assertEqual(entry['type_hex'], '26')
self.assertEqual(entry['qualifier'], '00')
self.assertEqual(entry['raw'], 'd0')
self.assertEqual(entry['cmd_decoded'][0]['label'], 'Qualifier')
self.assertIn('id', entry)
def test_log_proactive_malformed_raw(self):
entry = _log_proactive(0x21, bytes([0xD0, 0xFF]), 0x00)
self.assertEqual(entry['cmd_decoded'], [])
def test_log_proactive_no_cmd_num(self):
entry = _log_proactive(0x05, bytes.fromhex('990101'), None)
self.assertNotIn('cmd_num', entry)
def test_record_tr_basic_result(self):
entry = {'type_hex': '03', 'qualifier': None}
_record_tr(entry, bytes.fromhex('8103010300820281838402011e030100'))
self.assertEqual(entry['tr_result'], '00')
self.assertEqual(entry['tr_result_name'], 'Command performed successfully')
self.assertEqual(entry['tr_hex'], '8402011e')
def test_record_tr_general_result(self):
entry = {'type_hex': '26', 'qualifier': '00'}
_record_tr(entry, bytes.fromhex('8103032600820281839305521001100083022001'))
self.assertEqual(entry['tr_result'], '2001')
self.assertEqual(entry['tr_result_name'], 'ME currently unable to process command')
self.assertEqual(entry['tr_hex'], '93055210011000')
def test_record_tr_unknown_result(self):
entry = {'type_hex': '26', 'qualifier': '00'}
_record_tr(entry, bytes.fromhex('810303260082028181030107'))
self.assertEqual(entry['tr_result'], '07')
self.assertNotIn('tr_result_name', entry)
def test_record_tr_no_result_tlv(self):
entry = {'type_hex': '26', 'qualifier': '00'}
_record_tr(entry, bytes.fromhex('810303260082028181'))
self.assertNotIn('tr_result', entry)
if __name__ == '__main__':
unittest.main()