Track device connection and capture gaps; surface sniff error flags
This commit is contained in:
1 parent
26043e5381
commit
5d91450b55
10 files changed
+287
-29
No files matched your search
@@ -55,9 +55,6 @@ def build_parser():
|
||||
p.add_argument('--vendor', type=lambda x: int(x, 16),
|
||||
default=0x1d50, metavar='HEX',
|
||||
help='USB vendor ID (default: 0x1d50)')
|
||||
p.add_argument('--product', type=lambda x: int(x, 16),
|
||||
default=None, metavar='HEX',
|
||||
help='USB product ID (default: auto-detect)')
|
||||
|
||||
rec = p.add_argument_group('recovery options')
|
||||
rec.add_argument('--no-reconnect', action='store_true',
|
||||
@@ -112,6 +109,7 @@ def main():
|
||||
reconnect_delay_max=args.reconnect_delay_max,
|
||||
backoff_factor=args.backoff_factor,
|
||||
inactivity_timeout=args.inactivity_timeout,
|
||||
vendor_id=args.vendor,
|
||||
)
|
||||
|
||||
exiting = False
|
||||
|
||||
+29
-10
@@ -45,13 +45,7 @@ def find_sniffer_device(vendor_id=USB_VENDOR_OPENMOKO):
|
||||
|
||||
Returns a ``usb.core.Device`` on success, or ``None``.
|
||||
"""
|
||||
devices = list(usb.core.find(
|
||||
find_all=True,
|
||||
idVendor=vendor_id,
|
||||
custom_match=lambda d: (
|
||||
True
|
||||
),
|
||||
))
|
||||
devices = list(usb.core.find(find_all=True, idVendor=vendor_id))
|
||||
|
||||
for dev in devices:
|
||||
for cfg in dev:
|
||||
@@ -71,7 +65,9 @@ def _get_ep_addrs(dev):
|
||||
intf.bInterfaceSubClass == SIMTRACE_SNIFFER_USB_SUBCLASS):
|
||||
for ep in intf:
|
||||
addr = ep.bEndpointAddress
|
||||
if addr & 0x80: # IN endpoint
|
||||
# BULK IN endpoint only — the interrupt IN endpoint carries
|
||||
# cardem notifications, not sniff data.
|
||||
if addr & 0x80 and ep.bmAttributes & 0x03 == 0x02:
|
||||
return addr
|
||||
return None
|
||||
|
||||
@@ -100,16 +96,19 @@ class SniffSession:
|
||||
reconnect_delay_min=1.0,
|
||||
reconnect_delay_max=30.0,
|
||||
backoff_factor=1.5,
|
||||
inactivity_timeout=0.0):
|
||||
inactivity_timeout=0.0,
|
||||
vendor_id=USB_VENDOR_OPENMOKO):
|
||||
self._reconnect = reconnect
|
||||
self._reconnect_delay_min = reconnect_delay_min
|
||||
self._reconnect_delay_max = reconnect_delay_max
|
||||
self._backoff_factor = backoff_factor
|
||||
self._inactivity_timeout = inactivity_timeout
|
||||
self._vendor_id = vendor_id
|
||||
|
||||
self._dev = None
|
||||
self._devh = None
|
||||
self._ep_in = None
|
||||
self._connected = False
|
||||
self._last_msg_time = 0.0
|
||||
self._disconnect_count = 0
|
||||
|
||||
@@ -121,7 +120,7 @@ class SniffSession:
|
||||
if self._devh is not None:
|
||||
return
|
||||
|
||||
dev = find_sniffer_device()
|
||||
dev = find_sniffer_device(vendor_id=self._vendor_id)
|
||||
if dev is None:
|
||||
raise DeviceDisconnected('No SIMtrace2 sniffer device found')
|
||||
|
||||
@@ -147,6 +146,7 @@ class SniffSession:
|
||||
self._dev = dev
|
||||
self._devh = devh
|
||||
self._ep_in = ep_in
|
||||
self._connected = True
|
||||
self._last_msg_time = time.monotonic()
|
||||
self._disconnect_count = 0
|
||||
|
||||
@@ -167,6 +167,7 @@ class SniffSession:
|
||||
self._devh = None
|
||||
self._dev = None
|
||||
self._ep_in = None
|
||||
self._connected = False
|
||||
|
||||
def _backoff_wait(self):
|
||||
"""Sleep with exponential backoff before reconnecting."""
|
||||
@@ -236,20 +237,38 @@ class SniffSession:
|
||||
f'Inactivity timeout ({elapsed:.1f}s > '
|
||||
f'{self._inactivity_timeout:.1f}s)')
|
||||
|
||||
@property
|
||||
def connected(self):
|
||||
"""True while the USB sniffer device is open and claimed."""
|
||||
return self._connected
|
||||
|
||||
def iter_messages(self):
|
||||
"""Generator of SniffMessage objects.
|
||||
|
||||
Automatically reconnects on disconnect if ``reconnect=True``.
|
||||
Raises ``DeviceDisconnected`` if reconnect is disabled or exhausted.
|
||||
|
||||
When an established connection is lost and a reconnect is pending, a
|
||||
single synthetic ``SniffMessage(type='gap')`` is yielded so callers can
|
||||
record the interruption (messages may have been missed).
|
||||
"""
|
||||
from .protocol import SniffMessage
|
||||
|
||||
was_connected = False
|
||||
while True:
|
||||
try:
|
||||
self._ensure_connected()
|
||||
was_connected = True
|
||||
yield from self._read_loop()
|
||||
except DeviceDisconnected:
|
||||
self._close()
|
||||
if not self._reconnect:
|
||||
raise
|
||||
if was_connected:
|
||||
yield SniffMessage(type='gap', type_hex='0x00', data=b'',
|
||||
flags=0, slot_nr=0, seq_nr=0,
|
||||
timestamp=time.time())
|
||||
was_connected = False
|
||||
self._backoff_wait()
|
||||
|
||||
def close(self):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in new issue
Block a user