Track device connection and capture gaps; surface sniff error flags

This commit is contained in:
catarrh committed 2026-08-19 18:00:52 +03:00
1 parent 26043e5381
commit 5d91450b55
10 files changed
+287 -29

No files matched your search

+13
View File
@@ -38,6 +38,11 @@ class DirectSniffer:
)
self._running = False
@property
def connected(self):
"""True while the USB sniffer device is open."""
return self._session.connected
def start(self):
self._running = True
@@ -81,6 +86,14 @@ class CaptureManager:
def latest_msg_id(self):
return self._latest_msg_id
@property
def device_connected(self):
"""True while the capture device is connected, or None when the
backend has no device concept (gsmtap)."""
if isinstance(self._backend, DirectSniffer):
return self._backend.connected
return None
def start_session(self):
if self._session_id is not None:
self.stop_session()
+18 -3
View File
@@ -257,11 +257,11 @@ class Database:
from .decode import decode_sniff_msg
rows = self._conn.execute(
"SELECT data, type FROM messages WHERE session_id=? AND id < ? "
"AND type IN ('tpdu','atr','change') ORDER BY id DESC LIMIT 500",
"AND type IN ('tpdu','atr','change','gap') ORDER BY id DESC LIMIT 500",
(session_id, before_id)).fetchall()
states = {}
for data, typ in reversed(rows):
if typ in ('atr', 'change'):
if typ in ('atr', 'change', 'gap'):
states = {}
continue
channel = data[0] & 0x03 if data and len(data) >= 1 else 0
@@ -358,7 +358,22 @@ class Database:
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['prev'] = self._context_from_decoded(d)
elif msg['type'] in ('atr', 'change'):
elif msg['type'] == 'atr':
states = {}
elif msg['type'] == 'gap':
# Device disconnected and reconnected — messages may have been
# missed, so the selection is uncertain.
states = {}
elif msg['type'] == 'change':
# Only card-level changes invalidate the selection. A
# waiting-time timeout interrupts a single TPDU but leaves the
# card selected, so the phone usually just retries.
from ..protocol import (
CHANGE_FLAG_CARD_INSERT, CHANGE_FLAG_CARD_EJECT,
CHANGE_FLAG_RESET_ASSERT, CHANGE_FLAG_RESET_DEASSERT,
)
if msg['flags'] & (CHANGE_FLAG_CARD_INSERT | CHANGE_FLAG_CARD_EJECT |
CHANGE_FLAG_RESET_ASSERT | CHANGE_FLAG_RESET_DEASSERT):
states = {}
msgs.append(msg)
return msgs
+45 -7
View File
@@ -3258,13 +3258,28 @@ CHANGE_FLAGS = {
1 << 4: 'Waiting time timeout',
}
# Sniff data error flags (SNIFF_DATA_FLAG_ERROR_*, simtrace_prot.h).
DATA_FLAGS = {
1 << 5: 'incomplete',
1 << 6: 'malformed',
1 << 7: 'checksum error',
1 << 8: 'overrun',
1 << 9: 'framing error',
1 << 10: 'parity error',
}
def _flag_names(flags, mapping):
names = []
for mask, name in sorted(mapping.items()):
if flags & mask:
names.append(name)
return names
def decode_change(flags):
"""Decode SIMtrace2 sniff_change flags into a list of human-readable names."""
bits = []
for mask, name in sorted(CHANGE_FLAGS.items()):
if flags & mask:
bits.append(name)
bits = _flag_names(flags, CHANGE_FLAGS)
return {
'type': 'change',
'flags_hex': f'{flags:08x}',
@@ -3272,6 +3287,14 @@ def decode_change(flags):
}
def decode_data_flags(flags):
"""Decode SIMtrace2 sniff_data error flags into a list of names (or None)."""
if not flags:
return None
names = _flag_names(flags, DATA_FLAGS)
return names or None
def decode_fidi(raw_data):
"""Decode a SIMtrace2 sniff_fidi payload (1-byte Fi/Di)."""
if len(raw_data) < 1:
@@ -3514,15 +3537,30 @@ def decode_sniff_msg(raw_data, msg_type, flags=0, prev=None):
resolving GET RESPONSE.
"""
if msg_type == 'tpdu' and raw_data:
return decode_message(raw_data, prev=prev)
result = decode_message(raw_data, prev=prev)
if result is not None:
errs = decode_data_flags(flags)
if errs:
result['errors'] = errs
return result
if msg_type == 'change':
return decode_change(flags)
if msg_type == 'fidi' and raw_data:
return decode_fidi(raw_data)
if msg_type == 'atr':
return _decode_atr(raw_data)
result = _decode_atr(raw_data)
if result is not None:
errs = decode_data_flags(flags)
if errs:
result['errors'] = errs
return result
if msg_type == 'pps':
return _decode_pps(raw_data)
result = _decode_pps(raw_data)
if result is not None:
errs = decode_data_flags(flags)
if errs:
result['errors'] = errs
return result
return None
+5 -1
View File
@@ -202,6 +202,7 @@ class RequestHandler(BaseHTTPRequestHandler):
'session_id': capture.session_id if capture else None,
'mode': active_session['mode'] if active_session else None,
'messages_count': self.db.count_messages(capture.session_id) if capture and capture.session_id else 0,
'device_connected': capture.device_connected if capture else None,
})
def _handle_list_sessions(self):
@@ -258,7 +259,8 @@ class RequestHandler(BaseHTTPRequestHandler):
def _handle_capture_latest(self, params):
after_id = int(params.get('after', [0])[0])
if not self.capture or not self.capture.active:
self._send_json({'messages': [], 'next_after': after_id, 'active': False})
self._send_json({'messages': [], 'next_after': after_id, 'active': False,
'device_connected': self.capture.device_connected if self.capture else None})
return
msg_id = self.capture.latest_msg_id
messages = self.db.get_messages_after(self.capture.session_id, after_id)
@@ -267,12 +269,14 @@ class RequestHandler(BaseHTTPRequestHandler):
'next_after': msg_id,
'active': True,
'session_id': self.capture.session_id,
'device_connected': self.capture.device_connected,
})
def _handle_capture_status(self):
self._send_json({
'active': self.capture.active if self.capture else False,
'session_id': self.capture.session_id if self.capture else None,
'device_connected': self.capture.device_connected if self.capture else None,
})
def _handle_capture_start(self):