feat(trace): scriptable on_frame callback + quiet mode + SimSession entries

SniffSession/SimSession.start_15693 gain on_frame(frame) and quiet kwargs (defaults preserve today's print behavior). Each frame is a plain dict {protocol, direction, timestamp, duration, data, data_hex, decoded[, crc_fail]} with no ANSI. SimSession now accumulates frames (entries property + clear(), mirroring SniffSession) and auto-streams when trace or on_frame is set; printing is gated on trace and not quiet. TraceFormatter.decode() exposes the annotation without color; format()/print() untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
michael
2026-07-05 01:50:39 -07:00
parent 3aab155004
commit 382732b881
5 changed files with 182 additions and 8 deletions

View File

@@ -0,0 +1,104 @@
"""Scriptable trace access: on_frame callback, quiet mode, entries accumulator."""
import struct
from unittest.mock import MagicMock
from pm3py.trace.format import TraceFormatter
from pm3py.trace import decode_15693
INV = bytes.fromhex("260100f60a") # ISO15693 INVENTORY request + CRC
def _fmt(mode="sniff"):
return TraceFormatter(mode=mode, decoder=decode_15693, crc_len=2, is_tty=False)
def _sniff_trace(direction, payload):
# CMD_HF_SNIFF_STREAM entry: {protocol,direction,duration,timestamp} + payload
return struct.pack("<BBHI", 0, direction, 0, 0) + payload
def _sim_trace_frame(direction, payload):
from pm3py.core.transport import RESP_POSTAMBLE_NOCRC
from pm3py.core.protocol import RESP_PREAMBLE_MAGIC
from pm3py.sim.sim_session import CMD_HF_ISO15693_SIM_TRACE
data = bytes([direction]) + payload
header = struct.pack("<IHbbH", RESP_PREAMBLE_MAGIC, len(data) | 0x8000,
0, 0, CMD_HF_ISO15693_SIM_TRACE)
return header + data + struct.pack("<H", RESP_POSTAMBLE_NOCRC)
def _run_sim_reader(session, frame):
ser = MagicMock()
ser.in_waiting = len(frame)
n = 0
def side(_):
nonlocal n
n += 1
if n == 1:
return frame
session._active = False
return b""
ser.read.side_effect = side
session._trace_reader(ser)
# ---------------- SniffSession ----------------
def test_sniff_on_frame_and_quiet(capsys):
from pm3py.sniff.session import SniffSession
s = SniffSession(MagicMock())
s._formatter = _fmt()
frames = []
s._on_frame = frames.append
s._quiet = True
s._handle_trace(_sniff_trace(0, INV))
assert len(frames) == 1 and len(s.entries) == 1
f = frames[0]
assert f["direction"] == 0
assert f["data"] == INV and f["data_hex"] == INV.hex()
assert f["decoded"] == "INVENTORY"
assert capsys.readouterr().out == "" # quiet -> no console output
def test_sniff_default_still_prints(capsys):
from pm3py.sniff.session import SniffSession
s = SniffSession(MagicMock())
s._formatter = _fmt()
# defaults preserved: _on_frame=None, _quiet=False
s._handle_trace(_sniff_trace(0, INV))
assert len(s.entries) == 1
assert capsys.readouterr().out != "" # prints as before
# ---------------- SimSession ----------------
def test_sim_on_frame_and_quiet(capsys):
from pm3py.sim.sim_session import SimSession
s = SimSession()
s._formatter = _fmt(mode="sim")
s._active = True
frames = []
s._on_frame = frames.append
s._print_frames = False # quiet
_run_sim_reader(s, _sim_trace_frame(0, INV))
assert len(frames) == 1 and len(s.entries) == 1
f = frames[0]
assert f["direction"] == 0 and f["data"] == INV
assert f["data_hex"] == INV.hex()
assert f["decoded"] == "INVENTORY" and f["crc_fail"] is False
assert capsys.readouterr().out == ""
def test_sim_entries_accumulate_and_clear():
from pm3py.sim.sim_session import SimSession
s = SimSession()
s._formatter = _fmt(mode="sim")
s._active = True
_run_sim_reader(s, _sim_trace_frame(0, INV))
assert len(s.entries) == 1
s.clear()
assert s.entries == []