Initial commit: simtrace2-pysniff v0.1.0

Python-based SIMtrace2 sniffer — drop-in replacement for simtrace2-sniff.
Single dependency (pyusb), auto-reconnect, GSMTAP/PCAP output, library API.

Modules:
  protocol.py — SIMtrace2 USB sniffing protocol parser
  device.py   — USB discovery + SniffSession with exponential backoff reconnect
  gsmtap.py   — GSMTAP UDP sender for Wireshark
  pcap.py     — PCAP file writer (LINKTYPE_GSMTAP)
  dump.py     — hex dump formatters + file output
  __main__.py — CLI entry point

Startup script sniff-start.sh handles udev permission checks and shows
install instructions. udev rule uses TAG+=uaccess (systemd-logind ACL).
This commit is contained in:
catarrh committed 2026-08-10 23:17:04 +03:00
commit 73b0277ef7
13 files changed
+1289

No files matched your search

+21
View File
@@ -0,0 +1,21 @@
"""simtrace2-pysniff — Python replacement for simtrace2-sniff."""
from .device import SniffSession, DeviceDisconnected, find_sniffer_device
from .protocol import parse_message, SniffMessage
from .gsmtap import GsmtapSender
from .pcap import PcapWriter
from .dump import FileDumper, format_message
__version__ = '0.1.0'
__all__ = [
'SniffSession',
'DeviceDisconnected',
'find_sniffer_device',
'parse_message',
'SniffMessage',
'GsmtapSender',
'PcapWriter',
'FileDumper',
'format_message',
'__version__',
]
+167
View File
@@ -0,0 +1,167 @@
"""simtrace2-pysniff — CLI entry point.
Usage: python -m simtrace2_pysniff [options]
"""
import argparse
import signal
import sys
import time
from . import (
SniffSession,
DeviceDisconnected,
GsmtapSender,
PcapWriter,
FileDumper,
format_message,
__version__,
)
from .gsmtap import build_gsmtap_packet, GSMTAP_SIM_ATR, GSMTAP_SIM_APDU
def parse_gsmtap_addr(value):
parts = value.rsplit(':', 1)
host = parts[0]
port = 4729
if len(parts) == 2:
try:
port = int(parts[1])
except ValueError:
raise argparse.ArgumentTypeError(
f'Invalid GSMTAP port: {parts[1]}')
return host, port
def build_parser():
p = argparse.ArgumentParser(
prog='simtrace2-pysniff',
description='Python-based SIMtrace2 sniffer with GSMTAP/PCAP output',
)
p.add_argument('--version', action='version',
version=f'simtrace2-pysniff {__version__}')
p.add_argument('--gsmtap', type=parse_gsmtap_addr, metavar='HOST[:PORT]',
help='Send ATR/TPDU as GSMTAP over UDP (default port 4729)')
p.add_argument('--pcap', metavar='FILE',
help='Write sniffed data to PCAP file for Wireshark')
p.add_argument('--output', '-o', metavar='FILE',
help='Write hex dump to file')
p.add_argument('--no-stdout', action='store_true',
help='Suppress hex dump to stdout')
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',
help='Exit on USB disconnect instead of reconnecting')
rec.add_argument('--reconnect-delay-min', type=float, default=1.0,
metavar='SECONDS',
help='Initial reconnect delay (default: 1.0)')
rec.add_argument('--reconnect-delay-max', type=float, default=30.0,
metavar='SECONDS',
help='Maximum reconnect delay (default: 30.0)')
rec.add_argument('--backoff-factor', type=float, default=1.5,
metavar='N',
help='Exponential backoff multiplier (default: 1.5)')
rec.add_argument('--inactivity-timeout', type=float, default=0.0,
metavar='SECONDS',
help='Reconnect after N seconds of silence (default: disabled)')
return p
def print_banner():
print('simtrace2-pysniff — Phone-SIM card communication sniffer\n'
f'Version {__version__} — Python-based replacement for simtrace2-sniff\n',
file=sys.stderr)
print('Entering main loop', file=sys.stderr)
def main():
parser = build_parser()
args = parser.parse_args()
print_banner()
gsmtap = None
if args.gsmtap:
host, port = args.gsmtap
gsmtap = GsmtapSender(host, port)
print(f'GSMTAP output: {host}:{port}', file=sys.stderr)
pcap = None
if args.pcap:
pcap = PcapWriter(args.pcap)
print(f'PCAP output: {args.pcap}', file=sys.stderr)
file_dumper = None
if args.output:
file_dumper = FileDumper(args.output)
print(f'File output: {args.output}', file=sys.stderr)
session = SniffSession(
reconnect=not args.no_reconnect,
reconnect_delay_min=args.reconnect_delay_min,
reconnect_delay_max=args.reconnect_delay_max,
backoff_factor=args.backoff_factor,
inactivity_timeout=args.inactivity_timeout,
)
exiting = False
def on_signal(signum, frame):
nonlocal exiting
if exiting:
raise SystemExit(1)
exiting = True
print('\nShutting down...', file=sys.stderr)
session.close()
if gsmtap:
gsmtap.close()
if pcap:
pcap.close()
if file_dumper:
file_dumper.close()
sys.exit(0)
signal.signal(signal.SIGINT, on_signal)
signal.signal(signal.SIGTERM, on_signal)
try:
for msg in session.iter_messages():
if not args.no_stdout:
print(format_message(msg))
if file_dumper is not None:
file_dumper.write(msg)
if gsmtap is not None and msg.type in ('atr', 'tpdu'):
if msg.type == 'atr':
gsmtap.send_atr(msg.data, msg.slot_nr)
else:
gsmtap.send_apdu(msg.data, msg.slot_nr)
if pcap is not None:
sub_type = GSMTAP_SIM_ATR if msg.type == 'atr' else GSMTAP_SIM_APDU
gsmtap_hdr = build_gsmtap_packet(sub_type, b'')[:16]
pcap.write_packet(gsmtap_hdr, msg.data, msg.timestamp)
except DeviceDisconnected as e:
print(f'Device disconnected: {e}', file=sys.stderr)
except KeyboardInterrupt:
pass
finally:
session.close()
if gsmtap:
gsmtap.close()
if pcap:
pcap.close()
if file_dumper:
file_dumper.close()
if __name__ == '__main__':
main()
+257
View File
@@ -0,0 +1,257 @@
"""USB device discovery and sniffing session with auto-reconnect."""
import os
import sys
import time
import usb.core
import usb.util
USB_VENDOR_OPENMOKO = 0x1d50
USB_CLASS_PROPRIETARY = 0xff
SIMTRACE_SNIFFER_USB_SUBCLASS = 1
FATAL_USB_ERRNOS = {
2, # ENOENT
5, # EIO (some platforms)
19, # ENODEV (Linux)
32, # EPIPE / ESHUTDOWN (some platforms)
108, # ESHUTDOWN (Linux)
}
class DeviceDisconnected(Exception):
"""Raised when the USB device disappears or becomes unresponsive."""
pass
def _is_fatal_usb_error(err):
if isinstance(err, usb.core.USBError):
if hasattr(err, 'errno') and err.errno is not None:
return err.errno in FATAL_USB_ERRNOS
if err.strerror and ('device' in err.strerror.lower() or
'disconnect' in err.strerror.lower()):
return True
return False
def _usb_error_str(err):
if isinstance(err, usb.core.USBError):
return f'USBError(errno={getattr(err, "errno", "N/A")}, strerror={err.strerror})'
return str(err)
def find_sniffer_device(vendor_id=USB_VENDOR_OPENMOKO):
"""Find a SIMtrace2 device running sniffer firmware.
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
),
))
for dev in devices:
for cfg in dev:
for intf in cfg:
cls = intf.bInterfaceClass
sub = intf.bInterfaceSubClass
if cls == USB_CLASS_PROPRIETARY and sub == SIMTRACE_SNIFFER_USB_SUBCLASS:
return dev
return None
def _get_ep_addrs(dev):
"""Return (ep_out, ep_in, ep_irq) addresses from the active config."""
cfg = dev.get_active_configuration()
for intf in cfg:
if (intf.bInterfaceClass == USB_CLASS_PROPRIETARY and
intf.bInterfaceSubClass == SIMTRACE_SNIFFER_USB_SUBCLASS):
for ep in intf:
addr = ep.bEndpointAddress
if addr & 0x80: # IN endpoint
return addr
return None
class SniffSession:
"""Manages a SIMtrace2 sniffer USB connection with auto-reconnect.
Usage::
session = SniffSession(
inactivity_timeout=10.0,
reconnect_delay_min=1.0,
reconnect_delay_max=30.0,
)
try:
for msg in session.iter_messages():
print(f"{msg.type}: {msg.data.hex()}")
except KeyboardInterrupt:
pass
finally:
session.close()
"""
def __init__(self, *,
reconnect=True,
reconnect_delay_min=1.0,
reconnect_delay_max=30.0,
backoff_factor=1.5,
inactivity_timeout=0.0):
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._dev = None
self._devh = None
self._ep_in = None
self._last_msg_time = 0.0
self._disconnect_count = 0
def _ensure_connected(self):
"""Find, open, and claim the sniffer device.
Raises DeviceDisconnected if no device found or open fails.
"""
if self._devh is not None:
return
dev = find_sniffer_device()
if dev is None:
raise DeviceDisconnected('No SIMtrace2 sniffer device found')
try:
if dev.is_kernel_driver_active(0):
dev.detach_kernel_driver(0)
except (usb.core.USBError, NotImplementedError):
pass
try:
devh = dev
devh.set_configuration()
usb.util.claim_interface(devh, 0)
except usb.core.USBError as e:
raise DeviceDisconnected(
f'Failed to open/claim device: {_usb_error_str(e)}') from e
ep_in = _get_ep_addrs(devh)
if ep_in is None:
usb.util.release_interface(devh, 0)
raise DeviceDisconnected('No BULK IN endpoint found on sniffer interface')
self._dev = dev
self._devh = devh
self._ep_in = ep_in
self._last_msg_time = time.monotonic()
self._disconnect_count = 0
print(f'Connected to SIMtrace2 sniffer (EP IN 0x{ep_in:02x})',
file=sys.stderr)
def _close(self):
"""Release USB resources gracefully."""
if self._devh is not None:
try:
usb.util.release_interface(self._devh, 0)
except Exception:
pass
try:
usb.util.dispose_resources(self._devh)
except Exception:
pass
self._devh = None
self._dev = None
self._ep_in = None
def _backoff_wait(self):
"""Sleep with exponential backoff before reconnecting."""
delay = self._reconnect_delay_min * (self._backoff_factor ** self._disconnect_count)
delay = min(delay, self._reconnect_delay_max)
self._disconnect_count += 1
print(f'Disconnected — retrying in {delay:.1f}s '
f'(attempt {self._disconnect_count})',
file=sys.stderr)
time.sleep(delay)
def _read_loop(self):
"""Inner read loop. Yields SniffMessages. Raises DeviceDisconnected on error."""
from .protocol import parse_message
buf = bytearray()
max_buf = 1024 * 1024 # 1 MB
while True:
try:
chunk = self._devh.read(
self._ep_in, 65536, timeout=1000)
chunk = bytes(chunk)
except usb.core.USBTimeoutError:
self._check_inactivity()
continue
except usb.core.USBError as e:
if _is_fatal_usb_error(e):
raise DeviceDisconnected(
f'USB read error: {_usb_error_str(e)}') from e
print(f'Transient USB error: {_usb_error_str(e)}',
file=sys.stderr)
time.sleep(0.1)
continue
except Exception as e:
print(f'Unexpected read error: {e}', file=sys.stderr)
time.sleep(0.1)
continue
if not chunk:
self._check_inactivity()
continue
self._last_msg_time = time.monotonic()
buf.extend(chunk)
if len(buf) > max_buf:
print(f'Buffer overflow ({len(buf)} > {max_buf}), '
f'discarding oldest data', file=sys.stderr)
buf = buf[-max_buf // 2:]
while True:
msg, consumed = parse_message(buf, timestamp=time.time())
if msg is not None:
yield msg
if consumed <= 0 or consumed > len(buf):
break
buf = buf[consumed:]
def _check_inactivity(self):
if self._inactivity_timeout <= 0:
return
elapsed = time.monotonic() - self._last_msg_time
if elapsed > self._inactivity_timeout:
raise DeviceDisconnected(
f'Inactivity timeout ({elapsed:.1f}s > '
f'{self._inactivity_timeout:.1f}s)')
def iter_messages(self):
"""Generator of SniffMessage objects.
Automatically reconnects on disconnect if ``reconnect=True``.
Raises ``DeviceDisconnected`` if reconnect is disabled or exhausted.
"""
while True:
try:
self._ensure_connected()
yield from self._read_loop()
except DeviceDisconnected:
self._close()
if not self._reconnect:
raise
self._backoff_wait()
def close(self):
self._reconnect = False
self._close()
+106
View File
@@ -0,0 +1,106 @@
"""Hex dump formatters for simtrace2 sniffing output."""
import sys
def hexdump(data, sep=' '):
"""Return hex string representation of bytes.
>>> hexdump(b'\\x3b\\x9e\\x12\\x34')
'3b 9e 12 34'
"""
return data.hex(sep)
def _flag_str(flags, flag_names):
if not flags:
return ''
names = []
for bit, name in sorted(flag_names.items()):
if flags & bit:
names.append(name)
flags &= ~bit
if flags:
names.append(f'unknown(0x{flags:08x})')
return ', '.join(names)
def format_message(msg, *, show_flags=True):
"""Format a SniffMessage as a human-readable line.
Returns a str suitable for stdout or file output.
"""
from .protocol import (
CHANGE_FLAG_CARD_INSERT, CHANGE_FLAG_CARD_EJECT,
CHANGE_FLAG_RESET_ASSERT, CHANGE_FLAG_RESET_DEASSERT,
CHANGE_FLAG_TIMEOUT_WT,
DATA_FLAG_ERROR_INCOMPLETE, DATA_FLAG_ERROR_MALFORMED,
DATA_FLAG_ERROR_CHECKSUM, DATA_FLAG_ERROR_OVERRUN,
DATA_FLAG_ERROR_FRAMING, DATA_FLAG_ERROR_PARITY,
)
change_flag_names = {
CHANGE_FLAG_CARD_INSERT: 'card inserted',
CHANGE_FLAG_CARD_EJECT: 'card ejected',
CHANGE_FLAG_RESET_ASSERT: 'reset asserted',
CHANGE_FLAG_RESET_DEASSERT: 'reset de-asserted',
CHANGE_FLAG_TIMEOUT_WT: 'waiting time timeout',
}
data_flag_names = {
DATA_FLAG_ERROR_INCOMPLETE: 'incomplete',
DATA_FLAG_ERROR_MALFORMED: 'malformed',
DATA_FLAG_ERROR_CHECKSUM: 'checksum error',
DATA_FLAG_ERROR_OVERRUN: 'overrun',
DATA_FLAG_ERROR_FRAMING: 'framing error',
DATA_FLAG_ERROR_PARITY: 'parity error',
}
type_label = msg.type.upper()
if msg.type == 'change':
flags_str = _flag_str(msg.flags, change_flag_names)
line = f'Card state change: {flags_str}' if flags_str else 'Card state change: no changes'
elif msg.type == 'fidi':
fidi = msg.data[0] if len(msg.data) >= 1 else 0
fi = fidi >> 4
di = fidi & 0x0f
line = f'Fi/Di switched to {_fi_table.get(fi, fi)}/{_di_table.get(di, di)}'
else:
flags_str = ''
if show_flags and msg.flags:
flags_str = f' ({_flag_str(msg.flags, data_flag_names)})'
line = f'{type_label}{flags_str}: {hexdump(msg.data)}'
return line
class FileDumper:
"""Write formatted sniff messages to a file."""
def __init__(self, path):
self._f = open(path, 'a', buffering=1)
def write(self, msg):
line = format_message(msg)
self._f.write(line + '\n')
def close(self):
self._f.close()
# ISO 7816-3:2006 Table 7
_fi_table = {
0: 372, 1: 372, 2: 558, 3: 744,
4: 1116, 5: 1488, 6: 1860, 7: 0,
8: 0, 9: 512, 10: 768, 11: 1024,
12: 1536, 13: 2048, 14: 0, 15: 0,
}
# ISO 7816-3:2006 Table 8
_di_table = {
0: 0, 1: 1, 2: 2, 3: 4,
4: 8, 5: 16, 6: 32, 7: 64,
8: 12, 9: 20, 10: 2, 11: 4,
12: 8, 13: 16, 14: 32, 15: 64,
}
+61
View File
@@ -0,0 +1,61 @@
"""GSMTAP UDP sender for Wireshark integration.
GSMTAP header format (big-endian, 16 bytes). Based on libosmocore's gsmtap.h.
"""
import struct
import socket
GSMTAP_VERSION = 0x02
GSMTAP_HDR_LEN = 4 # in 32-bit words (16 bytes)
GSMTAP_TYPE_SIM = 0x07
GSMTAP_SIM_ATR = 0x01
GSMTAP_SIM_APDU = 0x02
GSMTAP_UDP_PORT = 4729
_GSMTAP_HDR_FMT = '!BBBBHBBIBBBB' # 16 bytes, big-endian
_GSMTAP_HDR_SIZE = struct.calcsize(_GSMTAP_HDR_FMT)
def build_gsmtap_packet(sub_type, data, slot_nr=0):
"""Build a GSMTAP packet as bytes.
Returns *gsmtap_hdr* + *data* as a single bytes object.
"""
hdr = struct.pack(
_GSMTAP_HDR_FMT,
GSMTAP_VERSION, # version
GSMTAP_HDR_LEN, # hdr_len (in 32-bit words)
GSMTAP_TYPE_SIM, # type
0, # timeslot
0, # arfcn
0, # noise_db
0, # signal_db
0, # frame_number
sub_type, # sub_type
0, # antenna_nr
slot_nr, # sub_slot
0, # res
)
return hdr + data
class GsmtapSender:
"""Send ATR/APDU data as GSMTAP UDP packets."""
def __init__(self, host='127.0.0.1', port=GSMTAP_UDP_PORT):
self._addr = (host, port)
self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
def send_atr(self, data, slot_nr=0):
packet = build_gsmtap_packet(GSMTAP_SIM_ATR, data, slot_nr)
self._sock.sendto(packet, self._addr)
def send_apdu(self, data, slot_nr=0):
packet = build_gsmtap_packet(GSMTAP_SIM_APDU, data, slot_nr)
self._sock.sendto(packet, self._addr)
def close(self):
self._sock.close()
+63
View File
@@ -0,0 +1,63 @@
"""PCAP file writer for Wireshark analysis.
Writes a standard PCAP file with LINKTYPE_GSMTAP (155, 0x009b).
Each packet is a GSMTAP header + raw APDU/ATR data.
"""
import struct
import time
PCAP_MAGIC = 0xa1b2c3d4
LINKTYPE_GSMTAP = 155 # 0x009b
_PCAP_GLOBAL_HDR_FMT = '<IHHiIII'
_PCAP_GLOBAL_HDR_SIZE = struct.calcsize(_PCAP_GLOBAL_HDR_FMT)
_PCAP_PKT_HDR_FMT = '<IIII'
_PCAP_PKT_HDR_SIZE = struct.calcsize(_PCAP_PKT_HDR_FMT)
class PcapWriter:
"""Write sniffed messages as GSMTAP-encapsulated PCAP packets."""
def __init__(self, path):
self._f = open(path, 'wb')
self._write_global_header()
def _write_global_header(self):
hdr = struct.pack(
_PCAP_GLOBAL_HDR_FMT,
PCAP_MAGIC, # magic_number
2, # version_major
4, # version_minor
0, # thiszone (GMT)
0, # sigfigs
65535, # snaplen
LINKTYPE_GSMTAP, # network (LINKTYPE_GSMTAP)
)
self._f.write(hdr)
def write_packet(self, gsmtap_hdr, data, ts=None):
"""Write a single GSMTAP-encapsulated packet.
*gsmtap_hdr* is the 16-byte GSMTAP header (already packed).
*data* is the raw APDU/ATR bytes.
"""
if ts is None:
ts = time.time()
packet = gsmtap_hdr + data
ts_sec = int(ts)
ts_usec = int((ts - ts_sec) * 1_000_000)
pkt_hdr = struct.pack(
_PCAP_PKT_HDR_FMT,
ts_sec, # ts_sec
ts_usec, # ts_usec
len(packet), # incl_len
len(packet), # orig_len
)
self._f.write(pkt_hdr + packet)
def close(self):
self._f.close()
+164
View File
@@ -0,0 +1,164 @@
"""Message parsing for SIMtrace2 USB sniffing protocol."""
import struct
from collections import namedtuple
MSG_HDR_FMT = '<BBBBHH'
MSG_HDR_SIZE = struct.calcsize(MSG_HDR_FMT) # 8 bytes
SIMTRACE_MSGC_SNIFF = 3
# --- Message types within SIMTRACE_MSGC_SNIFF ---
SIMTRACE_MSGT_SNIFF_CHANGE = 0
SIMTRACE_MSGT_SNIFF_FIDI = 1
SIMTRACE_MSGT_SNIFF_ATR = 2
SIMTRACE_MSGT_SNIFF_PPS = 3
SIMTRACE_MSGT_SNIFF_TPDU = 4
_TYPE_NAMES = {
SIMTRACE_MSGT_SNIFF_CHANGE: 'change',
SIMTRACE_MSGT_SNIFF_FIDI: 'fidi',
SIMTRACE_MSGT_SNIFF_ATR: 'atr',
SIMTRACE_MSGT_SNIFF_PPS: 'pps',
SIMTRACE_MSGT_SNIFF_TPDU: 'tpdu',
}
# --- Change flags ---
CHANGE_FLAG_CARD_INSERT = 1 << 0
CHANGE_FLAG_CARD_EJECT = 1 << 1
CHANGE_FLAG_RESET_ASSERT = 1 << 2
CHANGE_FLAG_RESET_DEASSERT = 1 << 3
CHANGE_FLAG_TIMEOUT_WT = 1 << 4
_CHANGE_FLAG_NAMES = {
CHANGE_FLAG_CARD_INSERT: 'card inserted',
CHANGE_FLAG_CARD_EJECT: 'card ejected',
CHANGE_FLAG_RESET_ASSERT: 'reset asserted',
CHANGE_FLAG_RESET_DEASSERT: 'reset de-asserted',
CHANGE_FLAG_TIMEOUT_WT: 'waiting time timeout',
}
# --- Data flags ---
DATA_FLAG_ERROR_INCOMPLETE = 1 << 5
DATA_FLAG_ERROR_MALFORMED = 1 << 6
DATA_FLAG_ERROR_CHECKSUM = 1 << 7
DATA_FLAG_ERROR_OVERRUN = 1 << 8
DATA_FLAG_ERROR_FRAMING = 1 << 9
DATA_FLAG_ERROR_PARITY = 1 << 10
_DATA_FLAG_NAMES = {
DATA_FLAG_ERROR_INCOMPLETE: 'incomplete',
DATA_FLAG_ERROR_MALFORMED: 'malformed',
DATA_FLAG_ERROR_CHECKSUM: 'checksum error',
DATA_FLAG_ERROR_OVERRUN: 'overrun',
DATA_FLAG_ERROR_FRAMING: 'framing error',
DATA_FLAG_ERROR_PARITY: 'parity error',
}
SniffMessage = namedtuple('SniffMessage', [
'type',
'type_hex',
'data',
'flags',
'slot_nr',
'seq_nr',
'timestamp',
])
def flags_to_names(value, mapping):
"""Convert a uint32 bitmask to a list of human-readable flag names."""
names = []
for bit, name in sorted(mapping.items()):
if value & bit:
names.append(name)
value &= ~bit
if value:
names.append(f'unknown(0x{value:08x})')
return names
def _parse_change(payload):
if len(payload) < 4:
return None
flags, = struct.unpack('<I', payload[:4])
consumed = 4
return consumed, flags, b''
_SNIFF_DATA_FMT = '<IH'
_SNIFF_DATA_HDR_SIZE = struct.calcsize(_SNIFF_DATA_FMT) # 6 bytes
def _parse_data(payload):
if len(payload) < _SNIFF_DATA_HDR_SIZE:
return None
flags, length = struct.unpack(_SNIFF_DATA_FMT, payload[:_SNIFF_DATA_HDR_SIZE])
total = _SNIFF_DATA_HDR_SIZE + length
if len(payload) < total:
return None
data = payload[_SNIFF_DATA_HDR_SIZE:total]
return total, flags, data
_PAYLOAD_PARSERS = {
SIMTRACE_MSGT_SNIFF_CHANGE: _parse_change,
SIMTRACE_MSGT_SNIFF_FIDI: _parse_change,
SIMTRACE_MSGT_SNIFF_ATR: _parse_data,
SIMTRACE_MSGT_SNIFF_PPS: _parse_data,
SIMTRACE_MSGT_SNIFF_TPDU: _parse_data,
}
def parse_message(buf, timestamp=None):
"""Try to extract one SniffMessage from the front of *buf*.
Returns ``(SniffMessage, consumed)`` on success, or ``(None, 0)`` on failure.
``consumed`` is the number of bytes to remove from the buffer.
"""
if len(buf) < MSG_HDR_SIZE:
return None, 0
msg_class, msg_type, seq_nr, slot_nr, _reserved, msg_len = \
struct.unpack(MSG_HDR_FMT, buf[:MSG_HDR_SIZE])
if msg_class != SIMTRACE_MSGC_SNIFF:
if msg_len < MSG_HDR_SIZE or msg_len > 65535:
consumed = MSG_HDR_SIZE
else:
consumed = msg_len
return None, consumed
payload_size = msg_len - MSG_HDR_SIZE
if payload_size < 0:
return None, MSG_HDR_SIZE
if len(buf) < msg_len:
return None, 0
payload = buf[MSG_HDR_SIZE:msg_len]
type_name = _TYPE_NAMES.get(msg_type, f'unknown(0x{msg_type:02x})')
data = b''
flags = 0
parser = _PAYLOAD_PARSERS.get(msg_type)
if parser is not None:
result = parser(payload)
if result is None:
return None, msg_len
consumed_payload, flags, data = result
else:
data = payload
consumed_payload = payload_size
if consumed_payload != payload_size:
return None, msg_len
return SniffMessage(
type=type_name,
type_hex=f'0x{msg_type:02x}',
data=data,
flags=flags,
slot_nr=slot_nr,
seq_nr=seq_nr,
timestamp=timestamp,
), msg_len