feat(rawcli): live relevance hint — per-transponder memory map
Surfaces what the input *means* on the identified tag, in a bottom-bar hint that updates as you type (the point, over aggressive autocomplete): - memory.py: page_role(transponder, page) maps a page/block to its role on the specific IC — UID, Capability Container, user memory, dynamic lock, CFG0/AUTH0, CFG1/ACCESS, PWD, PACK. Layouts mirror the models' page attributes (NTAG210/212/213/215/216, Ultralight EV1); a test guards against drift. - input_hint(session, text): the command's purpose, and — once a page argument is typed (even partial, hex or decimal) — where that page lives. e.g. "READ(4)" -> user memory, "READ(0x2B" -> PWD, "WRITE(0x29" -> CFG0/AUTH0. - app: wired as the prompt's bottom_toolbar. 11 new tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -158,12 +158,25 @@ def run(port=None) -> int:
|
||||
|
||||
from .completer import RawCompleter
|
||||
|
||||
from prompt_toolkit.application import get_app
|
||||
from .memory import input_hint
|
||||
|
||||
def out(text):
|
||||
# parse the ANSI our formatters emit and render it through prompt_toolkit (renders
|
||||
# color on a terminal, strips it when piped) — plain print() shows the codes literally
|
||||
print_formatted_text(ANSI(str(text)))
|
||||
|
||||
state = RawSession(device=_connect(port))
|
||||
|
||||
def bottom_toolbar():
|
||||
# live relevance for the current input: what the command does, and where a page lives
|
||||
try:
|
||||
text = get_app().current_buffer.text
|
||||
except Exception:
|
||||
text = ""
|
||||
hint = input_hint(state, text)
|
||||
return hint if hint else f"entry {state.entry_prefix} · Ctrl-/ hex/binary · 'help'"
|
||||
|
||||
kb = KeyBindings()
|
||||
install_key_bindings(kb, state)
|
||||
session = PromptSession(
|
||||
@@ -171,6 +184,7 @@ def run(port=None) -> int:
|
||||
completer=RawCompleter(state),
|
||||
complete_while_typing=True, # ipython-style live menu
|
||||
auto_suggest=AutoSuggestFromHistory(), # inline suggestion hints
|
||||
bottom_toolbar=bottom_toolbar, # live relevance hint
|
||||
)
|
||||
|
||||
def message():
|
||||
|
||||
90
pm3py/cli/rawcli/memory.py
Normal file
90
pm3py/cli/rawcli/memory.py
Normal file
@@ -0,0 +1,90 @@
|
||||
"""Per-transponder memory maps + the live relevance hint.
|
||||
|
||||
The point of rawcli isn't just to send bytes — it's to tell you what they *mean* on the tag in
|
||||
front of you. ``page_role`` maps a page/block number to its role on the identified transponder
|
||||
(user memory, CC, AUTH0, PWD, lock bytes, …), and ``input_hint`` turns the current input line into
|
||||
a one-line relevance hint for the prompt's bottom bar: the command's purpose, and — when you've
|
||||
typed a page argument — where that page lives.
|
||||
|
||||
Layouts mirror the transponder models' page attributes (``_cfg0_page`` etc.); kept static to
|
||||
avoid an import-order circular, with a test guarding against drift.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
# Page layout per NTAG21x / Ultralight EV1 IC (user range inclusive; keys are identify's names).
|
||||
_LAYOUTS = {
|
||||
"NTAG210": {"user": (0x04, 0x0F), "cfg0": 0x10, "cfg1": 0x11, "pwd": 0x12, "pack": 0x13},
|
||||
"NTAG212": {"user": (0x04, 0x23), "lock": 0x24, "cfg0": 0x25, "cfg1": 0x26, "pwd": 0x27, "pack": 0x28},
|
||||
"NTAG213": {"user": (0x04, 0x27), "lock": 0x28, "cfg0": 0x29, "cfg1": 0x2A, "pwd": 0x2B, "pack": 0x2C},
|
||||
"NTAG215": {"user": (0x04, 0x81), "lock": 0x82, "cfg0": 0x83, "cfg1": 0x84, "pwd": 0x85, "pack": 0x86},
|
||||
"NTAG216": {"user": (0x04, 0xE1), "lock": 0xE2, "cfg0": 0xE3, "cfg1": 0xE4, "pwd": 0xE5, "pack": 0xE6},
|
||||
"MIFARE Ultralight EV1 (MF0UL11)": {"user": (0x04, 0x0F), "cfg0": 0x10, "cfg1": 0x11, "pwd": 0x12, "pack": 0x13},
|
||||
"MIFARE Ultralight EV1 (MF0UL21)": {"user": (0x04, 0x23), "lock": 0x24, "cfg0": 0x25, "cfg1": 0x26, "pwd": 0x27, "pack": 0x28},
|
||||
}
|
||||
|
||||
_ROLES = {
|
||||
"cfg0": "CFG0 — MIRROR / MIRROR_PAGE / AUTH0",
|
||||
"cfg1": "CFG1 — ACCESS (PROT / CFGLCK / AUTHLIM)",
|
||||
"pwd": "PWD (32-bit password)",
|
||||
"pack": "PACK (password acknowledge)",
|
||||
"lock": "dynamic lock bytes",
|
||||
}
|
||||
|
||||
_CALL_HEAD = re.compile(r"^\s*([A-Za-z_][A-Za-z0-9_]*)\s*\(\s*([^,)]*)")
|
||||
|
||||
|
||||
def page_role(transponder: str | None, page: int | None) -> str | None:
|
||||
"""The role of ``page`` on ``transponder`` (user memory / CC / AUTH0 / PWD / …), or None."""
|
||||
layout = _LAYOUTS.get(transponder or "")
|
||||
if layout is None or page is None:
|
||||
return None
|
||||
if 0 <= page <= 2:
|
||||
return "UID / serial number"
|
||||
if page == 3:
|
||||
return "Capability Container (CC)"
|
||||
lo, hi = layout["user"]
|
||||
if lo <= page <= hi:
|
||||
return "user memory"
|
||||
for key, role in _ROLES.items():
|
||||
if page == layout.get(key):
|
||||
return role
|
||||
return None
|
||||
|
||||
|
||||
def _to_int(text: str) -> int | None:
|
||||
try:
|
||||
return int(text, 0)
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
def input_hint(session, text: str) -> str | None:
|
||||
"""A one-line relevance hint for the current input: the command's purpose, and — for a page/
|
||||
block argument — where that page lives on the identified transponder."""
|
||||
from .catalog import catalog_for
|
||||
|
||||
stripped = (text or "").strip()
|
||||
if not stripped:
|
||||
return None
|
||||
catalog = catalog_for(session)
|
||||
if catalog is None:
|
||||
return None
|
||||
|
||||
m = _CALL_HEAD.match(stripped)
|
||||
if m:
|
||||
tc = catalog.get(m.group(1))
|
||||
first = m.group(2).strip()
|
||||
if tc:
|
||||
base = f"{tc.usage()} — {tc.help}"
|
||||
if tc.params and tc.params[0] in ("page", "block") and first:
|
||||
role = page_role(session.transponder, _to_int(first))
|
||||
if role:
|
||||
return f"{base} · {tc.params[0]} {first} → {role}"
|
||||
return base
|
||||
|
||||
tc = catalog.get(stripped)
|
||||
if tc:
|
||||
return f"{tc.usage()} — {tc.help}"
|
||||
return None
|
||||
Reference in New Issue
Block a user