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:
80
pm3py/cli/rawcli/app.py
Normal file
80
pm3py/cli/rawcli/app.py
Normal file
@@ -0,0 +1,80 @@
|
||||
"""rawcli interactive loop.
|
||||
|
||||
Phase 1 skeleton: connect (best-effort), a live breadcrumb prompt, ``help``/``quit``, and a raw
|
||||
hex payload → annotated exchange. Entry-mode toggling, identify/connection, the command catalog,
|
||||
and TLV editing arrive in later phases (see the plan). The interactive loop is intentionally thin;
|
||||
the testable logic lives in :mod:`session` / :mod:`trace_view` / (later) ``parser``.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
from .session import RawSession
|
||||
from .trace_view import render_exchange
|
||||
|
||||
_HELP = """\
|
||||
rawcli — raw command interface
|
||||
|
||||
help show this help
|
||||
quit | exit | q leave rawcli
|
||||
<hex ...> send raw bytes to the tag; the annotated exchange prints above
|
||||
|
||||
Type hex as "30 04" or "3004". Entry-mode toggle (0x/0b), identify, and function-style
|
||||
transponder commands (READ(4), GET_DATA) land in later phases."""
|
||||
|
||||
|
||||
def _connect(port):
|
||||
"""Best-effort device connection; returns the client or None (offline)."""
|
||||
try:
|
||||
from pm3py import Proxmark3
|
||||
return Proxmark3(port)
|
||||
except Exception as exc: # no device / connect failure — run offline
|
||||
print(f"rawcli: no device ({type(exc).__name__}: {exc}); running offline", file=sys.stderr)
|
||||
return None
|
||||
|
||||
|
||||
def _raw_exchange(device, payload: bytes) -> bytes | None:
|
||||
"""Issue one raw 14a exchange, returning the response bytes (or None)."""
|
||||
if device is None:
|
||||
return None
|
||||
try:
|
||||
resp = device.hf.iso14a.raw(payload)
|
||||
return resp.get("data") if isinstance(resp, dict) else None
|
||||
except Exception as exc:
|
||||
print(f"rawcli: exchange failed ({type(exc).__name__}: {exc})", file=sys.stderr)
|
||||
return None
|
||||
|
||||
|
||||
def run(port=None) -> int:
|
||||
from prompt_toolkit import PromptSession
|
||||
from prompt_toolkit.patch_stdout import patch_stdout
|
||||
|
||||
state = RawSession(device=_connect(port))
|
||||
session = PromptSession()
|
||||
|
||||
def message():
|
||||
return state.breadcrumb() + " "
|
||||
|
||||
with patch_stdout():
|
||||
while True:
|
||||
try:
|
||||
line = session.prompt(message).strip()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
break
|
||||
if not line:
|
||||
continue
|
||||
if line in ("quit", "exit", "q"):
|
||||
break
|
||||
if line == "help":
|
||||
print(_HELP)
|
||||
continue
|
||||
try:
|
||||
payload = bytes.fromhex(line.lower().replace("0x", "").replace(" ", ""))
|
||||
except ValueError:
|
||||
print(f"rawcli: could not parse {line!r} as hex")
|
||||
continue
|
||||
response = _raw_exchange(state.device, payload)
|
||||
print(render_exchange(payload, response, protocol="hf14a",
|
||||
is_tty=sys.stdout.isatty()))
|
||||
print("bye")
|
||||
return 0
|
||||
Reference in New Issue
Block a user