diff --git a/frontend/index.html b/frontend/index.html
index 404d6d8..7a203ab 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -1048,9 +1048,13 @@ function data_text(m) {
function cmd_description(m) {
if (m.type === 'gap') return t('device disconnected — messages may be missing');
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 || '';
+ // No raw-hex fallback for these: STATUS's decoded summary is the only
+ // useful list text (it has no command body), and ATR/PPS/RST/VCC get a
+ // decoded summary (or nothing) instead of opaque bytes like "010100".
+ if (d && (d.ins_hex === 'f2' || d.type === 'atr' || d.type === 'pps'
+ || d.type === 'rst' || d.type === 'vcc')) {
+ return d.summary || '';
+ }
if (d && d.summary) return d.summary;
return data_text(m);
}
@@ -2633,7 +2637,7 @@ function onFilterChange() {
}
// ──────────────────── Versioning ────────────────────
-const PWA_VERSION = '1.22.19';
+const PWA_VERSION = '1.22.20';
const MIN_SERVER_VERSION = '1.0.0';
function parseVersion(v) {
diff --git a/frontend/sw.js b/frontend/sw.js
index ec52685..cceb82d 100644
--- a/frontend/sw.js
+++ b/frontend/sw.js
@@ -1,4 +1,4 @@
-const CACHE = 'simtrace-analyser-v51';
+const CACHE = 'simtrace-analyser-v52';
const PRECACHE = [
'index.html',
diff --git a/pyproject.toml b/pyproject.toml
index 52c8f3c..e8082b8 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.backends._legacy:_Backend"
[project]
name = "simtrace2-pysniff"
-version = "1.22.19"
+version = "1.22.20"
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 190e4ca..d578d92 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.19'
+__version__ = '1.22.20'
__all__ = [
'SniffSession',
'DeviceDisconnected',
diff --git a/simtrace2_pysniff/server/decode.py b/simtrace2_pysniff/server/decode.py
index f17ff82..eb995fc 100644
--- a/simtrace2_pysniff/server/decode.py
+++ b/simtrace2_pysniff/server/decode.py
@@ -4345,6 +4345,57 @@ def _decode_atr(data, clk_hz=None):
return result
+def _atr_summary(result):
+ """Build the APDU-list description of an ATR.
+
+ Protocol(s), TA1 Fi/Di (with the F/D ratio), the T=15 class indicator
+ and — when a CLK rate was measured — the resulting data rate.
+ """
+ if 't0' not in result:
+ return None # unparsed/unknown-convention ATR
+ parts = []
+ protocols = result.get('protocols') or ['T=0']
+ parts.append(', '.join(protocols))
+ interface = result.get('interface') or []
+ for ifc in interface:
+ if ifc.get('name') == 'TA1':
+ vals = []
+ if 'f' in ifc:
+ vals.append(f"F={ifc['f']}")
+ if 'd' in ifc:
+ vals.append(f"D={ifc['d']}")
+ if ifc.get('f_div_d') is not None:
+ vals.append(f"F/D={ifc['f_div_d']}")
+ if vals:
+ parts.append(', '.join(vals))
+ break
+ for ifc in interface:
+ if ifc.get('classes'):
+ parts.append('classes ' + ', '.join(ifc['classes']))
+ break
+ if result.get('data_rate'):
+ parts.append(f"{result['data_rate']} bit/s")
+ return ' \u00b7 '.join(parts)
+
+
+def _pps_summary(result):
+ """Build the APDU-list description of a PPS exchange (protocol + Fi/Di)."""
+ parts = []
+ if result.get('protocol'):
+ parts.append(result['protocol'])
+ fd = result.get('fi_di') or {}
+ vals = []
+ if 'f' in fd:
+ vals.append(f"F={fd['f']}")
+ if 'd' in fd:
+ vals.append(f"D={fd['d']}")
+ if fd.get('f_div_d') is not None:
+ vals.append(f"F/D={fd['f_div_d']}")
+ if vals:
+ parts.append(', '.join(vals))
+ return ' \u00b7 '.join(parts) if parts else None
+
+
def _decode_pps(data):
"""Decode a Protocol and Parameter Selection exchange (ISO 7816-3 §9.2)."""
if not data:
@@ -4407,6 +4458,8 @@ def decode_sniff_msg(raw_data, msg_type, flags=0, prev=None, clk_hz=None):
return decode_change(flags)
if msg_type in ('rst', 'vcc'):
result = decode_line_event(raw_data, msg_type)
+ if result.get('clk_hz'):
+ result['summary'] = f"CLK {result['clk_hz'] / 1e6:.3f} MHz"
errs = gsmtap_flag_names(flags)
if errs:
result['errors'] = errs
@@ -4416,6 +4469,7 @@ def decode_sniff_msg(raw_data, msg_type, flags=0, prev=None, clk_hz=None):
if msg_type == 'atr':
result = _decode_atr(raw_data, clk_hz=clk_hz)
if result is not None:
+ result['summary'] = _atr_summary(result)
errs = (decode_data_flags(flags) or []) + (gsmtap_flag_names(flags) or [])
if errs:
result['errors'] = errs
@@ -4423,6 +4477,7 @@ def decode_sniff_msg(raw_data, msg_type, flags=0, prev=None, clk_hz=None):
if msg_type == 'pps':
result = _decode_pps(raw_data)
if result is not None:
+ result['summary'] = _pps_summary(result)
errs = (decode_data_flags(flags) or []) + (gsmtap_flag_names(flags) or [])
if errs:
result['errors'] = errs
diff --git a/tests/test_decode.py b/tests/test_decode.py
index 1e2b463..e465147 100644
--- a/tests/test_decode.py
+++ b/tests/test_decode.py
@@ -951,6 +951,45 @@ class TestAtrPps(unittest.TestCase):
self.assertEqual(r['pck'], 'FE')
+class TestPacketSummaries(unittest.TestCase):
+ """APDU-list descriptions for ATR/PPS/RST/VCC packets."""
+
+ def _dec(self, hexstr, kind, clk_hz=None):
+ from simtrace2_pysniff.server.decode import decode_sniff_msg
+ return decode_sniff_msg(bytes.fromhex(hexstr), kind, clk_hz=clk_hz)
+
+ def test_atr_simple(self):
+ self.assertEqual(self._dec('3B00', 'atr')['summary'], 'T=0')
+
+ def test_atr_ta1(self):
+ self.assertEqual(self._dec('3B12110000', 'atr')['summary'],
+ 'T=0 \u00b7 F=372, D=1, F/D=372')
+
+ def test_atr_classes_and_data_rate(self):
+ r = self._dec('3B9F95801FC78031E073FE21136364035B83079000F7', 'atr',
+ clk_hz=3582090)
+ self.assertEqual(
+ r['summary'],
+ 'T=0 \u00b7 F=512, D=16, F/D=32 \u00b7 classes A, B, C \u00b7 111940 bit/s')
+
+ def test_atr_unparsed_has_no_summary(self):
+ self.assertIsNone(self._dec('3B', 'atr')['summary'])
+
+ def test_pps_summary(self):
+ self.assertEqual(self._dec('FF10957A', 'pps')['summary'],
+ 'T=0 \u00b7 F=512, D=16, F/D=32')
+
+ def test_rst_clk_summary(self):
+ self.assertEqual(self._dec('0001010036A88A', 'rst')['summary'],
+ 'CLK 3.582 MHz')
+
+ def test_rst_without_clk_has_no_summary(self):
+ self.assertIsNone(self._dec('010000', 'rst').get('summary'))
+
+ def test_vcc_has_no_summary(self):
+ self.assertIsNone(self._dec('010100', 'vcc').get('summary'))
+
+
class TestSummary(unittest.TestCase):
def test_select_path_from_mf(self):
r = decode_message(bytes.fromhex('a0a40804022fe29000'))
diff --git a/tests/test_pcap_import.py b/tests/test_pcap_import.py
index b94ad12..d782f98 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.19')
+ self.assertEqual(__version__, '1.22.20')
def test_status_includes_version(self):
import os