From ec129c446346398f12cd918fa370867cecf83823 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Mon, 10 Aug 2026 23:53:38 +0300 Subject: [PATCH] Add --format option: timestamp and atr-time TPDU output modes dump.py: tpdu_timestamp_prefix(), tpdu_atr_time_prefix(), format_message() accepts tpdu_prefix parameter, FileDumper supports fmt + ATR time tracking __main__.py: --format/-f flag (hex|timestamp|atr-time), atr_time_start tracking in message loop sniff-start.sh: pass through --format/-f and FORMAT env var --- README.md | 15 +++++++++++++-- simtrace2_pysniff/__main__.py | 20 ++++++++++++++++++-- simtrace2_pysniff/dump.py | 33 ++++++++++++++++++++++++++++----- sniff-start.sh | 5 ++++- 4 files changed, 63 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 1d0137a..6ea05b4 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,8 @@ Or use the bundled startup script: ```sh ./sniff-start.sh # hex dump to stdout +./sniff-start.sh --format timestamp # TPDU lines with local time +./sniff-start.sh --format atr-time # TPDU lines relative to ATR ./sniff-start.sh --gsmtap 127.0.0.1:4729 # + Wireshark GSMTAP ./sniff-start.sh --pcap trace.pcap # save PCAP for later ./sniff-start.sh --inactivity-timeout 30 # reconnect on silence @@ -58,6 +60,10 @@ After that, `python -m simtrace2_pysniff` works identically to Linux. ``` python -m simtrace2_pysniff [OPTIONS] + --format, -f FORMAT Output format (default: hex) + hex — plain hex dump + timestamp — TPDU lines prefixed with [HH:MM:SS.mmm] + atr-time — TPDU lines prefixed with [SSSS.mmm] from last ATR --gsmtap HOST[:PORT] Send ATR/TPDU as GSMTAP over UDP (default port 4729) --pcap FILE Write sniffed data to PCAP file for Wireshark --output, -o FILE Write hex dump to file @@ -80,9 +86,14 @@ for msg in session.iter_messages(): print(f"{msg.type}: {msg.data.hex()}") ``` -## Output +## Output Formats + +- **`hex`** (default): plain hex dump — `ATR: 3b 9e ...`, `TPDU: a0 a4 00 00 02 3f 00` +- **`timestamp`**: TPDU lines prefixed with local time — `[14:32:05.123] TPDU: a0 a4 00 00 02 3f 00` +- **`atr-time`**: TPDU lines prefixed with seconds since last ATR — `[0001.234] TPDU: a0 a4 00 00 02 3f 00` + +Non-TPDU messages (ATR, PPS, card state changes, Fi/Di) print identically in all formats. -- **stdout**: `ATR: 3b 9e ...`, `TPDU: a0 a4 00 00 02 3f 00`, `Card state change: reset de-asserted` - **GSMTAP**: ATR (subtype 1) and TPDU (subtype 2) over UDP to Wireshark - **PCAP**: all messages as GSMTAP-encapsulated packets (LINKTYPE 155), openable in Wireshark diff --git a/simtrace2_pysniff/__main__.py b/simtrace2_pysniff/__main__.py index a71bc32..5235774 100644 --- a/simtrace2_pysniff/__main__.py +++ b/simtrace2_pysniff/__main__.py @@ -17,6 +17,7 @@ from . import ( format_message, __version__, ) +from .dump import tpdu_timestamp_prefix, tpdu_atr_time_prefix from .gsmtap import build_gsmtap_packet, GSMTAP_SIM_ATR, GSMTAP_SIM_APDU @@ -48,6 +49,9 @@ def build_parser(): help='Write hex dump to file') p.add_argument('--no-stdout', action='store_true', help='Suppress hex dump to stdout') + p.add_argument('--format', '-f', choices=['hex', 'timestamp', 'atr-time'], + default='hex', + help='Output format for TPDU lines (default: hex)') p.add_argument('--vendor', type=lambda x: int(x, 16), default=0x1d50, metavar='HEX', help='USB vendor ID (default: 0x1d50)') @@ -99,7 +103,7 @@ def main(): file_dumper = None if args.output: - file_dumper = FileDumper(args.output) + file_dumper = FileDumper(args.output, fmt=args.format) print(f'File output: {args.output}', file=sys.stderr) session = SniffSession( @@ -130,10 +134,22 @@ def main(): signal.signal(signal.SIGINT, on_signal) signal.signal(signal.SIGTERM, on_signal) + atr_time_start = time.monotonic() + try: for msg in session.iter_messages(): + if msg.type == 'atr': + atr_time_start = msg.timestamp + + tpdu_prefix = '' + if args.format == 'timestamp' and msg.type == 'tpdu': + tpdu_prefix = tpdu_timestamp_prefix(msg) + elif args.format == 'atr-time' and msg.type == 'tpdu': + tpdu_prefix = tpdu_atr_time_prefix( + msg.timestamp - atr_time_start) + if not args.no_stdout: - print(format_message(msg)) + print(format_message(msg, tpdu_prefix=tpdu_prefix)) if file_dumper is not None: file_dumper.write(msg) diff --git a/simtrace2_pysniff/dump.py b/simtrace2_pysniff/dump.py index d85df9f..6aa56ec 100644 --- a/simtrace2_pysniff/dump.py +++ b/simtrace2_pysniff/dump.py @@ -1,6 +1,7 @@ """Hex dump formatters for simtrace2 sniffing output.""" -import sys +import time +from datetime import datetime def hexdump(data, sep=' '): @@ -25,7 +26,20 @@ def _flag_str(flags, flag_names): return ', '.join(names) -def format_message(msg, *, show_flags=True): +def tpdu_timestamp_prefix(msg): + """Return ``[HH:MM:SS.mmm]`` for a message timestamp (local time).""" + dt = datetime.fromtimestamp(msg.timestamp) + return f"[{dt.strftime('%H:%M:%S')}.{dt.microsecond // 1000:03d}]" + + +def tpdu_atr_time_prefix(elapsed_sec): + """Return ``[SSSS.mmm]`` — zero-filled seconds + milliseconds.""" + s = int(elapsed_sec) + ms = int((elapsed_sec - s) * 1000) + return f"[{s:04d}.{ms:03d}]" + + +def format_message(msg, *, show_flags=True, tpdu_prefix=''): """Format a SniffMessage as a human-readable line. Returns a str suitable for stdout or file output. @@ -70,7 +84,7 @@ def format_message(msg, *, show_flags=True): 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)}' + line = f'{tpdu_prefix}{type_label}{flags_str}: {hexdump(msg.data)}' return line @@ -78,11 +92,20 @@ def format_message(msg, *, show_flags=True): class FileDumper: """Write formatted sniff messages to a file.""" - def __init__(self, path): + def __init__(self, path, fmt='hex'): + self._fmt = fmt self._f = open(path, 'a', buffering=1) + self._atr_start = time.monotonic() def write(self, msg): - line = format_message(msg) + tpdu_prefix = '' + if self._fmt == 'timestamp' and msg.type == 'tpdu': + tpdu_prefix = tpdu_timestamp_prefix(msg) + elif self._fmt == 'atr-time' and msg.type == 'tpdu': + tpdu_prefix = tpdu_atr_time_prefix(msg.timestamp - self._atr_start) + if msg.type == 'atr': + self._atr_start = msg.timestamp + line = format_message(msg, tpdu_prefix=tpdu_prefix) self._f.write(line + '\n') def close(self): diff --git a/sniff-start.sh b/sniff-start.sh index 9415e95..a464121 100755 --- a/sniff-start.sh +++ b/sniff-start.sh @@ -1,7 +1,7 @@ #!/bin/bash # simtrace2-pysniff startup script # -# Usage: ./sniff-start.sh [--gsmtap HOST[:PORT]] [--pcap FILE] [--output FILE] [--inactivity-timeout SECONDS] +# Usage: ./sniff-start.sh [--format FORMAT] [--gsmtap HOST[:PORT]] [--pcap FILE] [--output FILE] [--inactivity-timeout SECONDS] # # Options set via environment variables act as defaults; CLI flags override them. @@ -13,6 +13,7 @@ PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)" GSMTAP="${GSMTAP:-}" PCAP="${PCAP:-}" OUTPUT="${OUTPUT:-}" +FORMAT="${FORMAT:-}" INACTIVITY_TIMEOUT="${INACTIVITY_TIMEOUT:-0}" while [[ $# -gt 0 ]]; do @@ -20,6 +21,7 @@ while [[ $# -gt 0 ]]; do --gsmtap) GSMTAP="$2"; shift 2 ;; --pcap) PCAP="$2"; shift 2 ;; --output|-o) OUTPUT="$2"; shift 2 ;; + --format|-f) FORMAT="$2"; shift 2 ;; --inactivity-timeout) INACTIVITY_TIMEOUT="$2"; shift 2 ;; --keep-running|--no-reconnect) EXTRA_ARGS+=("$1"); shift ;; @@ -76,6 +78,7 @@ ARGS=() [[ -n "$GSMTAP" ]] && ARGS+=(--gsmtap "$GSMTAP") [[ -n "$PCAP" ]] && ARGS+=(--pcap "$PCAP") [[ -n "$OUTPUT" ]] && ARGS+=(--output "$OUTPUT") +[[ -n "$FORMAT" ]] && ARGS+=(--format "$FORMAT") [[ "$INACTIVITY_TIMEOUT" != "0" ]] && ARGS+=(--inactivity-timeout "$INACTIVITY_TIMEOUT") ARGS+=("${EXTRA_ARGS[@]}")