Closes the last loose end from the byte-line autocomplete fix: the entry-mode toggle
correctly flips the breadcrumb prefix (0x <-> 0b) and requests a redraw. Adds
test_toggle_updates_breadcrumb_prefix and updates the roadmap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drive the real PromptSession that run() builds (real key bindings + RawCompleter +
complete_while_typing) via a prompt_toolkit pipe input simulating TTY keystrokes, so
the actual rawcli input stack is exercised, not just the handler in isolation. Asserts
every input path yields the correct accepted line (hex byte line, binary via toggle,
mixed hex/binary, function call, command name) and that the completer is queried live
during binary entry (0 queries before the autocomplete fix, since buf.document= reset
complete_state).
prompt()'s internal asyncio.run() nulls the current-loop pointer; the helper saves and
restores it so the shared-loop `_run` convention in other test files is not poisoned.
Full suite stays green (1383).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The digit key handler applied each keystroke as `buf.document = Document(...)`.
Assigning the buffer document resets prompt_toolkit's complete_state and never
re-fires completion, so the live autocomplete menu never appeared during raw byte
entry -- in BOTH hex and binary (only command-name completion worked, since that
path already used insert_text).
type_digit only ever appends the digit (optionally after an auto-space) or drops it,
so the change is always a pure suffix. Apply it with buf.insert_text, which triggers
completion exactly like an ordinary keystroke.
Verified by driving the real key binding: the completion menu now populates on every
digit in hex and binary (before: complete_state None, start_completion never called).
Adds a regression test asserting the handler routes through insert_text; updates the
rawcli roadmap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds ISO 15693 / ICODE support in the datasheet-catalog style:
- iso15_frame() header builder assembles a request (flags | command | [NXP mfg 0x04] | [UID
LSByte-first] | params); hf.iso15.raw() sends it (CRC host-side, auto long-wait for write/lock).
- ICODE SLIX (SL2S2002) catalog: the datasheet command set — standard 15693 (READ/WRITE/LOCK
block, READ_MULTIPLE, AFI/DSFID, GET_SYSTEM_INFO, security, select/reset/stay-quiet) and NXP
customs (GET_NXP_SYSTEM_INFO, GET_RANDOM, SET/WRITE/LOCK_PASSWORD, PROTECT_PAGE, EAS set/reset/
lock/alarm/write-id, ENABLE_PRIVACY, DESTROY). catalog_for routes SLIX/ICODE names here.
- TagCommand gains an explicit opcode= : in 15693 the frame's first byte is the request FLAGS,
not the command, so completion keys on the command byte (byte 1).
- Position-aware raw completion for hf15: byte 0 = the flags menu with a live decode of each
value, byte 1 = the command, byte 2 = the 04 mfg code on custom commands (previewed with the
user). Function-call form and command-name completion unchanged.
- Per-chip identify: E0 04 -> NXP ICODE; a full 32-byte READ_SIGNATURE distinguishes SLIX2 from
SLIX. Robust UID fetch (inventory retry + a validated direct GET_SYSTEM_INFO fallback), since
vicinity anticollision misses often.
Hardware-verified on an ICODE SLIX: identify -> "ICODE SLIX" (reliably), the frames build
correctly (02 2B / 02 20 00 / 02 B2 04), and the flags/command/mfg menus + block map resolve.
(Command *responses* intermittently desync on this specific flaky PM3 link — a known device issue
across protocols, not the framing.) 1376 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the MIFARE Classic catalog with its datasheet wire commands (the standing rule: every
transponder exposes its full datasheet command set, like NTAG21x), each carrying its wire opcode
so it autocompletes by name and by the hex/binary opcode value, with the block memory map:
AUTH_A 0x60 / AUTH_B 0x61 auth a sector (test whether a key works; auth is otherwise implicit)
READ 0x30 / WRITE 0xA0 (existing)
INCREMENT 0xC1 / DECREMENT 0xC0 / RESTORE 0xC2 / TRANSFER 0xB0 value-block ops
PERSONALIZE_UID 0x40 / SET_MOD_TYPE 0x43 EV1 (opcode + hint; execution not yet wired)
CHK (key finder) / HALT 0x50 (existing)
core: hf.mf.value(block, action, value, transfer_block, key, key_type) wires CMD_HF_MIFARE_VALUE
(0x0627) — payload mirrors the stock client's CmdHF14AMfValue / firmware MifareValue (key[0:6],
action[9], transferBlk[10], operand[11:15], transfer-key[27:33], nested-auth flag[33]); the op is
committed to transfer_block, or in place when None; a cross-sector transfer sets the nested-auth
flag. INCREMENT/DECREMENT/RESTORE commit in place; TRANSFER copies a value block block->dest.
No completer/memory/app changes — the machinery is data-driven off the catalog.
Hardware-verified on a MIFARE Classic 1K: identify; c1/60/b0 (and binary) surface INCREMENT/AUTH_A
/TRANSFER with the block map; AUTH_A reports the right key ok and a wrong key failed; a value
round-trip INCREMENTed 100 -> 105 with the value-block format intact (block restored after). 1370
green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entry now tracks each raw byte's base, so hex and binary can share one line with a clean display:
`30` (hex) then Ctrl-/ then `00000100` (binary) shows `30 00000100` and sends 0x30 0x04. The
toggle only changes the base of the NEXT byte — it never rewrites what you've typed. Each byte
auto-seals at its base width (2 hex / 8 binary); the next digit starts a fresh byte in the current
mode; a digit invalid for its byte's base is dropped. Applies to raw byte entry only (function
args stay base-10).
- entry.type_digit / is_byte_line / reconcile_bases carry the logic (pure, unit-tested); the digit
and backspace key bindings maintain session.byte_bases; the toggle just flips the mode.
- parser.parse_bytes/parse_line take per-byte bases (explicit 0x/0b still wins). A line mixing a
command with raw bytes (`READ 30`) raises instead of transmitting.
- completer parses the opcode token in its own base, so `30`+binary still autocompletes the page.
- help documents all of it (per-byte base, toggle, auto-seal, base-10 args, no command/byte mix).
Also fixes a real bug found on hardware: the MFC catalog called hf.mfc (nonexistent) — it's
hf.mf. Hardware-verified on a MIFARE Classic 1K: identify, CHK(0)=FFFFFFFFFFFF, READ(0) returns
the manufacturer block, READ(4)/READ(1) the data blocks. Intermixed entry verified end-to-end in
the live prompt (30 00000100 -> 0x30 0x04). 1351 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Classic block ops are gated by a crypto1 auth per sector, so a bare 0x30 does nothing. Wire the
MFC catalog to hf.mfc (which does the auth + op in firmware), run-based like the T5577 commands:
READ(<block>[, <12-hex key>][, A|B]) -> auth + read the 16-byte block (default FFFFFFFFFFFF/A)
WRITE(<block>, <32-hex>[, key][, A|B]) -> auth + write
CHK(<block>[, A|B]) -> try a default key list, report the working key
build() still exposes the wire opcode (0x30/0xA0) so hex/binary completion and the raw-byte page
map keep working; execution goes through run(). Key type A/B accepted as letters or 0/1.
Mock-tested (rdbl/wrbl/chk call args + result lines, key override, failure path). Hardware verify
needs an actual MIFARE Classic card on the antenna (current tag is the NTAG213). 1340 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
MFC gets the same memory-map hinting as NTAG, at the block level:
block 0 manufacturer block — UID, BCC, SAK, ATQA, vendor data
sector trailer Key A [0-5], access bits [6-8] + GPB [9], Key B [10-15]
other blocks data block (sector N)
Handles 1K (16 sectors), Mini (5 sectors) and 4K — including the eight 16-block sectors 32-39
(trailer = last block, e.g. 143 = sector 32, 255 = sector 39). landmark_pages lists block 0 +
every sector trailer (the map skeleton; data blocks are uniform), so READ( / raw 30 surface it
in the completion dropdown. Also routes "MIFARE Mini" to the MFC catalog (was falling through to
Type 2).
Tests cover the block roles, large-sector trailers, bounds, Mini routing, and the dropdown map.
Path verified end-to-end with a mocked SAK-0x08 scan (identify -> catalog -> completer). Live
hardware verify still needs an actual Classic card on the antenna. 1339 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The page hinting was coarse and partly wrong: pages 0-2 were all "UID", page 3 was always
"CC" (it's OTP on Ultralight), config fields were abbreviated, and the NTAG210/212 (no counter)
and Ultralight (no ASCII mirror) differences were ignored. Now every named region is covered
and the fields track the actual IC:
00-01 UID / serial number
02 static lock bytes + internal (locks pages 03-0F)
03 Capability Container (CC) [NTAG] / OTP (one-time programmable) [Ultralight]
04..N user memory
<lock> dynamic lock bytes (present on 212/213/215/216 and MF0UL21; absent on 210/UL11)
CFG0 AUTH0, MIRROR (mode/page/byte on NTAG only), STRG_MOD_EN
CFG1 ACCESS: PROT, CFGLCK, [NFC_CNT_EN, NFC_CNT_PWD_PROT on 213/215/216], AUTHLIM
PWD 32-bit password
PACK password acknowledge + RFUI
_LAYOUTS gains a family tag ("ntag"/"ul") and a counter flag; page_role/landmark_pages derive
page 3 (CC vs OTP) and the CFG0/CFG1 field lists from them. The generic fallback (unknown model)
now also names the static-lock and CC/OTP header pages. Raw-byte completion matches the
fixed-width byte form only (so partial "2" no longer wrongly hits page 02).
Hardware-verified on NTAG213: both READ( and raw "30 " list the full 9-region map. Tests cover
the family differences (counter bits, mirror, OTP, dynamic-lock presence) and layout/model drift
for the Ultralight parts too. 1330 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The memory-location info was being surfaced on the bottom toolbar (input_hint). It belongs in
the autocomplete dropdown — the same popup where identify/READ/… appear. Moved it there and
took it off the bottom bar entirely.
- Bottom toolbar shows only the entry-mode status now; the input_hint machinery is removed.
- Raw hex/binary entry: a matched opcode inserts the RAW BYTE (30, 00110000), labelled with
the command name — not "READ(". You're building a raw byte string, so accepting a hint keeps
you in raw bytes.
- New: after a page-command opcode in raw entry, the next byte gets the tag's memory map as raw
bytes (30 04 -> "04 user memory"), matching what READ( offers. Renders in the entry base.
- Function-call page args still insert 0x-hex addresses (the chosen display).
Verified through the real interactive TUI (pty): typing "30 " pops the memory map in the
completion menu as raw bytes; the old bottom-bar hint string no longer appears anywhere. 1328
green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two corrections:
1. Memory-location names are a SUGGESTION, never command output. Removed the region annotation
printed after each exchange (both the function-call and raw-hex paths) along with its dead
machinery (region_annotation, pages_for_payload, TagCommand.pages). The location now shows in
exactly two places: the completion menu (page landmarks) and the live bottom-bar hint —
which now also covers raw byte entry (30 04 -> "READ(page) … · page 0x04 → user memory")
via Catalog.by_opcode.
2. Function-call arguments are base 10 by default. READ(04) crashed on int("04", 0) (Python
rejects leading-zero decimals in base 0). parser.parse_arg_int parses a call argument as
decimal unless it carries a 0x/0b/0o prefix, so READ(04)/READ(40) work and READ(0x28) still
overrides to hex. Raw byte entry keeps its opposite default (hex, 0b to intermix) — unchanged.
Hardware-verified on NTAG213: READ(04) sends 30 04 with no region line in the output; the hint
shows the page role for READ(4), READ(04), READ(0x28), raw 30 04 and 30 28; completion still
lists the memory map. 1331 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The page-role annotations and completion were gated on identify resolving an *exact* model
string (NTAG216). On a flaky NG link a single lost GET_VERSION dropped identify to the generic
SAK name "MIFARE Ultralight / NTAG", which isn't in the layout table — so every region
annotation and memory-map completion silently went blank. (The happy-path checks always got a
clean identify, so this never showed in earlier verification.)
Two fixes:
- _probe_version retries GET_VERSION up to 3x, re-selecting between attempts, so a lost shot on
a flaky link no longer costs the exact model (and its full memory map).
- memory._resolve_layout falls back to a generic Type 2 map (UID / CC / the always-user pages
0x04-0x0F) when only the NTAG/Ultralight family is known — so READ(0)/READ(4) still annotate
even when the exact model can't be determined (or an original Ultralight has no GET_VERSION).
Config pages differ by model and are left unnamed rather than guessed.
Tests: retry recovers the model after two lost shots; a never-answering GET_VERSION yields the
generic name yet still annotates the universal pages; generic names resolve UID/CC/user but not
model-specific pages. 1328 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
The memory-map completion for a page/block argument now follows the entry mode instead of
always emitting hex:
hex mode READ( -> 0x04 user memory
binary mode READ( -> 0b00000100 user memory (full 8 bits — each bit visible)
Matching narrows in the same base (a binary partial filters bit-prefix-wise), and a 0x/0b
prefix on the argument overrides the session mode. Binary matters here because for the config
pages each bit carries its own meaning (CFG0 AUTH0, CFG1 PROT/CFGLCK/AUTHLIM), so seeing the
whole pattern is the point.
Hardware-verified against a real NTAG216 in binary mode (0b00000000..0b11100110 with roles).
Tests for binary rendering, bit-prefix filtering, and prefix-override. 1271 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Typing the page/block argument of a READ/WRITE now pops the identified tag's memory landmarks
as completions, each labelled with its datasheet role, so you pick a location by meaning:
READ( -> 0x00 UID / serial number
0x03 Capability Container (CC)
0x04 user memory
0xE3 CFG0 — MIRROR / MIRROR_PAGE / AUTH0
0xE5 PWD (32-bit password) ...
READ(0xE -> filters to the E-page landmarks
memory.landmark_pages() derives the notable pages from the same _LAYOUTS the hint/annotation use
(NTAG21x / Ultralight EV1 landmarks; T5577 blocks 0-7). The completer matches the partial in
0x-, bare-hex, or decimal form and inserts canonical 0xNN. A comma ends the page argument, so
data args get no page suggestions; tags with no known layout (MIFARE Classic) offer nothing.
Hardware-verified: completer run against a real identified NTAG216 lists 0x00..0xE6 with roles.
Tests for the map, partial filtering, second-arg guard, T5577 blocks, and the no-layout case.
1269 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
catalog_for() returned None for LF, so after identifying a T5577/EM4100 the completer and
input_hint had nothing — no command suggestions, no hex/binary opcode hints. Now LF chips
get catalogs like the HF ones:
- T5577 catalog: READ(block) / WRITE(block, data) / WAKE(password) / DETECT(), with the
downlink opcode exposed via build() for hex+binary completion, block-role hints (0=config,
7=password, 1-6=data), and execution via a new run() path (LF isn't a raw-byte exchange —
it drives lf.t55 / the demod). READ(0)/DETECT use the reliable rotation-fixed config read;
data-block reads are best-effort and labelled as such. capture.read_t55xx_block added.
- LF read-only credentials (native EM4100/HID/AWID/FDX-B) get a minimal catalog (INFO -> re-read).
- catalog_for dispatches protocol "lf": "T5577" in the label -> T5577, else read-only.
- TagCommand gains an optional run(device, *args); completer/_opcode tolerate build=None.
Hardware-verified: identify -> catalog T5577, help lists the commands, DETECT()/READ(0) return
config 00148040 (EM4100). Tests: LF resolver, T5577 opcodes, T5577 block-role hints.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The firmware returns raw ADC envelope samples for LF reads (T55xx readbl replies
PM3_SUCCESS with NULL data); demodulation was always the C client's job, so pm3py
had none and LF identify could never see a tag. This adds the missing DSP stack in
Python — new pm3py.lf package:
- dsp.py: modulation-agnostic primitives — robust threshold, edge-interval clock
recovery, ASK binarize, half-bit resample, Manchester + biphase decode.
- protocols.py: EM4100 (ASK/Manchester -> 40-bit ID, validated by EM4100Code's own
header/row/col/stop parity checks) and best-effort T55xx block-0 config read
(find the repeating 32-bit word, score by T5577Config sanity, name the emulation).
- capture.py: live-device orchestration — read the emitted stream + probe a T55xx via
block-0 read, combine into "is it a T5577, and what is it (emulating)?".
rawcli identify now demodulates LF instead of the dead readbl-only probe: reports
"T5577 (EM4100 / EM4102)" for an emulator, "EM4100 <id>" for a bare credential.
Fully self-testing without hardware: the LF transponder models already encode these
protocols, so a synthetic envelope built from an encoder round-trips through the
demod. 17 demod tests (EM4100 across IDs/rates/mid-frame/noise, T55xx config, mocked
identify) + updated rawcli LF tests. Full suite 1225 green.
FSK/HID + PSK/Indala + biphase/FDX-B decoders queued (same dsp primitives).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- identify clears the previous field/protocol/transponder up front, so
consecutive identifies (esp. when swapping tags) never show stale data.
- LF identify: probe a T5577 by reading config block 0 (lf.t55.readbl(0)).
A valid config confirms the T5577 and, decoded via T5577Config, names
what it's emulating — "T5577 (EM4100 / EM4102)", "T5577 (HID Prox)", etc.
(exact-word map, else modulation family). Tightly gated (status ok,
non-trivial config, known modulation, sane max_block) so no-tag reads
don't false-positive; lf.search() couldn't do this without demod.
- format_summary shows the config word for LF (no UID).
4 tests (T5577 emulation report, no-T5577, stale-clear).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
With only a MIFARE Classic present, identify randomly reported ISO15693
(bogus UID) or "LF tag" — because a flaky-link 14a miss fell through to
lenient checks:
- 15693 accepted any `uid` even without `found`; now requires found AND a
real E0-prefixed UID (a 14a miss leaves non-E0 garbage).
- LF reported a tag whenever lf.search() returned its (always-truthy)
sample-capture result; it can't confirm a tag without demod, so drop it
from identify (reliable LF detection is a follow-up).
- Retry the 14a scan (up to 3x) so a transient miss doesn't fall through.
3 tests (E0-gated 15693, bogus-UID + false-LF rejection).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Color-code the prompt so the session state reads at a glance, using the
trace formatter's palette so prompt and trace look like one UI:
[ raw / hf / NTAG213 / 0x ]
dim ^bold ^cyan ^bold-green ^yellow(hex) | magenta(binary) dim
- bold `raw`; cyan RF field; bold-green identified transponder; entry
mode yellow for hex, magenta for binary (so the base is obvious and
flips color on Ctrl-/); dim brackets/separators.
- session.colored_breadcrumb() returns the ANSI form; breadcrumb() stays
plain (stripping the ANSI yields it — asserted). Prompt message renders
it via ANSI().
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- A write needs both a location and data. NTAG WRITE (0xA2) already did;
the two-phase 0xA0 writes (Type 2 COMPAT_WRITE, MIFARE Classic WRITE)
only took the page/block. They now take (page/block, data) and build a
frame *list* — the command frame plus the 16-byte data frame.
- Catalog build() may now return a list of frames; _handle_call sends each
in sequence and traces every exchange (so both phases of a write show).
- 14a catalog commands now append the ISO14443-A CRC (ISO14A_APPEND_CRC),
which real tags require — manual raw hex is still sent verbatim.
3 new/updated tests (two-phase build + send, CRC flag on reads).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
- Crash on raw hex: reader raw() returns `data` as a hex STRING; the raw
path fed that to bytes() -> "string argument without an encoding".
_raw_exchange now returns the bytes (`raw` field, or converts `data`);
render_exchange accepts bytes or a hex string.
- Completion for numeric entry, transponder-dependent: typing hex ("60")
or binary ("01100000") now matches the identified tag's command opcodes
and surfaces the named command (GET_VERSION) with its help. Honours a
0x/0b prefix and the current entry mode.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three fixes from live hardware use:
- Colors/ANSI now render. Output routes through prompt_toolkit's
print_formatted_text(ANSI(...)) instead of print(), which showed the
escape codes literally under patch_stdout. Formatters force is_tty so
the printer decides color-vs-plain. dispatch()/handlers take an `out`.
- Auto-byte-spacing no longer mangles non-hex input. byte_space() only
regroups a *pure* hex/binary run; command words ("help transponder",
"identify", "close") and 0x/0b-prefixed tokens are left intact (a-f in
words used to trigger regrouping -> failed hex parse).
- Type 2 catalog gains PWD_AUTH (0x1B), COMPAT_WRITE (0xA0), HALT — the
NTAG213 set was missing password auth.
- identify trims the firmware buffer padding off the GET_VERSION response
(was dumping ~40 trailing 00 bytes).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`identify` now probes and shows its work instead of a coarse SAK guess:
- Streams every probe exchange to the trace (proxmark-style annotated):
the reconstructed 14a activation (WUPA/ATQA/anticollision CL1+CL2/
SELECT/SAK) plus a REAL GET_VERSION (0x60 +CRC) sent over the persistent
connection.
- Names the exact IC from GET_VERSION: an NTAG216 now reports "NTAG216",
not "MIFARE UL/NTAG". Exact map mirrors the transponder models'
_version_bytes (static, to dodge an import-order circular; a test guards
against drift). Unknown chips fall back to product family + an honest
memory-size *range* (AN10833 storage-byte encoding), not a bogus size.
- SAK fallback table + GET_VERSION product-nibble map aligned to NXP
AN10833 (MIFARE type identification procedure).
11 tests (exact/structural decode, map-model sync, traced probe, SAK
names). GET_VERSION is a real device exchange — the user's hardware test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
prompt_toolkit doesn't accept "c-/". Ctrl-/ is emitted as Ctrl-_ (0x1F)
by terminals, so bind "c-_" (plus "c-t" as an always-available fallback)
for the hex/binary toggle. install_key_bindings() now ignores any key
name a prompt_toolkit build rejects, so a bad key can never crash launch.
Regression test added.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- completer.py: RawCompleter completes the leading command token —
control verbs + the identified transponder's catalog commands — with
each command's help as the completion meta (tooltip). `help <cmd>`
completes catalog command names.
- app: wire the completer with complete_while_typing (live menu) +
AutoSuggestFromHistory (inline hints) for the ipython feel.
Completes the 6-phase rawcli plan. 4 new tests (verb/catalog completion,
tooltips, help-arg completion).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- tlv.py: parse_tlv() walks a Type-2 TLV block (NULL/LOCK/MEM/NDEF/PROP/
TERMINATOR, incl. 3-byte 0xFF length); format_tlv() renders an indented
multi-line breakdown and annotates NDEF messages via the existing
decode_ndef_annotation. prompt_tlv() opens a multi-line editor to
compose a TLV as hex across lines.
- app/parser: `tlv <hex>` decodes + prints the breakdown; bare `tlv`
opens the multi-line editor.
5 new tests (structure, extended length, multi-line format, command).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- 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>
- parser.py: classify a line into control / function-call / raw payload;
parse loose hex/binary with intermixed 0x/0b tokens (whitespace-
insensitive, defaulting to the session entry mode)
- entry.py: byte_space() byte-group formatting + key bindings — Ctrl-/
toggles hex/binary entry mode (breadcrumb updates), digits auto-space
into byte groups as you type
- app.py: dispatch() drives the loop via parse_line (testable without a
live prompt); identify/call are stubbed for Phases 3/4
14 new tests (parser tokens/intermix/errors, byte_space, dispatch).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First slice of `pm3py rawcli` — the interactive, annotated raw-command
tool (separate from pyws and from the future `pm3py client` CLI).
- pm3py/cli: a top-level `pm3py` console-script dispatcher (argparse)
with a `rawcli` subcommand; prompt_toolkit gated behind a [rawcli] extra
- rawcli/session.py: RawSession — device/field/transponder/entry-mode
state + the segmented breadcrumb `[raw / hf|lf / $tag / 0x|0b]`
- rawcli/trace_view.py: render a reader<->tag exchange as annotated,
Proxmark-style trace lines, reusing TraceFormatter + the 14a/15693
decoders
- rawcli/app.py: minimal PromptSession loop (connect best-effort,
breadcrumb prompt, help/quit, raw-hex -> annotated exchange)
Entry-mode toggle, identify/connection, the command catalog, and TLV
editing follow in later phases. 9 hardware-free tests (session, trace
render, CLI dispatch); device I/O is the user's local hardware test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>