STATUS list entries: suppress raw hex when empty, describe FCP data

- decode.py: _build_summary emits 'Data: N bytes — FCP of <file> (...)' for
  STATUS responses with data (FCP already decoded into decoded.response);
  _fcp_brief builds the 'FCP of MF (DF or ADF)' / 'FCP of EF_xxx (...)'
  fragment
- index.html: cmd_description suppresses the raw-hex fallback for
  body-less STATUS rows (nothing to describe beyond type + SW)
- tests: no-data STATUS summary None; DF/EF FCP summaries; pluralization
- Bump 1.22.19, SW cache v50->v51
This commit is contained in:
catarrh committed 2026-09-19 00:10:09 +03:00
1 parent 39ee173a7d
commit ccbb3f231b
7 files changed
+87 -6

No files matched your search

+6 -2
View File
@@ -1047,7 +1047,11 @@ function data_text(m) {
function cmd_description(m) { function cmd_description(m) {
if (m.type === 'gap') return t('device disconnected — messages may be missing'); 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); return data_text(m);
} }
@@ -2629,7 +2633,7 @@ function onFilterChange() {
} }
// ──────────────────── Versioning ──────────────────── // ──────────────────── Versioning ────────────────────
const PWA_VERSION = '1.22.18'; const PWA_VERSION = '1.22.19';
const MIN_SERVER_VERSION = '1.0.0'; const MIN_SERVER_VERSION = '1.0.0';
function parseVersion(v) { function parseVersion(v) {
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simtrace-analyser-v50'; const CACHE = 'simtrace-analyser-v51';
const PRECACHE = [ const PRECACHE = [
'index.html', 'index.html',
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.backends._legacy:_Backend"
[project] [project]
name = "simtrace2-pysniff" name = "simtrace2-pysniff"
version = "1.22.18" version = "1.22.19"
description = "Python-based sniffing tool for SIMtrace2 hardware (SIM card communication sniffer)" description = "Python-based sniffing tool for SIMtrace2 hardware (SIM card communication sniffer)"
readme = {file = "README.md", content-type = "text/markdown"} readme = {file = "README.md", content-type = "text/markdown"}
license = {text = "GPL-2.0-or-later"} license = {text = "GPL-2.0-or-later"}
+1 -1
View File
@@ -6,7 +6,7 @@ from .gsmtap import GsmtapSender, GsmtapReceiver
from .pcap import PcapWriter from .pcap import PcapWriter
from .dump import FileDumper, format_message from .dump import FileDumper, format_message
__version__ = '1.22.18' __version__ = '1.22.19'
__all__ = [ __all__ = [
'SniffSession', 'SniffSession',
'DeviceDisconnected', 'DeviceDisconnected',
+43
View File
@@ -3629,6 +3629,39 @@ def _fcp_summary(response_for, fd, response):
return label 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): def _build_summary(result):
"""Build a concise human-readable description of a decoded command. """Build a concise human-readable description of a decoded command.
@@ -3685,6 +3718,16 @@ def _build_summary(result):
if bodytxt: if bodytxt:
parts.append(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') cmd = result.get('cmd')
if cmd: if cmd:
if cmd.get('context'): if cmd.get('context'):
+34
View File
@@ -1586,6 +1586,40 @@ class TestStatusFcp(unittest.TestCase):
self.assertEqual(resp['df_name'], 'A0000000871002FF45FF018902011100') self.assertEqual(resp['df_name'], 'A0000000871002FF45FF018902011100')
self.assertEqual(resp['file_descriptor']['file_type'], 'DF or ADF') self.assertEqual(resp['file_descriptor']['file_type'], 'DF or ADF')
self.assertEqual(resp['life_cycle'], 'operational state (activated)') 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): class TestLengthMismatch(unittest.TestCase):
+1 -1
View File
@@ -224,7 +224,7 @@ class TestImportEndpoint(unittest.TestCase):
class TestVersion(unittest.TestCase): class TestVersion(unittest.TestCase):
def test_package_version(self): def test_package_version(self):
from simtrace2_pysniff import __version__ from simtrace2_pysniff import __version__
self.assertEqual(__version__, '1.22.18') self.assertEqual(__version__, '1.22.19')
def test_status_includes_version(self): def test_status_includes_version(self):
import os import os