diff --git a/frontend/index.html b/frontend/index.html
index 6e4d003..ee77229 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -667,6 +667,7 @@ function cmdRows(cmd) {
if (cmd.aid) rows.push(['AID', cmd.aid]);
if (cmd.title) rows.push(['Title', cmd.title]);
if (cmd.text !== undefined) rows.push(['Text', cmd.text]);
+ if (cmd.icon_id !== undefined) rows.push(['Icon', cmd.icon_id ? 'record #' + cmd.icon_id : 'none']);
if (cmd.items) {
for (const it of cmd.items) {
rows.push(['Item ' + (it.id !== undefined ? '0x' + hx(it.id, 2) : '?'), it.text]);
@@ -1851,7 +1852,7 @@ function onFilterChange() {
}
// ──────────────────── Versioning ────────────────────
-const PWA_VERSION = '1.10.0';
+const PWA_VERSION = '1.11.0';
const MIN_SERVER_VERSION = '1.0.0';
function parseVersion(v) {
diff --git a/frontend/sw.js b/frontend/sw.js
index 009df1a..f6116bd 100644
--- a/frontend/sw.js
+++ b/frontend/sw.js
@@ -1,4 +1,4 @@
-const CACHE = 'simtrace-analyser-v13';
+const CACHE = 'simtrace-analyser-v14';
const PRECACHE = [
'index.html',
diff --git a/pyproject.toml b/pyproject.toml
index 5b9899e..cb62ba8 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.backends._legacy:_Backend"
[project]
name = "simtrace2-pysniff"
-version = "1.10.0"
+version = "1.11.0"
description = "Python-based sniffing tool for SIMtrace2 hardware (SIM card communication sniffer)"
readme = {file = "README.md", content-type = "text/markdown"}
license = {text = "GPL-2.0-or-later"}
diff --git a/simtrace2_pysniff/__init__.py b/simtrace2_pysniff/__init__.py
index 1494d69..9222449 100644
--- a/simtrace2_pysniff/__init__.py
+++ b/simtrace2_pysniff/__init__.py
@@ -6,7 +6,7 @@ from .gsmtap import GsmtapSender, GsmtapReceiver
from .pcap import PcapWriter
from .dump import FileDumper, format_message
-__version__ = '1.10.0'
+__version__ = '1.11.0'
__all__ = [
'SniffSession',
'DeviceDisconnected',
diff --git a/simtrace2_pysniff/server/decode.py b/simtrace2_pysniff/server/decode.py
index cf5f14a..ecf42f9 100644
--- a/simtrace2_pysniff/server/decode.py
+++ b/simtrace2_pysniff/server/decode.py
@@ -1047,13 +1047,28 @@ PLI_QUALIFIERS = {
# TS 102 223 §8.6 — Command Qualifier tables per Type of Command.
# Only commands with a known qualifier coding are listed; others fall back
-# to a raw '0xXX' in _command_qualifier.
+# to a raw '0xXX' in _command_qualifier. An entry may also be a callable
+# for bitmask-style qualifiers (returns the decoded string or None).
COMMAND_QUALIFIERS = {
0x01: REFRESH_MODES, # REFRESH
0x26: PLI_QUALIFIERS, # PROVIDE LOCAL INFORMATION
}
+def _idle_text_qualifier(qualifier):
+ """SET UP IDLE MODE TEXT (TS 102 223 §6.6.13): bit 1 = icon
+ self-explanatory flag, all other bits RFU."""
+ if qualifier & ~0x01:
+ return f'0x{qualifier:02X}'
+ if qualifier & 0x01:
+ return 'icon self-explanatory'
+ return None
+
+
+# TS 102 223 §6.6.13 — SET UP IDLE MODE TEXT
+COMMAND_QUALIFIERS[0x28] = _idle_text_qualifier
+
+
def _command_qualifier(cmd_type, qualifier):
"""Decode a Command Qualifier (Command Details byte 3) into a string.
@@ -1064,6 +1079,8 @@ def _command_qualifier(cmd_type, qualifier):
if qualifier is None:
return None
table = COMMAND_QUALIFIERS.get(cmd_type)
+ if callable(table):
+ return table(qualifier)
if table:
name = table.get(qualifier)
if name is None:
@@ -2235,6 +2252,7 @@ _P_ITEM = 0x0F
_P_FILE_LIST = 0x12
_P_RESPONSE_LEN = 0x11
_P_EVENT_LIST = 0x19
+_P_ICON_ID = 0x1E
_P_AID = 0x2F
@@ -2268,7 +2286,7 @@ def _decode_proactive(body):
qualifier = value[2]
elif base == _P_ALPHA_ID and value:
result['title'] = _decode_annex_a(value)
- elif base == _P_TEXT_STRING and value:
+ elif base == _P_TEXT_STRING:
result['text'] = _decode_dcs_text(value)
elif base == _P_ITEM and len(value) >= 2:
items.append({'id': value[0], 'text': _decode_annex_a(value[1:])})
@@ -2282,6 +2300,8 @@ def _decode_proactive(body):
result['events'] = [EVENT_TYPES.get(e, f'0x{e:02X}') for e in value]
elif base == _P_ADDRESS and value:
result['address'] = _decode_bcd_address(value)
+ elif base == _P_ICON_ID and value:
+ result['icon_id'] = value[0]
elif base == _P_FILE_LIST and value:
file_list = [value[i:i + 2].hex().upper() for i in range(0, len(value), 2)]
elif base == _P_AID and value:
diff --git a/tests/test_decode.py b/tests/test_decode.py
index 20d4cc9..3f48a62 100644
--- a/tests/test_decode.py
+++ b/tests/test_decode.py
@@ -1545,6 +1545,53 @@ class TestRefresh(unittest.TestCase):
self.assertEqual(r['cmd']['file_list'], ['6F07', '6F20'])
+class TestIdleModeText(unittest.TestCase):
+ """SET UP IDLE MODE TEXT (TS 102 223 §6.6.13) proactive command."""
+
+ def _fetch(self, inner_hex):
+ # FETCH response body: D0 BER-TLV wrapping the proactive command
+ inner = bytes.fromhex(inner_hex)
+ body = b'\xd0' + bytes([len(inner)]) + inner
+ return decode_message(
+ bytes.fromhex('80120000%02x' % len(body)) + body + b'\x90\x00')
+
+ def test_8bit_text_icon_qualifier(self):
+ # "Hello" 8-bit + icon record 5 + qualifier bit 1 (self-explanatory)
+ r = self._fetch(
+ '810301280182028182' # cmd details (qual 0x01) + device ids
+ '8d060448656c6c6f' # Text String: DCS 0x04 (8-bit), "Hello"
+ '1e0105') # Icon Identifier: record 5
+ self.assertEqual(r['cat_command'], 'SET UP IDLE MODE TEXT')
+ self.assertEqual(r['cmd']['text'], 'Hello')
+ self.assertEqual(r['cmd']['qualifier'], 'icon self-explanatory')
+ self.assertEqual(r['cmd']['icon_id'], 5)
+
+ def test_ucs2_text(self):
+ # "Привет" UCS-2 (DCS 0x08)
+ r = self._fetch(
+ '810301280082028182'
+ '8d0d08041f04400438043204350442')
+ self.assertEqual(r['cat_command'], 'SET UP IDLE MODE TEXT')
+ self.assertEqual(r['cmd']['text'], 'Привет')
+
+ def test_qualifier_zero_hidden(self):
+ r = self._fetch('8103012800820281828d060448656c6c6f')
+ self.assertNotIn('qualifier', r['cmd'])
+
+ def test_qualifier_rfu_bits_raw(self):
+ r = self._fetch('8103012802820281828d060448656c6c6f')
+ self.assertEqual(r['cmd']['qualifier'], '0x02')
+
+ def test_empty_text_removes_idle_text(self):
+ # Zero-length Text String instructs the ME to remove the idle text
+ r = self._fetch('8103012800820281828d00')
+ self.assertEqual(r['cmd']['text'], '')
+
+ def test_no_icon_tlv(self):
+ r = self._fetch('8103012800820281828d060448656c6c6f')
+ self.assertNotIn('icon_id', r['cmd'])
+
+
class TestSwUiccSpecific(unittest.TestCase):
"""TS 102 221 UICC-specific SWs (tables 10.7-10.15)."""
diff --git a/tests/test_pcap_import.py b/tests/test_pcap_import.py
index fc89dc5..cb5c6fc 100644
--- a/tests/test_pcap_import.py
+++ b/tests/test_pcap_import.py
@@ -197,7 +197,7 @@ class TestImportEndpoint(unittest.TestCase):
class TestVersion(unittest.TestCase):
def test_package_version(self):
from simtrace2_pysniff import __version__
- self.assertEqual(__version__, '1.10.0')
+ self.assertEqual(__version__, '1.11.0')
def test_status_includes_version(self):
import os