Decode standard PPS sub-types + RST CLK extension → ATR data rate
- capture.py: map GSMTAP PPS request/response (0x02/0x03) to 'pps', matching the pcap-import mapping (was decoded as TPDU live) - decode.py: parse the optional RST CLK extension (sigrok v1.9.0+, payload flag bit 0); ATR gains clk_hz/data_rate/etu_us derived from TA1 Fi/Di (data_rate = clk_hz × D / F) - database.py: CLK state is consumed by the next ATR (no stale rate for an ATR without preceding RST); a late-arriving RST measurement is backfilled onto the ATR it belongs to; replay context carries the CLK across pagination/live polling - PWA: ATR detail shows CLK/Data rate/ETU; RST event detail shows CLK - tests: PPS mapping, CLK parse with/without flag, rate math, DB consume + backfill round-trips - README/RUS updated - Bump 1.22.18, SW cache v49->v50
This commit is contained in:
1 parent
c2f605a391
commit
a46667d93f
12 files changed
+211
-40
No files matched your search
@@ -130,14 +130,20 @@ tunable via `--log-interval SECONDS`.
|
|||||||
The GSMTAP listener also accepts the **sigrok-iso7816-stream** decoder
|
The GSMTAP listener also accepts the **sigrok-iso7816-stream** decoder
|
||||||
(https://github.com/anttro/sigrok_iso7816_stream) — an FX2-logic-analyzer
|
(https://github.com/anttro/sigrok_iso7816_stream) — an FX2-logic-analyzer
|
||||||
passive SIM sniffer that emits the same GSMTAP-SIM format on UDP 4729. Its
|
passive SIM sniffer that emits the same GSMTAP-SIM format on UDP 4729. Its
|
||||||
custom sub-types for RST/VCC line events (`0x10`/`0x11`, added for
|
standard PPS request/response sub-types (`0x02`/`0x03`) are decoded as PPS,
|
||||||
|
and its custom sub-types for RST/VCC line events (`0x10`/`0x11`, added for
|
||||||
compatibility with that project) are decoded as `RESET ASSERTED/DE-ASSERTED`
|
compatibility with that project) are decoded as `RESET ASSERTED/DE-ASSERTED`
|
||||||
and `VCC ON/OFF` timeline markers, and a reset-assert or power-removal
|
and `VCC ON/OFF` timeline markers, and a reset-assert or power-removal
|
||||||
correctly invalidates the file-selection tracking. GSMTAP packets carrying
|
correctly invalidates the file-selection tracking. When an RST event carries
|
||||||
the `GSMTAP_FLAG_BAD_FCS` desync flag (set by that decoder on mis-framed
|
the measured CLK frequency (decoder v1.9.0+, optional payload extension), the
|
||||||
exchanges) are surfaced as a flagged/desynced entry in the capture timeline,
|
following ATR is annotated with the CLK frequency, data rate and ETU derived
|
||||||
so such artefacts are easy to spot when reviewing a capture rather than
|
from its TA1 Fi/Di (`data_rate = clk_hz × D / F`); the measurement is
|
||||||
silently altering the decode.
|
consumed by the ATR so a later ATR without a reset gets no stale rate (a
|
||||||
|
late-arriving measurement is backfilled onto the ATR it belongs to). GSMTAP
|
||||||
|
packets carrying the `GSMTAP_FLAG_BAD_FCS` desync flag (set by that decoder
|
||||||
|
on mis-framed exchanges) are surfaced as a flagged/desynced entry in the
|
||||||
|
capture timeline, so such artefacts are easy to spot when reviewing a capture
|
||||||
|
rather than silently altering the decode.
|
||||||
|
|
||||||
Server options:
|
Server options:
|
||||||
|
|
||||||
|
|||||||
+15
-9
@@ -137,15 +137,21 @@ heartbeat `Capture alive: session=N messages=M bytes=B dropped=D`
|
|||||||
GSMTAP-листенер также принимает декодер **sigrok-iso7816-stream**
|
GSMTAP-листенер также принимает декодер **sigrok-iso7816-stream**
|
||||||
(https://github.com/anttro/sigrok_iso7816_stream) — пассивный SIM-сниффер
|
(https://github.com/anttro/sigrok_iso7816_stream) — пассивный SIM-сниффер
|
||||||
на базе FX2-логического анализатора, выдающий тот же формат GSMTAP-SIM
|
на базе FX2-логического анализатора, выдающий тот же формат GSMTAP-SIM
|
||||||
на UDP 4729. Его пользовательские подтипы для событий линий RST/VCC
|
на UDP 4729. Его стандартные подтипы PPS-запроса/ответа (`0x02`/`0x03`)
|
||||||
(`0x10`/`0x11`, добавлены для совместимости с тем проектом) декодируются
|
декодируются как PPS, а пользовательские подтипы для событий линий RST/VCC
|
||||||
как временные маркеры `RESET ASSERTED/DE-ASSERTED` и `VCC ON/OFF`,
|
(`0x10`/`0x11`, добавлены для совместимости с тем проектом) — как временные
|
||||||
а сброс или пропадание питания корректно сбрасывает отслеживание
|
маркеры `RESET ASSERTED/DE-ASSERTED` и `VCC ON/OFF`; сброс или пропадание
|
||||||
выбранного файла. GSMTAP-пакеты с флагом десинхронизации
|
питания корректно сбрасывает отслеживание выбранного файла. Когда событие
|
||||||
`GSMTAP_FLAG_BAD_FCS` (устанавливается тем декодером при нарушении
|
RST несёт измеренную частоту CLK (декодер v1.9.0+, опциональное расширение
|
||||||
кадрирования) отображаются в таймлайне как помеченная/десинхронизированная
|
полезной нагрузки), следующий ATR получает частоту CLK, скорость передачи
|
||||||
запись, чтобы такие артефакты было легко заметить при разборе, а не молча
|
данных и ETU, вычисленные из его TA1 Fi/Di
|
||||||
искажали декодирование.
|
(`data_rate = clk_hz × D / F`); измерение потребляется ATR, поэтому
|
||||||
|
следующий ATR без сброса не получит устаревшую скорость (поздно пришедшее
|
||||||
|
измерение восстанавливается для «своего» ATR). GSMTAP-пакеты с флагом
|
||||||
|
десинхронизации `GSMTAP_FLAG_BAD_FCS` (устанавливается тем декодером при
|
||||||
|
нарушении кадрирования) отображаются в таймлайне как
|
||||||
|
помеченная/десинхронизированная запись, чтобы такие артефакты было легко
|
||||||
|
заметить при разборе, а не молча искажали декодирование.
|
||||||
|
|
||||||
Параметры сервера:
|
Параметры сервера:
|
||||||
|
|
||||||
|
|||||||
+5
-1
@@ -1512,6 +1512,9 @@ function atrPpsRows(decoded) {
|
|||||||
rows.push([ifc.name, ifaceValue(ifc)]);
|
rows.push([ifc.name, ifaceValue(ifc)]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (decoded.clk_hz) rows.push(['CLK', (decoded.clk_hz / 1e6).toFixed(3) + ' MHz']);
|
||||||
|
if (decoded.data_rate) rows.push(['Data rate', decoded.data_rate + ' bit/s']);
|
||||||
|
if (decoded.etu_us) rows.push(['ETU', decoded.etu_us + ' µs']);
|
||||||
if (decoded.historical) {
|
if (decoded.historical) {
|
||||||
const h = decoded.historical;
|
const h = decoded.historical;
|
||||||
let v = h.category || '';
|
let v = h.category || '';
|
||||||
@@ -1580,6 +1583,7 @@ function showDetail(msg, entry) {
|
|||||||
<table>
|
<table>
|
||||||
<tr><td class="label">Event</td><td class="value">${evName}</td></tr>
|
<tr><td class="label">Event</td><td class="value">${evName}</td></tr>
|
||||||
<tr><td class="label">Level</td><td class="value">${decoded.level || '?'}</td></tr>
|
<tr><td class="label">Level</td><td class="value">${decoded.level || '?'}</td></tr>
|
||||||
|
${decoded.clk_hz ? `<tr><td class="label">CLK</td><td class="value">${(decoded.clk_hz / 1e6).toFixed(3)} MHz</td></tr>` : ''}
|
||||||
${decoded.raw ? `<tr><td class="label">Raw</td><td class="hex">${decoded.raw}</td></tr>` : ''}
|
${decoded.raw ? `<tr><td class="label">Raw</td><td class="hex">${decoded.raw}</td></tr>` : ''}
|
||||||
</table>
|
</table>
|
||||||
</div>`;
|
</div>`;
|
||||||
@@ -2625,7 +2629,7 @@ function onFilterChange() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ──────────────────── Versioning ────────────────────
|
// ──────────────────── Versioning ────────────────────
|
||||||
const PWA_VERSION = '1.22.17';
|
const PWA_VERSION = '1.22.18';
|
||||||
const MIN_SERVER_VERSION = '1.0.0';
|
const MIN_SERVER_VERSION = '1.0.0';
|
||||||
|
|
||||||
function parseVersion(v) {
|
function parseVersion(v) {
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'simtrace-analyser-v49';
|
const CACHE = 'simtrace-analyser-v50';
|
||||||
|
|
||||||
const PRECACHE = [
|
const PRECACHE = [
|
||||||
'index.html',
|
'index.html',
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.backends._legacy:_Backend"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "simtrace2-pysniff"
|
name = "simtrace2-pysniff"
|
||||||
version = "1.22.17"
|
version = "1.22.18"
|
||||||
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"}
|
||||||
|
|||||||
@@ -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.17'
|
__version__ = '1.22.18'
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'SniffSession',
|
'SniffSession',
|
||||||
'DeviceDisconnected',
|
'DeviceDisconnected',
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import time
|
|||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from ..gsmtap import (GsmtapReceiver, GSMTAP_SIM_ATR,
|
from ..gsmtap import (GsmtapReceiver, GSMTAP_SIM_ATR,
|
||||||
|
GSMTAP_SIM_PPS_REQ, GSMTAP_SIM_PPS_RSP,
|
||||||
GSMTAP_SIM_RST_EVENT, GSMTAP_SIM_VCC_EVENT)
|
GSMTAP_SIM_RST_EVENT, GSMTAP_SIM_VCC_EVENT)
|
||||||
|
|
||||||
|
|
||||||
@@ -22,11 +23,14 @@ def _log(msg):
|
|||||||
def gsmtap_msg_type(sub_type):
|
def gsmtap_msg_type(sub_type):
|
||||||
"""Map a GSMTAP-SIM sub_type to a stored message type.
|
"""Map a GSMTAP-SIM sub_type to a stored message type.
|
||||||
|
|
||||||
Custom sigrok-iso7816-stream line events (0x10/0x11) become 'rst'/'vcc';
|
PPS request/response (0x02/0x03, standard GSMTAP) become 'pps'; custom
|
||||||
everything except ATR defaults to 'tpdu'.
|
sigrok-iso7816-stream line events (0x10/0x11) become 'rst'/'vcc';
|
||||||
|
everything else defaults to 'tpdu'.
|
||||||
"""
|
"""
|
||||||
return {
|
return {
|
||||||
GSMTAP_SIM_ATR: 'atr',
|
GSMTAP_SIM_ATR: 'atr',
|
||||||
|
GSMTAP_SIM_PPS_REQ: 'pps',
|
||||||
|
GSMTAP_SIM_PPS_RSP: 'pps',
|
||||||
GSMTAP_SIM_RST_EVENT: 'rst',
|
GSMTAP_SIM_RST_EVENT: 'rst',
|
||||||
GSMTAP_SIM_VCC_EVENT: 'vcc',
|
GSMTAP_SIM_VCC_EVENT: 'vcc',
|
||||||
}.get(sub_type, 'tpdu')
|
}.get(sub_type, 'tpdu')
|
||||||
|
|||||||
@@ -179,17 +179,18 @@ class Database:
|
|||||||
'WHERE session_id=? ORDER BY elapsed, id LIMIT ? OFFSET ?',
|
'WHERE session_id=? ORDER BY elapsed, id LIMIT ? OFFSET ?',
|
||||||
(session_id, limit, offset)).fetchall()
|
(session_id, limit, offset)).fetchall()
|
||||||
initial_states = None
|
initial_states = None
|
||||||
|
clk_hz = None
|
||||||
if offset > 0 and rows:
|
if offset > 0 and rows:
|
||||||
initial_states = self._replay_context(session_id, rows[0][0])
|
initial_states, clk_hz = self._replay_context(session_id, rows[0][0])
|
||||||
return self._decode_rows(rows, initial_states=initial_states)
|
return self._decode_rows(rows, initial_states=initial_states, clk_hz=clk_hz)
|
||||||
|
|
||||||
def get_messages_after(self, session_id, after_id):
|
def get_messages_after(self, session_id, after_id):
|
||||||
rows = self._conn.execute(
|
rows = self._conn.execute(
|
||||||
'SELECT id, session_id, elapsed, type, data, flags FROM messages '
|
'SELECT id, session_id, elapsed, type, data, flags FROM messages '
|
||||||
'WHERE session_id=? AND id > ? ORDER BY elapsed, id',
|
'WHERE session_id=? AND id > ? ORDER BY elapsed, id',
|
||||||
(session_id, after_id)).fetchall()
|
(session_id, after_id)).fetchall()
|
||||||
initial_states = self._replay_context(session_id, after_id + 1)
|
initial_states, clk_hz = self._replay_context(session_id, after_id + 1)
|
||||||
return self._decode_rows(rows, initial_states=initial_states)
|
return self._decode_rows(rows, initial_states=initial_states, clk_hz=clk_hz)
|
||||||
|
|
||||||
def get_messages_raw(self, session_id):
|
def get_messages_raw(self, session_id):
|
||||||
rows = self._conn.execute(
|
rows = self._conn.execute(
|
||||||
@@ -226,7 +227,7 @@ class Database:
|
|||||||
(session_id,)).fetchall()
|
(session_id,)).fetchall()
|
||||||
return [{'type': r[0], 'count': r[1]} for r in rows]
|
return [{'type': r[0], 'count': r[1]} for r in rows]
|
||||||
|
|
||||||
def _row_to_message(self, row, prev=None):
|
def _row_to_message(self, row, prev=None, clk_hz=None):
|
||||||
data_blob = row[4]
|
data_blob = row[4]
|
||||||
flags = row[5]
|
flags = row[5]
|
||||||
msg = {
|
msg = {
|
||||||
@@ -240,7 +241,8 @@ class Database:
|
|||||||
if row[3] in ('tpdu', 'change', 'fidi', 'atr', 'pps', 'rst', 'vcc'):
|
if row[3] in ('tpdu', 'change', 'fidi', 'atr', 'pps', 'rst', 'vcc'):
|
||||||
try:
|
try:
|
||||||
from .decode import decode_sniff_msg
|
from .decode import decode_sniff_msg
|
||||||
msg['decoded'] = decode_sniff_msg(data_blob, row[3], flags, prev=prev)
|
msg['decoded'] = decode_sniff_msg(data_blob, row[3], flags,
|
||||||
|
prev=prev, clk_hz=clk_hz)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import sys
|
import sys
|
||||||
print(f'Decode error for msg {row[0]} ({row[3]}, {len(data_blob)} bytes): {e}',
|
print(f'Decode error for msg {row[0]} ({row[3]}, {len(data_blob)} bytes): {e}',
|
||||||
@@ -260,16 +262,27 @@ class Database:
|
|||||||
|
|
||||||
def _replay_context(self, session_id, before_id):
|
def _replay_context(self, session_id, before_id):
|
||||||
"""Replay (bounded) messages before *before_id* to recover per-channel
|
"""Replay (bounded) messages before *before_id* to recover per-channel
|
||||||
selection, DF, and SFI map."""
|
selection, DF, SFI map, and the last RST-reported CLK frequency.
|
||||||
|
|
||||||
|
Returns ``(states, clk_hz)``."""
|
||||||
from .decode import decode_sniff_msg
|
from .decode import decode_sniff_msg
|
||||||
rows = self._conn.execute(
|
rows = self._conn.execute(
|
||||||
"SELECT data, type FROM messages WHERE session_id=? AND id < ? "
|
"SELECT data, type FROM messages WHERE session_id=? AND id < ? "
|
||||||
"AND type IN ('tpdu','atr','change','gap') ORDER BY id DESC LIMIT 500",
|
"AND type IN ('tpdu','atr','change','gap','rst','vcc') "
|
||||||
|
"ORDER BY id DESC LIMIT 500",
|
||||||
(session_id, before_id)).fetchall()
|
(session_id, before_id)).fetchall()
|
||||||
states = {}
|
states = {}
|
||||||
|
clk_hz = None
|
||||||
for data, typ in reversed(rows):
|
for data, typ in reversed(rows):
|
||||||
if typ in ('atr', 'change', 'gap'):
|
if typ in ('atr', 'change', 'gap'):
|
||||||
states = {}
|
states = {}
|
||||||
|
# An ATR consumes the CLK rate; a replay starts fresh after it.
|
||||||
|
clk_hz = None
|
||||||
|
continue
|
||||||
|
if typ in ('rst', 'vcc'):
|
||||||
|
d = decode_sniff_msg(data, typ, 0)
|
||||||
|
if d and d.get('clk_hz'):
|
||||||
|
clk_hz = d['clk_hz']
|
||||||
continue
|
continue
|
||||||
channel = data[0] & 0x03 if data and len(data) >= 1 else 0
|
channel = data[0] & 0x03 if data and len(data) >= 1 else 0
|
||||||
st = self._channel_state(states, channel)
|
st = self._channel_state(states, channel)
|
||||||
@@ -282,7 +295,7 @@ class Database:
|
|||||||
continue
|
continue
|
||||||
st['sel'], st['df'], st['sfi_map'] = self._advance_state(d, st['sel'], st['df'], st['sfi_map'])
|
st['sel'], st['df'], st['sfi_map'] = self._advance_state(d, st['sel'], st['df'], st['sfi_map'])
|
||||||
st['prev'] = self._context_from_decoded(d)
|
st['prev'] = self._context_from_decoded(d)
|
||||||
return states
|
return states, clk_hz
|
||||||
|
|
||||||
def _advance_state(self, d, sel, df, sfi_map):
|
def _advance_state(self, d, sel, df, sfi_map):
|
||||||
"""Update (sel, df, sfi_map) for one decoded TPDU."""
|
"""Update (sel, df, sfi_map) for one decoded TPDU."""
|
||||||
@@ -350,9 +363,12 @@ class Database:
|
|||||||
return new
|
return new
|
||||||
return sel
|
return sel
|
||||||
|
|
||||||
def _decode_rows(self, rows, initial_states=None):
|
def _decode_rows(self, rows, initial_states=None, clk_hz=None):
|
||||||
|
from .decode import atr_rate_fields
|
||||||
msgs = []
|
msgs = []
|
||||||
states = dict(initial_states or {})
|
states = dict(initial_states or {})
|
||||||
|
last_atr_i = None # index of the most recent ATR in *msgs*
|
||||||
|
last_atr_rated = False # whether that ATR already carries a data rate
|
||||||
for row in rows:
|
for row in rows:
|
||||||
channel = 0
|
channel = 0
|
||||||
if row[3] == 'tpdu' and row[4] and len(row[4]) >= 5:
|
if row[3] == 'tpdu' and row[4] and len(row[4]) >= 5:
|
||||||
@@ -362,13 +378,18 @@ class Database:
|
|||||||
ctx['sel'] = st['sel']
|
ctx['sel'] = st['sel']
|
||||||
ctx['df'] = st['df']
|
ctx['df'] = st['df']
|
||||||
ctx['sfi_map'] = st['sfi_map']
|
ctx['sfi_map'] = st['sfi_map']
|
||||||
msg = self._row_to_message(row, prev=ctx)
|
msg = self._row_to_message(row, prev=ctx, clk_hz=clk_hz)
|
||||||
d = msg.get('decoded')
|
d = msg.get('decoded')
|
||||||
if msg['type'] == 'tpdu' and d and d.get('ins_hex'):
|
if msg['type'] == 'tpdu' and d and d.get('ins_hex'):
|
||||||
st['sel'], st['df'], st['sfi_map'] = self._advance_state(d, st['sel'], st['df'], st['sfi_map'])
|
st['sel'], st['df'], st['sfi_map'] = self._advance_state(d, st['sel'], st['df'], st['sfi_map'])
|
||||||
st['prev'] = self._context_from_decoded(d)
|
st['prev'] = self._context_from_decoded(d)
|
||||||
elif msg['type'] == 'atr':
|
elif msg['type'] == 'atr':
|
||||||
states = {}
|
states = {}
|
||||||
|
# The CLK rate is consumed by the ATR it precedes: an ATR
|
||||||
|
# without a preceding RST event must not inherit a stale rate.
|
||||||
|
last_atr_i = len(msgs)
|
||||||
|
last_atr_rated = bool(d and d.get('data_rate'))
|
||||||
|
clk_hz = None
|
||||||
elif msg['type'] == 'gap':
|
elif msg['type'] == 'gap':
|
||||||
# Device disconnected and reconnected — messages may have been
|
# Device disconnected and reconnected — messages may have been
|
||||||
# missed, so the selection is uncertain.
|
# missed, so the selection is uncertain.
|
||||||
@@ -389,6 +410,17 @@ class Database:
|
|||||||
# removal destroys the card's selection state. De-assert /
|
# removal destroys the card's selection state. De-assert /
|
||||||
# power-on are markers only (the following ATR resets state).
|
# power-on are markers only (the following ATR resets state).
|
||||||
data = row[4] if len(row) > 4 and row[4] else b''
|
data = row[4] if len(row) > 4 and row[4] else b''
|
||||||
|
if d and d.get('clk_hz'):
|
||||||
|
if (last_atr_i is not None and not last_atr_rated
|
||||||
|
and msgs[last_atr_i].get('decoded')):
|
||||||
|
# Ordering caveat: a short ATR can be emitted before
|
||||||
|
# the RST event that measured its CLK rate — backfill
|
||||||
|
# the rate onto that ATR.
|
||||||
|
msgs[last_atr_i]['decoded'].update(
|
||||||
|
atr_rate_fields(d['clk_hz'], msgs[last_atr_i]['decoded']))
|
||||||
|
last_atr_rated = True
|
||||||
|
else:
|
||||||
|
clk_hz = d['clk_hz']
|
||||||
if msg['type'] == 'rst':
|
if msg['type'] == 'rst':
|
||||||
if data and data[0] == 1: # asserted
|
if data and data[0] == 1: # asserted
|
||||||
states = {}
|
states = {}
|
||||||
|
|||||||
@@ -3992,6 +3992,11 @@ DATA_FLAGS = {
|
|||||||
1 << 10: 'parity error',
|
1 << 10: 'parity error',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# sigrok-iso7816-stream line-event payload flags (payload[2] of RST/VCC,
|
||||||
|
# decoder v1.9.0+): bit 0 set → a big-endian uint32 CLK frequency in Hz
|
||||||
|
# follows at bytes 3..6.
|
||||||
|
LINE_EVENT_FLAG_CLK_HZ = 0x01
|
||||||
|
|
||||||
|
|
||||||
def _flag_names(flags, mapping):
|
def _flag_names(flags, mapping):
|
||||||
names = []
|
names = []
|
||||||
@@ -4013,11 +4018,13 @@ def decode_change(flags):
|
|||||||
|
|
||||||
def decode_line_event(raw_data, kind):
|
def decode_line_event(raw_data, kind):
|
||||||
"""Decode a sigrok-iso7816-stream RST/VCC line event (GSMTAP sub_type
|
"""Decode a sigrok-iso7816-stream RST/VCC line event (GSMTAP sub_type
|
||||||
0x10/0x11): payload [direction, level, reserved].
|
0x10/0x11): payload [direction, level, flags, clk_hz_be32?].
|
||||||
|
|
||||||
direction — RST: 1 = asserted (high→low), 0 = de-asserted (low→high);
|
direction — RST: 1 = asserted (high→low), 0 = de-asserted (low→high);
|
||||||
VCC: 1 = power applied (low→high), 0 = removed (high→low).
|
VCC: 1 = power applied (low→high), 0 = removed (high→low).
|
||||||
level — resulting line level: 0 = low, 1 = high.
|
level — resulting line level: 0 = low, 1 = high.
|
||||||
|
flags — bit 0 (LINE_EVENT_FLAG_CLK_HZ) set when a CLK frequency follows
|
||||||
|
(decoder v1.9.0+); bytes 3..6 are then a big-endian uint32 in Hz.
|
||||||
|
|
||||||
``event`` is the canonical value; ``label`` is the human-facing
|
``event`` is the canonical value; ``label`` is the human-facing
|
||||||
summary (e.g. RST de-asserted → "ATR STARTS", VCC applied →
|
summary (e.g. RST de-asserted → "ATR STARTS", VCC applied →
|
||||||
@@ -4041,6 +4048,8 @@ def decode_line_event(raw_data, kind):
|
|||||||
result['event'] = 'power removed'
|
result['event'] = 'power removed'
|
||||||
result['label'] = 'VCC OFF (power-down)'
|
result['label'] = 'VCC OFF (power-down)'
|
||||||
result['level'] = 'high' if level else 'low'
|
result['level'] = 'high' if level else 'low'
|
||||||
|
if (len(raw_data) >= 7 and raw_data[2] & LINE_EVENT_FLAG_CLK_HZ):
|
||||||
|
result['clk_hz'] = int.from_bytes(raw_data[3:7], 'big')
|
||||||
result['raw'] = raw_data.hex().upper()
|
result['raw'] = raw_data.hex().upper()
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -4126,6 +4135,25 @@ def _fidi_dict(fi, di):
|
|||||||
return entry
|
return entry
|
||||||
|
|
||||||
|
|
||||||
|
def atr_rate_fields(clk_hz, decoded):
|
||||||
|
"""Derive the CLK frequency, data rate and ETU for a decoded ATR.
|
||||||
|
|
||||||
|
The Fi/Di communicated by TA1 (ISO 7816-3 §8.3) give the bit duration:
|
||||||
|
``data_rate = clk_hz × D / F`` and ``ETU = F / (clk_hz × D)``. When TA1
|
||||||
|
is absent the interface defaults (F=372, D=1) apply.
|
||||||
|
"""
|
||||||
|
f, d = 372, 1
|
||||||
|
for ifc in (decoded.get('interface') or []):
|
||||||
|
if ifc.get('name') == 'TA1' and 'f' in ifc and 'd' in ifc:
|
||||||
|
f, d = ifc['f'], ifc['d']
|
||||||
|
break
|
||||||
|
return {
|
||||||
|
'clk_hz': clk_hz,
|
||||||
|
'data_rate': int(round(clk_hz * d / f)),
|
||||||
|
'etu_us': round(f / (clk_hz * d) * 1e6, 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _decode_historical(hist):
|
def _decode_historical(hist):
|
||||||
"""Decode ATR historical bytes (ISO 7816-4 §8.1.1)."""
|
"""Decode ATR historical bytes (ISO 7816-4 §8.1.1)."""
|
||||||
result = {'raw': hist.hex().upper()}
|
result = {'raw': hist.hex().upper()}
|
||||||
@@ -4149,8 +4177,13 @@ def _decode_historical(hist):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def _decode_atr(data):
|
def _decode_atr(data, clk_hz=None):
|
||||||
"""Decode an Answer-To-Reset (ISO 7816-3 §8) into a structured dict."""
|
"""Decode an Answer-To-Reset (ISO 7816-3 §8) into a structured dict.
|
||||||
|
|
||||||
|
*clk_hz* — when the sigrok-iso7816-stream decoder reported a CLK
|
||||||
|
frequency on a preceding RST event, the ATR gains ``clk_hz``,
|
||||||
|
``data_rate`` and ``etu_us`` fields derived from TA1 Fi/Di.
|
||||||
|
"""
|
||||||
if not data:
|
if not data:
|
||||||
return {'type': 'atr', 'raw': ''}
|
return {'type': 'atr', 'raw': ''}
|
||||||
ts = data[0]
|
ts = data[0]
|
||||||
@@ -4169,6 +4202,8 @@ def _decode_atr(data):
|
|||||||
'raw': raw,
|
'raw': raw,
|
||||||
}
|
}
|
||||||
if not body:
|
if not body:
|
||||||
|
if clk_hz:
|
||||||
|
result.update(atr_rate_fields(clk_hz, result))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
t0 = body[0]
|
t0 = body[0]
|
||||||
@@ -4261,6 +4296,9 @@ def _decode_atr(data):
|
|||||||
result['tck'] = f'{tck:02X}'
|
result['tck'] = f'{tck:02X}'
|
||||||
result['tck_valid'] = check == tck
|
result['tck_valid'] = check == tck
|
||||||
|
|
||||||
|
if clk_hz:
|
||||||
|
result.update(atr_rate_fields(clk_hz, result))
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@@ -4304,14 +4342,16 @@ def _decode_pps(data):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def decode_sniff_msg(raw_data, msg_type, flags=0, prev=None):
|
def decode_sniff_msg(raw_data, msg_type, flags=0, prev=None, clk_hz=None):
|
||||||
"""Decode a raw sniff message of the given type.
|
"""Decode a raw sniff message of the given type.
|
||||||
|
|
||||||
Returns a structured dict with at least a 'type' key, or None if
|
Returns a structured dict with at least a 'type' key, or None if
|
||||||
the message type has no structured decode.
|
the message type has no structured decode.
|
||||||
|
|
||||||
*prev* optionally carries the previous TPDU's decoded context for
|
*prev* optionally carries the previous TPDU's decoded context for
|
||||||
resolving GET RESPONSE.
|
resolving GET RESPONSE. *clk_hz* is the CLK frequency reported by a
|
||||||
|
preceding RST event (sigrok-iso7816-stream v1.9.0+), used to derive
|
||||||
|
the data rate of an ATR.
|
||||||
"""
|
"""
|
||||||
if msg_type == 'tpdu' and raw_data:
|
if msg_type == 'tpdu' and raw_data:
|
||||||
result = decode_message(raw_data, prev=prev)
|
result = decode_message(raw_data, prev=prev)
|
||||||
@@ -4331,7 +4371,7 @@ def decode_sniff_msg(raw_data, msg_type, flags=0, prev=None):
|
|||||||
if msg_type == 'fidi' and raw_data:
|
if msg_type == 'fidi' and raw_data:
|
||||||
return decode_fidi(raw_data)
|
return decode_fidi(raw_data)
|
||||||
if msg_type == 'atr':
|
if msg_type == 'atr':
|
||||||
result = _decode_atr(raw_data)
|
result = _decode_atr(raw_data, clk_hz=clk_hz)
|
||||||
if result is not None:
|
if result is not None:
|
||||||
errs = (decode_data_flags(flags) or []) + (gsmtap_flag_names(flags) or [])
|
errs = (decode_data_flags(flags) or []) + (gsmtap_flag_names(flags) or [])
|
||||||
if errs:
|
if errs:
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ class TestGsmtapSubtypeMapping(unittest.TestCase):
|
|||||||
def test_standard_subtypes(self):
|
def test_standard_subtypes(self):
|
||||||
self.assertEqual(gsmtap_msg_type(0x00), 'tpdu')
|
self.assertEqual(gsmtap_msg_type(0x00), 'tpdu')
|
||||||
self.assertEqual(gsmtap_msg_type(0x01), 'atr')
|
self.assertEqual(gsmtap_msg_type(0x01), 'atr')
|
||||||
self.assertEqual(gsmtap_msg_type(0x02), 'tpdu') # PPS combined
|
self.assertEqual(gsmtap_msg_type(0x02), 'pps') # PPS request
|
||||||
|
self.assertEqual(gsmtap_msg_type(0x03), 'pps') # PPS response
|
||||||
|
|
||||||
|
|
||||||
class _FakeBackend:
|
class _FakeBackend:
|
||||||
|
|||||||
@@ -871,6 +871,35 @@ class TestAtrPps(unittest.TestCase):
|
|||||||
self.assertEqual(r['historical_len'], 2)
|
self.assertEqual(r['historical_len'], 2)
|
||||||
self.assertNotIn('tck', r)
|
self.assertNotIn('tck', r)
|
||||||
|
|
||||||
|
def test_atr_data_rate_from_clk(self):
|
||||||
|
from simtrace2_pysniff.server.decode import _decode_atr
|
||||||
|
# TA1=11 → F=372, D=1; CLK 3.579545 MHz → 9622 bit/s, ETU 103.9 µs
|
||||||
|
r = _decode_atr(bytes.fromhex('3B12110000'), clk_hz=3579545)
|
||||||
|
self.assertEqual(r['clk_hz'], 3579545)
|
||||||
|
self.assertEqual(r['data_rate'], 9622)
|
||||||
|
self.assertEqual(r['etu_us'], 103.9)
|
||||||
|
|
||||||
|
def test_atr_data_rate_uses_ta1_fidi(self):
|
||||||
|
from simtrace2_pysniff.server.decode import _decode_atr
|
||||||
|
# TA1=95 → F=512, D=16; CLK 4 MHz → 125000 bit/s, ETU 8.0 µs
|
||||||
|
r = _decode_atr(bytes.fromhex('3B1095'), clk_hz=4000000)
|
||||||
|
self.assertEqual(r['data_rate'], 125000)
|
||||||
|
self.assertEqual(r['etu_us'], 8.0)
|
||||||
|
|
||||||
|
def test_atr_data_rate_defaults_without_ta1(self):
|
||||||
|
from simtrace2_pysniff.server.decode import _decode_atr
|
||||||
|
# No TA1 → default F=372, D=1
|
||||||
|
r = _decode_atr(bytes.fromhex('3B00'), clk_hz=4000000)
|
||||||
|
self.assertEqual(r['data_rate'], 10753)
|
||||||
|
self.assertEqual(r['etu_us'], 93.0)
|
||||||
|
|
||||||
|
def test_atr_no_clk_no_rate(self):
|
||||||
|
from simtrace2_pysniff.server.decode import _decode_atr
|
||||||
|
r = _decode_atr(bytes.fromhex('3B12110000'))
|
||||||
|
self.assertNotIn('clk_hz', r)
|
||||||
|
self.assertNotIn('data_rate', r)
|
||||||
|
self.assertNotIn('etu_us', r)
|
||||||
|
|
||||||
def test_atr_td1_t1_has_tck(self):
|
def test_atr_td1_t1_has_tck(self):
|
||||||
from simtrace2_pysniff.server.decode import _decode_atr
|
from simtrace2_pysniff.server.decode import _decode_atr
|
||||||
# 3B 80 01 80 31 80 66 ... : construct T=1 with TCK
|
# 3B 80 01 80 31 80 66 ... : construct T=1 with TCK
|
||||||
@@ -2431,6 +2460,55 @@ class TestLineEvents(unittest.TestCase):
|
|||||||
self.assertEqual(r['type'], 'rst')
|
self.assertEqual(r['type'], 'rst')
|
||||||
self.assertNotIn('event', r)
|
self.assertNotIn('event', r)
|
||||||
|
|
||||||
|
def test_rst_clk_extension(self):
|
||||||
|
# v1.9.0 payload: direction, level, flags(bit0=CLK), clk_hz_be32.
|
||||||
|
r = self._dec('rst', '000101003d0900') # 4 MHz
|
||||||
|
self.assertEqual(r['event'], 'reset de-asserted')
|
||||||
|
self.assertEqual(r['clk_hz'], 4000000)
|
||||||
|
|
||||||
|
def test_rst_clk_extension_asserted(self):
|
||||||
|
r = self._dec('rst', '010001003d0900') # 4 MHz
|
||||||
|
self.assertEqual(r['event'], 'reset asserted')
|
||||||
|
self.assertEqual(r['clk_hz'], 4000000)
|
||||||
|
|
||||||
|
def test_rst_clk_flag_absent(self):
|
||||||
|
r = self._dec('rst', '000100003d0900')
|
||||||
|
self.assertNotIn('clk_hz', r)
|
||||||
|
|
||||||
|
def test_rst_clk_flag_truncated(self):
|
||||||
|
# Flag says CLK follows but the payload is too short — ignore.
|
||||||
|
r = self._dec('rst', '000101003d09')
|
||||||
|
self.assertNotIn('clk_hz', r)
|
||||||
|
|
||||||
|
def test_db_atr_data_rate(self):
|
||||||
|
# CLK from RST → data rate on the following ATR; rate consumed once,
|
||||||
|
# so a second ATR without a preceding RST has no rate.
|
||||||
|
import tempfile
|
||||||
|
from simtrace2_pysniff.server.database import Database
|
||||||
|
with tempfile.NamedTemporaryFile(suffix='.db') as tmp:
|
||||||
|
db = Database(tmp.name)
|
||||||
|
sid = db.create_session('capture')
|
||||||
|
db.insert_message(sid, 0.1, 'rst', bytes.fromhex('000101003d0900'), 0)
|
||||||
|
db.insert_message(sid, 0.2, 'atr', bytes.fromhex('3B12110000'), 0)
|
||||||
|
db.insert_message(sid, 0.3, 'atr', bytes.fromhex('3B12110000'), 0)
|
||||||
|
msgs = db.get_messages(sid)
|
||||||
|
self.assertEqual(msgs[1]['decoded']['data_rate'], 10753) # 4 MHz / 372
|
||||||
|
self.assertEqual(msgs[1]['decoded']['etu_us'], 93.0)
|
||||||
|
self.assertNotIn('data_rate', msgs[2]['decoded'])
|
||||||
|
|
||||||
|
def test_db_atr_rate_backfill(self):
|
||||||
|
# Ordering caveat: a short ATR can be emitted before the RST event
|
||||||
|
# that measured its CLK rate — the rate is backfilled onto it.
|
||||||
|
import tempfile
|
||||||
|
from simtrace2_pysniff.server.database import Database
|
||||||
|
with tempfile.NamedTemporaryFile(suffix='.db') as tmp:
|
||||||
|
db = Database(tmp.name)
|
||||||
|
sid = db.create_session('capture')
|
||||||
|
db.insert_message(sid, 0.1, 'atr', bytes.fromhex('3B12110000'), 0)
|
||||||
|
db.insert_message(sid, 0.2, 'rst', bytes.fromhex('000101003d0900'), 0)
|
||||||
|
msgs = db.get_messages(sid)
|
||||||
|
self.assertEqual(msgs[0]['decoded']['data_rate'], 10753)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
@@ -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.17')
|
self.assertEqual(__version__, '1.22.18')
|
||||||
|
|
||||||
def test_status_includes_version(self):
|
def test_status_includes_version(self):
|
||||||
import os
|
import os
|
||||||
|
|||||||
Reference in new issue
Block a user