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>
This commit is contained in:
michael
2026-07-14 12:10:51 -07:00
parent 54e52f029f
commit e024294ec6
4 changed files with 174 additions and 11 deletions

View File

@@ -1,6 +1,8 @@
"""rawcli Phase 1 — session state, breadcrumb, trace rendering, CLI dispatch. Hardware-free."""
import re
from unittest.mock import MagicMock
import pytest
from pm3py.cli.main import build_parser, main
@@ -8,6 +10,7 @@ from pm3py.cli.rawcli.session import RawSession
from pm3py.cli.rawcli.trace_view import render_exchange
from pm3py.cli.rawcli.parser import parse_token, parse_bytes, parse_line
from pm3py.cli.rawcli.entry import byte_space
from pm3py.cli.rawcli.identify import identify, clear, name_14a
from pm3py.cli.rawcli.app import dispatch
_ANSI = re.compile(r"\x1b\[[0-9;]*m")
@@ -138,3 +141,62 @@ class TestDispatch:
def test_bad_hex_reports(self, capsys):
assert dispatch(RawSession(), "0b0011") is True
assert "whole number of bytes" in capsys.readouterr().out
def _mock_device(scan14=None, scan15=None):
dev = MagicMock()
dev.hf.iso14a.scan.return_value = scan14 or {"found": False}
dev.hf.iso15.scan.return_value = scan15 or {"found": False}
dev.lf.search.return_value = None
return dev
class TestIdentify:
def test_no_device(self):
s = RawSession()
r = identify(s)
assert r["found"] is False and s.field is None
def test_14a_sets_field_and_name(self):
s = RawSession(device=_mock_device(
scan14={"found": True, "sak": 0x08, "uid": b"\x01\x02\x03\x04"}))
r = identify(s)
assert r["found"] and s.field == "hf" and s.protocol == "hf14a"
assert s.transponder == "MIFARE Classic 1K"
assert r["uid"] == b"\x01\x02\x03\x04"
def test_15693_fallback(self):
s = RawSession(device=_mock_device(scan15={"found": True, "uid": b"\xE0\x04"}))
r = identify(s)
assert r["found"] and s.field == "hf" and s.protocol == "hf15"
assert s.transponder == "ISO15693"
def test_name_unknown_sak(self):
assert name_14a({"sak": 0x99}) == "ISO14443-A (SAK 99)"
def test_clear_forgets_tag(self):
s = RawSession(device=_mock_device(scan14={"found": True, "sak": 0x08}))
identify(s)
clear(s)
assert s.field is None and s.transponder is None and s.protocol == "hf14a"
class TestControlCommands:
def test_identify_command(self, capsys):
s = RawSession(device=_mock_device(
scan14={"found": True, "sak": 0x00, "uid": b"\x04\x11\x22\x33"}))
dispatch(s, "identify")
out = capsys.readouterr().out
assert "MIFARE UL/NTAG" in out and "04112233" in out
assert s.breadcrumb() == "[raw / hf / MIFARE UL/NTAG / 0x]"
def test_transponder_when_none(self, capsys):
dispatch(RawSession(), "transponder")
assert "run 'identify'" in capsys.readouterr().out
def test_close(self, capsys):
s = RawSession(device=_mock_device(scan14={"found": True, "sak": 0x08}))
dispatch(s, "identify")
dispatch(s, "close")
assert "closed" in capsys.readouterr().out
assert s.transponder is None