diff --git a/frontend/index.html b/frontend/index.html
index b20cb7a..404d6d8 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -1047,7 +1047,11 @@ function data_text(m) {
function cmd_description(m) {
if (m.type === 'gap') return t('device disconnected — messages may be missing');
- if (m.decoded && m.decoded.summary) return m.decoded.summary;
+ const d = m.decoded;
+ // STATUS has no command body: without response data there is nothing
+ // to describe beyond the type and SW (no raw-hex fallback).
+ if (d && d.ins_hex === 'f2' && !d.body) return d.summary || '';
+ if (d && d.summary) return d.summary;
return data_text(m);
}
@@ -2629,7 +2633,7 @@ function onFilterChange() {
}
// ──────────────────── Versioning ────────────────────
-const PWA_VERSION = '1.22.18';
+const PWA_VERSION = '1.22.19';
const MIN_SERVER_VERSION = '1.0.0';
function parseVersion(v) {
diff --git a/frontend/sw.js b/frontend/sw.js
index f9ff5d5..ec52685 100644
--- a/frontend/sw.js
+++ b/frontend/sw.js
@@ -1,4 +1,4 @@
-const CACHE = 'simtrace-analyser-v50';
+const CACHE = 'simtrace-analyser-v51';
const PRECACHE = [
'index.html',
diff --git a/pyproject.toml b/pyproject.toml
index 6e109fb..52c8f3c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.backends._legacy:_Backend"
[project]
name = "simtrace2-pysniff"
-version = "1.22.18"
+version = "1.22.19"
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 b497707..190e4ca 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.22.18'
+__version__ = '1.22.19'
__all__ = [
'SniffSession',
'DeviceDisconnected',
diff --git a/simtrace2_pysniff/server/decode.py b/simtrace2_pysniff/server/decode.py
index 54d8e9f..f17ff82 100644
--- a/simtrace2_pysniff/server/decode.py
+++ b/simtrace2_pysniff/server/decode.py
@@ -3629,6 +3629,39 @@ def _fcp_summary(response_for, fd, response):
return label
+def _fcp_brief(response):
+ """Build a short "what this FCP describes" fragment for STATUS entries.
+
+ E.g. ``FCP of MF (DF or ADF)`` or ``FCP of EF_ICCID (Working EF,
+ transparent, 10 B)``.
+ """
+ fd = response.get('file_descriptor') or {}
+ fid = response.get('file_id')
+ name = response.get('file_id_name')
+ what = name or (fid.upper() if fid else None)
+ if not what and not fd:
+ return None
+ label = f'FCP of {what}' if what else 'FCP'
+ details = []
+ ft = fd.get('file_type')
+ if ft:
+ details.append(ft)
+ if ft and ft != 'DF or ADF':
+ struct = fd.get('structure')
+ if struct:
+ details.append(struct)
+ nrec = fd.get('num_records')
+ rlen = fd.get('record_length')
+ fsize = response.get('file_size') or response.get('total_file_size')
+ if nrec and rlen:
+ details.append(f'{nrec} rec \u00d7 {rlen} B')
+ elif fsize:
+ details.append(f'{fsize} B')
+ if details:
+ label += f" ({', '.join(details)})"
+ return label
+
+
def _build_summary(result):
"""Build a concise human-readable description of a decoded command.
@@ -3685,6 +3718,16 @@ def _build_summary(result):
if bodytxt:
parts.append(bodytxt)
+ if ins == 'f2' and body:
+ # STATUS response data is the FCP of the current DF/EF (seen in a
+ # TPDU capture as the bytes after the 5-byte command header).
+ size = body.get('size') or 0
+ txt = f"Data: {size} byte{'s' if size != 1 else ''}"
+ brief = _fcp_brief(result.get('response') or {})
+ if brief:
+ txt += f' \u2014 {brief}'
+ parts.append(txt)
+
cmd = result.get('cmd')
if cmd:
if cmd.get('context'):
diff --git a/tests/test_decode.py b/tests/test_decode.py
index fd430c4..1e2b463 100644
--- a/tests/test_decode.py
+++ b/tests/test_decode.py
@@ -1586,6 +1586,40 @@ class TestStatusFcp(unittest.TestCase):
self.assertEqual(resp['df_name'], 'A0000000871002FF45FF018902011100')
self.assertEqual(resp['file_descriptor']['file_type'], 'DF or ADF')
self.assertEqual(resp['life_cycle'], 'operational state (activated)')
+ self.assertEqual(r['summary'], 'Data: 56 bytes \u2014 FCP of 7FF0 (DF or ADF)')
+
+ def test_status_no_data_has_no_summary(self):
+ # 80 F2 00 00 00 6C 25 — P3=0, card answers "wrong length"; no data.
+ r = decode_message(bytes.fromhex('80f20000006c25'))
+ self.assertEqual(r['ins_name'], 'STATUS')
+ self.assertNotIn('body', r)
+ self.assertNotIn('response', r)
+ self.assertIsNone(r['summary'])
+
+ def test_status_mf_summary(self):
+ raw = bytes.fromhex(
+ '80f200000d'
+ '620b8202782183023f008a0105'
+ '9000')
+ r = decode_message(raw)
+ self.assertEqual(r['response']['file_id'], '3F00')
+ self.assertEqual(r['response']['file_id_name'], 'MF')
+ self.assertEqual(r['summary'], 'Data: 13 bytes \u2014 FCP of MF (DF or ADF)')
+
+ def test_status_ef_summary(self):
+ raw = bytes.fromhex(
+ '80f2000024'
+ '62228202412183026f73a509c0018091047f206f538a01058b036f06098002000e880160'
+ '9000')
+ r = decode_message(raw)
+ self.assertEqual(r['response']['file_id_name'], 'EF_PSLOCI')
+ self.assertEqual(
+ r['summary'],
+ 'Data: 36 bytes \u2014 FCP of EF_PSLOCI (Working EF, transparent, 14 B)')
+
+ def test_status_single_byte_pluralization(self):
+ r = decode_message(bytes.fromhex('80f2000001009000'))
+ self.assertEqual(r['summary'], 'Data: 1 byte')
class TestLengthMismatch(unittest.TestCase):
diff --git a/tests/test_pcap_import.py b/tests/test_pcap_import.py
index bfb6bff..b94ad12 100644
--- a/tests/test_pcap_import.py
+++ b/tests/test_pcap_import.py
@@ -224,7 +224,7 @@ class TestImportEndpoint(unittest.TestCase):
class TestVersion(unittest.TestCase):
def test_package_version(self):
from simtrace2_pysniff import __version__
- self.assertEqual(__version__, '1.22.18')
+ self.assertEqual(__version__, '1.22.19')
def test_status_includes_version(self):
import os