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>