feat(rawcli): annotate read/write output with datasheet memory regions
A READ now names the memory locations the pages cover, per the tag's datasheet layout, grouping consecutive same-role pages: READ(0) -> 00-02 → UID / serial number · 03 → Capability Container (CC) READ(4) -> 04-07 → user memory READ(0xE3) -> E3 → CFG0 · E4 → CFG1 · E5 → PWD · E6 → PACK TagCommand gains a pages() callable (which page/block numbers a command touches: READ = 4 pages from <page>, FAST_READ = <start>..<end>, WRITE = one page). memory.region_annotation() groups those into "lo-hi → role" via the existing page_role layouts; _handle_call prints it after the exchange. No layout for the tag (e.g. MIFARE Classic) -> no annotation. Hardware-verified on NTAG216 (READ 0/4/0xE3, FAST_READ). Tests for the grouping + the dispatch path. 1264 green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -170,6 +170,17 @@ def _handle_call(state: RawSession, cmd, out=print) -> None:
|
||||
response = _raw_exchange(state.device, frame, protocol=catalog.protocol, crc=crc)
|
||||
out(render_exchange(frame, response, protocol=catalog.protocol, is_tty=True))
|
||||
|
||||
# name the datasheet memory regions the command covered (READ(4) -> "04-07 → user memory")
|
||||
if tc.pages is not None:
|
||||
from .memory import region_annotation
|
||||
try:
|
||||
covered = tc.pages(*cmd.args)
|
||||
except (TypeError, ValueError):
|
||||
covered = None
|
||||
ann = region_annotation(state.transponder, covered) if covered else None
|
||||
if ann:
|
||||
out(f" {ann}")
|
||||
|
||||
|
||||
def _handle_tlv(state: RawSession, args, out=print) -> None:
|
||||
from .tlv import format_tlv, prompt_tlv
|
||||
|
||||
@@ -18,6 +18,7 @@ class TagCommand:
|
||||
build: Callable[..., bytes] | None = None # HF: string args -> raw payload bytes to exchange
|
||||
help: str = ""
|
||||
run: Callable[..., object] | None = None # LF: (device, *args) -> a human result line
|
||||
pages: Callable[..., list[int]] | None = None # the page/block numbers the command reads/writes
|
||||
|
||||
def usage(self) -> str:
|
||||
return f"{self.name}({', '.join(self.params)})"
|
||||
@@ -48,17 +49,19 @@ def _catalog(name: str, protocol: str, *cmds: TagCommand) -> Catalog:
|
||||
return Catalog(name, protocol, {c.name.upper(): c for c in cmds})
|
||||
|
||||
|
||||
def _c(name, params, build=None, help="", run=None) -> TagCommand:
|
||||
return TagCommand(name, tuple(params), build, help, run)
|
||||
def _c(name, params, build=None, help="", run=None, pages=None) -> TagCommand:
|
||||
return TagCommand(name, tuple(params), build, help, run, pages)
|
||||
|
||||
|
||||
# ---- ISO 14443-A Type 2 (NTAG / Ultralight) ----
|
||||
TYPE2 = _catalog(
|
||||
"NTAG / Ultralight (Type 2)", "hf14a",
|
||||
_c("READ", ["page"], lambda page: bytes([0x30, _int(page)]),
|
||||
"read 4 pages starting at <page> (0x30)"),
|
||||
"read 4 pages starting at <page> (0x30)",
|
||||
pages=lambda page: list(range(_int(page), _int(page) + 4))),
|
||||
_c("FAST_READ", ["start", "end"], lambda start, end: bytes([0x3A, _int(start), _int(end)]),
|
||||
"read pages <start>..<end> in one frame (0x3A)"),
|
||||
"read pages <start>..<end> in one frame (0x3A)",
|
||||
pages=lambda start, end: list(range(_int(start), _int(end) + 1))),
|
||||
_c("GET_VERSION", [], lambda: bytes([0x60]), "product/version info (0x60)"),
|
||||
_c("READ_SIG", [], lambda: bytes([0x3C, 0x00]), "read the ECC originality signature (0x3C)"),
|
||||
_c("READ_CNT", ["counter"], lambda counter="0": bytes([0x39, _int(counter)]),
|
||||
@@ -68,10 +71,12 @@ TYPE2 = _catalog(
|
||||
"authenticate with the 4-byte password → PACK (0x1B)"),
|
||||
_c("WRITE", ["page", "data"],
|
||||
lambda page, data: bytes([0xA2, _int(page)]) + _hex(data).ljust(4, b"\x00")[:4],
|
||||
"write 4 bytes <data> to <page> (0xA2)"),
|
||||
"write 4 bytes <data> to <page> (0xA2)",
|
||||
pages=lambda page, data: [_int(page)]),
|
||||
_c("COMPAT_WRITE", ["page", "data"],
|
||||
lambda page, data: [bytes([0xA0, _int(page)]), _hex(data).ljust(16, b"\x00")[:16]],
|
||||
"compatibility write: 16-byte <data> to <page>, two-phase (0xA0)"),
|
||||
"compatibility write: 16-byte <data> to <page>, two-phase (0xA0)",
|
||||
pages=lambda page, data: [_int(page)]),
|
||||
_c("HALT", [], lambda: bytes([0x50, 0x00]), "halt the tag (0x50 00)"),
|
||||
)
|
||||
|
||||
|
||||
@@ -69,6 +69,28 @@ def page_role(transponder: str | None, page: int | None) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def region_annotation(transponder: str | None, pages: list[int]) -> str | None:
|
||||
"""Name the memory regions a read/write touched, grouping consecutive same-role pages:
|
||||
``04-07 → user memory`` or ``00-02 → UID / serial number · 03 → Capability Container (CC)``.
|
||||
Returns None when the transponder has no known layout (nothing useful to say)."""
|
||||
if not pages:
|
||||
return None
|
||||
segs: list[list] = [] # [lo, hi, role]
|
||||
for p in pages:
|
||||
role = page_role(transponder, p)
|
||||
if segs and segs[-1][2] == role and segs[-1][1] == p - 1:
|
||||
segs[-1][1] = p
|
||||
else:
|
||||
segs.append([p, p, role])
|
||||
if all(role is None for _, _, role in segs): # no layout for this tag -> nothing to add
|
||||
return None
|
||||
parts = []
|
||||
for lo, hi, role in segs:
|
||||
loc = f"{lo:02X}" if lo == hi else f"{lo:02X}-{hi:02X}"
|
||||
parts.append(f"{loc} → {role or 'reserved / unknown'}")
|
||||
return " · ".join(parts)
|
||||
|
||||
|
||||
def _to_int(text: str) -> int | None:
|
||||
try:
|
||||
return int(text, 0)
|
||||
|
||||
Reference in New Issue
Block a user