From 500e764a1bbf1d81a1053dc0c86f23ec4f2cb1c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Sat, 19 Sep 2026 00:17:47 +0300 Subject: [PATCH] List view: decoded summaries for ATR/PPS/RST/VCC instead of raw hex MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - decode.py: decode_sniff_msg builds list summaries — ATR: protocol(s), TA1 F/D + ratio, T=15 classes, data rate (when a CLK rate was known); PPS: protocol + F/D + ratio; RST: 'CLK x.xxx MHz' when the event carried a rate - index.html: cmd_description returns summary || '' for these types and STATUS, never falling back to opaque bytes like 010100 - tests: ATR (plain/TA1/classes+rate/unparsed), PPS, RST CLK/no-CLK, VCC - Bump 1.22.20, SW cache v51->v52 --- frontend/index.html | 12 ++++--- frontend/sw.js | 2 +- pyproject.toml | 2 +- simtrace2_pysniff/__init__.py | 2 +- simtrace2_pysniff/server/decode.py | 55 ++++++++++++++++++++++++++++++ tests/test_decode.py | 39 +++++++++++++++++++++ tests/test_pcap_import.py | 2 +- 7 files changed, 106 insertions(+), 8 deletions(-) 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