terminal profile at runtime + canonical custom-file paths (v2.2.3)

TERMINAL PROFILE:
- GET /api/terminal-profile returns the profile in effect + the CLI default;
  POST /api/terminal-profile validates ({profile}, hex, even, 1-255 bytes),
  stores it in memory and re-sends it, resetting the STK session like
  /api/rescue (the shared _resend_terminal_profile helper; rescue now
  delegates to it). __main__ keeps server.cli_terminal_profile.
- Phone tab: a TERMINAL PROFILE block next to STATUS and Polling with the
  current hex/byte count, Send (re-send) and Configure. The Configure
  dialog has a device-model preset selector, a hex field and a per-bit
  form generated from a 264-entry table for TS 102 223 V18.3.0 5.2 bytes
  1-33 (pySim's table as scaffold, later bytes/3GPP bits added from the
  spec; beyond the table generic RFU labels). Form <-> hex sync both
  ways, hex authoritative, bits preserved. Apply posts the new value.
- Presets: Xiaomi Mi A1 (project default), Quectel GSM module example.
  In-memory only, no persistence.

Custom files:
- Canonical paths rooted at MF / ADF.USIM / ADF.ISIM; entries are
  {path, name, kind}. The editor now uses root + parent-DF selector +
  4-hex FID + alias, requires the parent DF to be defined first, rejects
  duplicates, rewrites descendants when a DF's path changes and cascades
  deletes after a confirmation.
- Legacy forms are normalized on load/import (3F00/... -> MF/..., relative
  a153/4954 resolved against the custom DFs); unresolvable entries are
  dropped and reported in the list.
- Tree injection matches by exact parent path via the new pysimFsNodePath
  (same-FID DFs under different parents no longer collide);
  profilerCustomNameForPath uses the same normalization.

SW cache otaman-v167; help EN/RU + docs/api.md + AGENTS updated.
Tests: 236 Python + 371 frontend.
This commit is contained in:
2026-09-16 20:46:40 +03:00
parent 20fbfd99a5
commit aa33d25466
15 changed files with 1057 additions and 180 deletions
+1
View File
@@ -179,6 +179,7 @@ def main():
server.sms_sc = opts.sms_sm_sc
server.log_requests = opts.log_requests
server.terminal_profile = opts.terminal_profile
server.cli_terminal_profile = opts.terminal_profile
server.web_dir = opts.web_dir
server.sim_menu = sim_menu
server.event_list = event_list
+69 -10
View File
@@ -21,7 +21,7 @@ from osmocom.construct import GsmOrUcs2Adapter
from osmocom.tlv import BER_TLV_IE
VERSION = '2.2.1'
VERSION = '2.2.3'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
@@ -2876,6 +2876,36 @@ def _send_terminal_profile(scc, tp_hex):
return sim_menu, event_list
def _validate_tp_hex(hex_str):
"""Validate a TERMINAL PROFILE hex string. Returns (hex_or_None, error)."""
h = re.sub(r'\s', '', str(hex_str or '')).upper()
if not h:
return None, 'profile is empty'
if not re.fullmatch(r'[0-9A-F]+', h):
return None, 'profile is not valid hex'
if len(h) % 2:
return None, 'profile must have an even number of hex digits'
if len(h) // 2 > 255:
return None, 'profile exceeds 255 bytes'
return h, None
def _resend_terminal_profile(server, scc):
"""Re-send the current TERMINAL PROFILE and reset the STK session state.
Shared by /api/rescue and the runtime profile update; the value lives in
server.terminal_profile (CLI default, overridable at runtime)."""
server.stk_pending = None
server.menu_active = False
_cancel_menu_timeout()
server.event_list = None
_reset_proactive_log()
sm, el = _send_terminal_profile(scc, server.terminal_profile)
server.sim_menu = sm
server.event_list = el
return {'ok': True, 'profile': server.terminal_profile,
'menu': sm is not None, 'events': el}
def _make_menu_fetch_handler(server, resp):
"""on_fetch callback for the menu chain: pauses on user-interactive commands
and stores the pending command so a TERMINAL RESPONSE can be sent later."""
@@ -3135,6 +3165,13 @@ class PysimHandler(BaseHTTPRequestHandler):
resp = self.server.event_list or []
self._send_json(resp)
self._log_resp(resp)
elif self.path == '/api/terminal-profile':
self._log_req()
tp = getattr(self.server, 'terminal_profile', None) or ''
resp = {'profile': tp or None, 'bytes': len(tp) // 2,
'cli_default': getattr(self.server, 'cli_terminal_profile', None)}
self._send_json(resp)
self._log_resp(resp)
elif self.path == '/api/pli-qualifiers':
qualifiers = [{'code': '%02X' % q, 'name': PLI_QUALIFIER_NAMES[q]} for q in PLI_QUALIFIER_NAMES]
self._send_json(qualifiers)
@@ -3285,15 +3322,37 @@ class PysimHandler(BaseHTTPRequestHandler):
self._log_resp({'error': 'no terminal profile configured'})
return
sys.stderr.write('RESCUE: re-sending TERMINAL PROFILE\n')
self.server.stk_pending = None
self.server.menu_active = False
_cancel_menu_timeout()
self.server.event_list = None
_reset_proactive_log()
sm, el = _send_terminal_profile(scc, self.server.terminal_profile)
self.server.sim_menu = sm
self.server.event_list = el
resp = {'menu': sm is not None, 'events': el}
resp = _resend_terminal_profile(self.server, scc)
self._send_json(resp)
self._log_resp(resp)
elif self.path == '/api/terminal-profile':
body = self._read_body()
self._log_req(body)
scc = self.server.scc
if not scc:
self._send_json({'error': _err('reader_not_init', lang)}, 503)
self._log_resp({'error': _err('reader_not_init', lang)})
return
profile = body.get('profile')
if profile is not None:
h, perr = _validate_tp_hex(profile)
if perr:
self._send_json({'error': perr}, 400)
self._log_resp({'error': perr})
return
self.server.terminal_profile = h
if not self.server.terminal_profile:
self._send_json({'error': 'no terminal profile configured'}, 400)
self._log_resp({'error': 'no terminal profile configured'})
return
sys.stderr.write('TERMINAL-PROFILE: re-sending %s\n' % self.server.terminal_profile)
try:
resp = _resend_terminal_profile(self.server, scc)
except Exception as e:
resp = {'ok': False, 'error': str(e)}
self._send_json(resp, 502)
self._log_resp(resp)
return
self._send_json(resp)
self._log_resp(resp)
elif self.path == '/api/help':