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
This commit is contained in:
catarrh committed 2026-08-10 23:53:38 +03:00
1 parent 73b0277ef7
commit ec129c4463
4 files changed
+63 -10

No files matched your search

+13 -2
View File
@@ -17,6 +17,8 @@ Or use the bundled startup script:
```sh ```sh
./sniff-start.sh # hex dump to stdout ./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 --gsmtap 127.0.0.1:4729 # + Wireshark GSMTAP
./sniff-start.sh --pcap trace.pcap # save PCAP for later ./sniff-start.sh --pcap trace.pcap # save PCAP for later
./sniff-start.sh --inactivity-timeout 30 # reconnect on silence ./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] 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) --gsmtap HOST[:PORT] Send ATR/TPDU as GSMTAP over UDP (default port 4729)
--pcap FILE Write sniffed data to PCAP file for Wireshark --pcap FILE Write sniffed data to PCAP file for Wireshark
--output, -o FILE Write hex dump to file --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()}") 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 - **GSMTAP**: ATR (subtype 1) and TPDU (subtype 2) over UDP to Wireshark
- **PCAP**: all messages as GSMTAP-encapsulated packets (LINKTYPE 155), openable in Wireshark - **PCAP**: all messages as GSMTAP-encapsulated packets (LINKTYPE 155), openable in Wireshark
+18 -2
View File
@@ -17,6 +17,7 @@ from . import (
format_message, format_message,
__version__, __version__,
) )
from .dump import tpdu_timestamp_prefix, tpdu_atr_time_prefix
from .gsmtap import build_gsmtap_packet, GSMTAP_SIM_ATR, GSMTAP_SIM_APDU 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') help='Write hex dump to file')
p.add_argument('--no-stdout', action='store_true', p.add_argument('--no-stdout', action='store_true',
help='Suppress hex dump to stdout') 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), p.add_argument('--vendor', type=lambda x: int(x, 16),
default=0x1d50, metavar='HEX', default=0x1d50, metavar='HEX',
help='USB vendor ID (default: 0x1d50)') help='USB vendor ID (default: 0x1d50)')
@@ -99,7 +103,7 @@ def main():
file_dumper = None file_dumper = None
if args.output: 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) print(f'File output: {args.output}', file=sys.stderr)
session = SniffSession( session = SniffSession(
@@ -130,10 +134,22 @@ def main():
signal.signal(signal.SIGINT, on_signal) signal.signal(signal.SIGINT, on_signal)
signal.signal(signal.SIGTERM, on_signal) signal.signal(signal.SIGTERM, on_signal)
atr_time_start = time.monotonic()
try: try:
for msg in session.iter_messages(): 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: if not args.no_stdout:
print(format_message(msg)) print(format_message(msg, tpdu_prefix=tpdu_prefix))
if file_dumper is not None: if file_dumper is not None:
file_dumper.write(msg) file_dumper.write(msg)
+28 -5
View File
@@ -1,6 +1,7 @@
"""Hex dump formatters for simtrace2 sniffing output.""" """Hex dump formatters for simtrace2 sniffing output."""
import sys import time
from datetime import datetime
def hexdump(data, sep=' '): def hexdump(data, sep=' '):
@@ -25,7 +26,20 @@ def _flag_str(flags, flag_names):
return ', '.join(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. """Format a SniffMessage as a human-readable line.
Returns a str suitable for stdout or file output. Returns a str suitable for stdout or file output.
@@ -70,7 +84,7 @@ def format_message(msg, *, show_flags=True):
flags_str = '' flags_str = ''
if show_flags and msg.flags: if show_flags and msg.flags:
flags_str = f' ({_flag_str(msg.flags, data_flag_names)})' 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 return line
@@ -78,11 +92,20 @@ def format_message(msg, *, show_flags=True):
class FileDumper: class FileDumper:
"""Write formatted sniff messages to a file.""" """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._f = open(path, 'a', buffering=1)
self._atr_start = time.monotonic()
def write(self, msg): 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') self._f.write(line + '\n')
def close(self): def close(self):
+4 -1
View File
@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# simtrace2-pysniff startup script # 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. # Options set via environment variables act as defaults; CLI flags override them.
@@ -13,6 +13,7 @@ PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
GSMTAP="${GSMTAP:-}" GSMTAP="${GSMTAP:-}"
PCAP="${PCAP:-}" PCAP="${PCAP:-}"
OUTPUT="${OUTPUT:-}" OUTPUT="${OUTPUT:-}"
FORMAT="${FORMAT:-}"
INACTIVITY_TIMEOUT="${INACTIVITY_TIMEOUT:-0}" INACTIVITY_TIMEOUT="${INACTIVITY_TIMEOUT:-0}"
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
@@ -20,6 +21,7 @@ while [[ $# -gt 0 ]]; do
--gsmtap) GSMTAP="$2"; shift 2 ;; --gsmtap) GSMTAP="$2"; shift 2 ;;
--pcap) PCAP="$2"; shift 2 ;; --pcap) PCAP="$2"; shift 2 ;;
--output|-o) OUTPUT="$2"; shift 2 ;; --output|-o) OUTPUT="$2"; shift 2 ;;
--format|-f) FORMAT="$2"; shift 2 ;;
--inactivity-timeout) INACTIVITY_TIMEOUT="$2"; shift 2 ;; --inactivity-timeout) INACTIVITY_TIMEOUT="$2"; shift 2 ;;
--keep-running|--no-reconnect) --keep-running|--no-reconnect)
EXTRA_ARGS+=("$1"); shift ;; EXTRA_ARGS+=("$1"); shift ;;
@@ -76,6 +78,7 @@ ARGS=()
[[ -n "$GSMTAP" ]] && ARGS+=(--gsmtap "$GSMTAP") [[ -n "$GSMTAP" ]] && ARGS+=(--gsmtap "$GSMTAP")
[[ -n "$PCAP" ]] && ARGS+=(--pcap "$PCAP") [[ -n "$PCAP" ]] && ARGS+=(--pcap "$PCAP")
[[ -n "$OUTPUT" ]] && ARGS+=(--output "$OUTPUT") [[ -n "$OUTPUT" ]] && ARGS+=(--output "$OUTPUT")
[[ -n "$FORMAT" ]] && ARGS+=(--format "$FORMAT")
[[ "$INACTIVITY_TIMEOUT" != "0" ]] && ARGS+=(--inactivity-timeout "$INACTIVITY_TIMEOUT") [[ "$INACTIVITY_TIMEOUT" != "0" ]] && ARGS+=(--inactivity-timeout "$INACTIVITY_TIMEOUT")
ARGS+=("${EXTRA_ARGS[@]}") ARGS+=("${EXTRA_ARGS[@]}")