Files
pm3py/pm3py/cli/rawcli/app.py
michael e024294ec6 feat(rawcli): identify + persistent connection (Phase 3)
- identify.py: probe HF (14a -> 15693) then LF; set session field /
  protocol / transponder. The 14a scan uses NO_DISCONNECT so the tag
  stays selected (the persistent connection). SAK -> coarse family name
  for the breadcrumb.
- session: add `protocol` (drives raw-exchange routing)
- app: connect via Proxmark3.sync() (scan/raw become plain calls); wire
  identify / transponder / close control commands; protocol-aware
  _raw_exchange; breadcrumb fills in field + transponder after identify

8 new tests (identify 14a/15693/none, clear, control commands). Device
probes are mocked; live identify is the user's hardware test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 12:10:51 -07:00

122 lines
4.3 KiB
Python

"""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 .parser import parse_line
from .entry import install_key_bindings
from .identify import identify, clear, format_summary
from .trace_view import render_exchange
_HELP = """\
rawcli — raw command interface
help show this help
identify probe the field; sets HF/LF + transponder, keeps the connection
transponder show the identified transponder
close drop the tag connection
quit | exit | q leave rawcli
<hex ...> send raw bytes to the tag; the annotated exchange prints above
Type hex as "30 04" or "3004"; Ctrl-/ toggles hex/binary entry (0x/0b). Function-style
transponder commands (READ(4), GET_DATA) land in the next phase."""
def _connect(port):
"""Best-effort device connection; returns the client or None (offline)."""
try:
from pm3py import Proxmark3
return Proxmark3.sync(port) # sync proxy: scan()/raw() are plain calls
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, protocol: str = "hf14a") -> bytes | None:
"""Issue one raw exchange over the identified protocol, returning the response bytes."""
if device is None:
return None
try:
if protocol == "hf14a":
resp = device.hf.iso14a.raw(payload)
elif protocol == "hf15":
raw = getattr(device.hf.iso15, "raw", None)
resp = raw(payload) if raw else None
else:
return None # lf raw exchange lands in a later phase
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 dispatch(state: RawSession, line: str) -> bool:
"""Handle one input line. Returns False to exit the REPL, True to keep going. Testable
without a live prompt."""
try:
cmd = parse_line(line, state.entry_mode)
except ValueError as exc:
print(f"rawcli: {exc}")
return True
if cmd.kind == "control":
if cmd.name in ("quit", "exit", "q"):
return False
if cmd.name == "help":
print(_HELP)
elif cmd.name == "identify":
print(format_summary(identify(state)))
elif cmd.name == "transponder":
print(state.transponder or "no transponder identified — run 'identify'")
elif cmd.name == "close":
clear(state)
print("connection closed")
return True
if cmd.kind == "call":
# function-style transponder commands land in Phase 4 (the command catalog)
print(f"rawcli: {cmd.name}(...) needs an identified transponder + catalog (Phase 4)")
return True
# raw payload
if not cmd.payload:
return True
response = _raw_exchange(state.device, cmd.payload, protocol=state.protocol)
print(render_exchange(cmd.payload, response, protocol=state.protocol,
is_tty=sys.stdout.isatty()))
return True
def run(port=None) -> int:
from prompt_toolkit import PromptSession
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.patch_stdout import patch_stdout
state = RawSession(device=_connect(port))
kb = KeyBindings()
install_key_bindings(kb, state)
session = PromptSession(key_bindings=kb)
def message():
return state.breadcrumb() + " "
with patch_stdout():
while True:
try:
line = session.prompt(message)
except (EOFError, KeyboardInterrupt):
break
if not line.strip():
continue
if not dispatch(state, line):
break
print("bye")
return 0