diff --git a/frontend/index.html b/frontend/index.html
index a638635..c6d493f 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -1450,7 +1450,10 @@ function atrPpsRows(decoded) {
rows.push(['Convention', decoded.convention || 'unknown']);
if (decoded.ts) rows.push(['TS', '0x' + decoded.ts]);
if (decoded.t0) rows.push(['T0', '0x' + decoded.t0 + ' — ' + decoded.historical_len + ' historical bytes']);
- if (decoded.protocols) rows.push(['Protocols', decoded.protocols.join(', ')]);
+ if (decoded.protocols && decoded.protocols.length) {
+ rows.push([decoded.protocols.length > 1 ? 'Protocols' : 'Protocol', decoded.protocols.join(', ')]);
+ }
+ if (decoded.pps) rows.push(['PPS', 'supported']);
if (decoded.interface) {
for (const ifc of decoded.interface) {
rows.push([ifc.name, ifaceValue(ifc)]);
@@ -2511,7 +2514,7 @@ function onFilterChange() {
}
// ──────────────────── Versioning ────────────────────
-const PWA_VERSION = '1.22.1';
+const PWA_VERSION = '1.22.2';
const MIN_SERVER_VERSION = '1.0.0';
function parseVersion(v) {
diff --git a/frontend/sw.js b/frontend/sw.js
index c68e36b..719d5c5 100644
--- a/frontend/sw.js
+++ b/frontend/sw.js
@@ -1,4 +1,4 @@
-const CACHE = 'simtrace-analyser-v33';
+const CACHE = 'simtrace-analyser-v34';
const PRECACHE = [
'index.html',
diff --git a/pyproject.toml b/pyproject.toml
index 5d5bd23..83f6118 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.backends._legacy:_Backend"
[project]
name = "simtrace2-pysniff"
-version = "1.22.1"
+version = "1.22.2"
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 be6f06d..580d50c 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.1'
+__version__ = '1.22.2'
__all__ = [
'SniffSession',
'DeviceDisconnected',
diff --git a/simtrace2_pysniff/server/decode.py b/simtrace2_pysniff/server/decode.py
index f8bbb31..b0f6f74 100644
--- a/simtrace2_pysniff/server/decode.py
+++ b/simtrace2_pysniff/server/decode.py
@@ -4177,6 +4177,7 @@ def _decode_atr(data):
interface = []
protocols = []
+ pps = False
flags = t0 & 0xF0
i = 1
level = 1
@@ -4229,7 +4230,10 @@ def _decode_atr(data):
b = body[i]
i += 1
cur_t = b & 0x0F
- protocols.append(_T_PROTOCOLS.get(cur_t, f'T={cur_t}'))
+ if cur_t == 15:
+ pps = True
+ else:
+ protocols.append(_T_PROTOCOLS.get(cur_t, f'T={cur_t}'))
flags = b & 0xF0
level += 1
else:
@@ -4239,6 +4243,8 @@ def _decode_atr(data):
result['interface'] = interface
if protocols:
result['protocols'] = protocols
+ if pps:
+ result['pps'] = True
k = result['historical_len']
if k and i + k <= len(body):
@@ -4246,7 +4252,7 @@ def _decode_atr(data):
i += k
# TCK present unless only T=0 is proposed (ISO 7816-3 §8.2.5).
- only_t0 = not protocols or protocols == ['T=0']
+ only_t0 = (not protocols or protocols == ['T=0']) and not pps
if not only_t0 and i < len(body):
tck = body[i]
check = 0
diff --git a/tests/test_decode.py b/tests/test_decode.py
index 42d373d..f19a9f4 100644
--- a/tests/test_decode.py
+++ b/tests/test_decode.py
@@ -898,6 +898,19 @@ class TestAtrPps(unittest.TestCase):
self.assertEqual(r['historical']['category'], 'status information (life cycle)')
self.assertEqual(r['historical']['life_cycle'], 'operational state (activated)')
+ def test_atr_t15_is_pps_capability(self):
+ from simtrace2_pysniff.server.decode import _decode_atr
+ # Real UICC ATR: T0=9F (TA1+TD1, 15 hist bytes), TA1=95, TD1=80 (T=0),
+ # TD2=1F (T=15 → PPS capability), TA3=C7 (clock stop + classes A/B/C).
+ r = _decode_atr(bytes.fromhex('3B9F95801FC78031E073FE21136364035B83079000F7'))
+ self.assertEqual(r['protocols'], ['T=0'])
+ self.assertTrue(r['pps'])
+ ta3 = r['interface'][1]
+ self.assertEqual(ta3['name'], 'TA3')
+ self.assertEqual(ta3['clock_stop'], 'no preference')
+ self.assertEqual(ta3['classes'], ['A', 'B', 'C'])
+ self.assertTrue(r['tck_valid'])
+
def test_pps(self):
from simtrace2_pysniff.server.decode import _decode_pps
# FF 10 11 xx: PPS0=10 (T=0, PPS1 present), PPS1=11 (Fi=1,Di=1), PCK=XOR(FF,10,11)=FE
diff --git a/tests/test_pcap_import.py b/tests/test_pcap_import.py
index 84fd6e4..f676dbf 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.1')
+ self.assertEqual(__version__, '1.22.2')
def test_status_includes_version(self):
import os