feat(rawcli): interactive raw-command TUI skeleton (Phase 1)

First slice of `pm3py rawcli` — the interactive, annotated raw-command
tool (separate from pyws and from the future `pm3py client` CLI).

- pm3py/cli: a top-level `pm3py` console-script dispatcher (argparse)
  with a `rawcli` subcommand; prompt_toolkit gated behind a [rawcli] extra
- rawcli/session.py: RawSession — device/field/transponder/entry-mode
  state + the segmented breadcrumb `[raw / hf|lf / $tag / 0x|0b]`
- rawcli/trace_view.py: render a reader<->tag exchange as annotated,
  Proxmark-style trace lines, reusing TraceFormatter + the 14a/15693
  decoders
- rawcli/app.py: minimal PromptSession loop (connect best-effort,
  breadcrumb prompt, help/quit, raw-hex -> annotated exchange)

Entry-mode toggle, identify/connection, the command catalog, and TLV
editing follow in later phases. 9 hardware-free tests (session, trace
render, CLI dispatch); device I/O is the user's local hardware test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
michael
2026-07-14 11:59:29 -07:00
parent 5d4af2c8e4
commit 028ce9c21e
8 changed files with 254 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
"""Render a raw reader↔tag exchange as annotated, Proxmark-style trace lines, reusing the
existing :class:`pm3py.trace.TraceFormatter` and the per-protocol decoders."""
from __future__ import annotations
from pm3py.trace import TraceFormatter, decode_14443a, decode_15693
#: protocol key -> (annotation decoder, CRC byte length)
_PROTOCOLS = {
"hf14a": (decode_14443a, 2),
"hf15": (decode_15693, 2),
"lf": (None, 0),
}
def render_exchange(command: bytes, response: bytes | None, *,
protocol: str = "hf14a", is_tty: bool | None = None) -> str:
"""Return the annotated trace for one exchange: a ``Reader → Tag`` line for ``command`` and,
if present, a ``Tag → Reader`` line for ``response``. ``is_tty`` forces/suppresses ANSI."""
decoder, crc_len = _PROTOCOLS.get(protocol, (None, 0))
fmt = TraceFormatter(mode="reader", decoder=decoder, crc_len=crc_len, is_tty=is_tty)
lines = [fmt.format(0, bytes(command)).lstrip("\n")]
if response:
lines.append(fmt.format(1, bytes(response)).lstrip("\n"))
return "\n".join(lines)