feat(rawcli): annotate raw hex/binary commands with datasheet memory regions too

Raw '30 04' is a READ(4), so it now earns the same region line as the function-call form —
the annotation follows the bytes, not the syntax:

  30 04       -> 04-07 → user memory
  3A 00 06    -> 00-02 → UID/serial · 03 → CC · 04-06 → user memory
  30 E3       -> E3 → CFG0 · E4 → CFG1 · E5 → PWD · E6 → PACK

catalog.pages_for_payload() reverse-maps a raw payload to the pages it touches by matching the
opcode (payload[0]) to a page/block command and reading its page argument from payload[1:].
dispatch's raw path prints region_annotation() after the exchange, mirroring _handle_call.
Works from either entry mode (it operates on the decoded bytes).

Along the way: TagCommand.opcode() centralises opcode extraction, tolerating data args (valid
hex placeholder) and two-phase (multi-frame) builds — fixing a latent bug where COMPAT_WRITE's
opcode came back as bytes/None. The completer now keys landmark suggestions on the first
parameter being a page/block/start address, so FAST_READ( gets the memory map too.

Hardware-verified on a real NTAG216 (raw 30 04 / 3A 00 06 / 30 E3, and the binary form
00110000 00000100). Tests for the reverse-map, the two-phase opcode, and the raw dispatch
annotation. 1274 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
michael
2026-07-15 17:03:37 -07:00
parent c53c087e84
commit 33e713549d
4 changed files with 87 additions and 11 deletions

View File

@@ -365,6 +365,23 @@ class TestCatalog:
assert {c.name for c in T5577.commands.values()} == {"READ", "WRITE", "WAKE", "DETECT"}
assert T5577.get("READ").run is not None and LF_READONLY.get("INFO").run is not None
def test_opcode_covers_two_phase_write(self):
# opcode() tolerates data args (which need valid hex) and multi-frame builds
assert TYPE2.get("WRITE").opcode() == 0xA2
assert TYPE2.get("COMPAT_WRITE").opcode() == 0xA0 # first frame's opcode, not None
assert TYPE2.get("GET_VERSION").opcode() == 0x60
assert LF_READONLY.get("INFO").opcode() is None # run-only, no build
def test_pages_for_payload(self):
# raw bytes reverse-map to the pages a page/block command touches
from pm3py.cli.rawcli.catalog import pages_for_payload
assert pages_for_payload(TYPE2, b"\x30\x04") == [4, 5, 6, 7] # READ(4)
assert pages_for_payload(TYPE2, b"\x3A\x00\x06") == [0, 1, 2, 3, 4, 5, 6] # FAST_READ(0,6)
assert pages_for_payload(TYPE2, b"\xA2\x04\x01\x02\x03\x04") == [4] # WRITE(4, ..)
assert pages_for_payload(TYPE2, b"\x60") is None # GET_VERSION — not a page command
assert pages_for_payload(TYPE2, b"\x30") is None # opcode only, no page byte
assert pages_for_payload(None, b"\x30\x04") is None # no catalog
class TestFunctionCalls:
def _id(self, sak=0x00):
@@ -407,6 +424,23 @@ class TestFunctionCalls:
o = _plain(capsys.readouterr().out)
assert "UID / serial number" in o and "Capability Container" in o
def test_raw_hex_annotates_memory_region(self, capsys):
# raw '30 04' is a READ(4) — it earns the same region annotation as the function form
s = RawSession(device=_mock_device(
scan14={"found": True, "sak": 0x00, "atqa": "4400", "uid": "04a1b2c3d4e5f6"},
version=NTAG216_VERSION))
dispatch(s, "identify")
assert s.transponder == "NTAG216"
s.device.hf.iso14a.raw.return_value = {"raw": b"\x03\x00\xFE\x00" * 4}
capsys.readouterr()
dispatch(s, "30 04") # raw READ(4)
assert "04-07 → user memory" in _plain(capsys.readouterr().out)
dispatch(s, "3A 00 06") # raw FAST_READ(0,6)
o = _plain(capsys.readouterr().out)
assert "UID / serial number" in o and "04-06 → user memory" in o
dispatch(s, "60") # GET_VERSION — no page op, no region
assert "user memory" not in _plain(capsys.readouterr().out)
def test_call_without_identify(self, capsys):
dispatch(RawSession(), "READ(4)")
assert "identify" in capsys.readouterr().out