Commit Graph

163 Commits

Author SHA1 Message Date
michael
2f9e825bd4 feat(rawcli): colorized breadcrumb prompt
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>
2026-07-14 13:11:47 -07:00
michael
92b4e8771d fix(rawcli): writes take (location, data); multi-frame + CRC
- 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>
2026-07-14 13:07:50 -07:00
michael
cecfc03ba7 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>
2026-07-14 13:02:59 -07:00
michael
1370526883 fix(rawcli): raw response crash + hex/binary opcode completion
- 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>
2026-07-14 13:00:31 -07:00
michael
7703b638f3 fix(rawcli): drop extra blank line between trace lines
The trace formatter's line now carries a trailing newline; rawcli was
stripping only a leading one (.lstrip), so the trailing \n survived and
the printer added another -> a blank line between every trace line (and a
\n\n join inside render_exchange). Use .strip("\n") so it's correct
whether the formatter emits a leading or trailing newline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 12:52:33 -07:00
michael
41db552e05 fix(rawcli): render ANSI, stop mangling commands, fuller Type 2 catalog
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>
2026-07-14 12:50:01 -07:00
michael
6b4a4551d4 feat(rawcli): deep, traced identify — exact model via GET_VERSION
`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>
2026-07-14 12:40:41 -07:00
michael
5f57cc4202 build(rawcli): make prompt_toolkit a core dependency + document rawcli
`pm3py rawcli` is a headline tool, so prompt_toolkit moves from the
[rawcli] extra into core dependencies — a plain `pip install` gets it and
the command just works. The extra is removed; the missing-module guard now
points at a reinstall (it only fires on a broken env).

README: add the rawcli feature bullet, a dedicated "pm3py rawcli"
section (breadcrumb, entry toggle, identify, function-style commands, live
trace, TLV, completion), prompt_toolkit in the dependency list, and cli/
in the architecture tree.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 12:25:52 -07:00
michael
3e7f47a740 fix(rawcli): Ctrl-/ key binding crashed launch ("Invalid key: c-/")
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>
2026-07-14 12:25:36 -07:00
michael
ae3fd3d4ea fix(rawcli): friendly message when prompt_toolkit isn't installed
`pm3py rawcli` lives behind the optional [rawcli] extra; a plain install
of pm3py doesn't pull prompt_toolkit. Catch the ModuleNotFoundError and
print an install hint (pip install 'pm3py[rawcli]') with exit code 2,
instead of dumping a traceback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 12:21:20 -07:00
michael
f76663f798 feat(rawcli): ipython-style completion (Phase 6)
- 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>
2026-07-14 12:19:42 -07:00
michael
4e7181d289 feat(rawcli): TLV/NDEF formatting + multi-line editing (Phase 5)
- 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>
2026-07-14 12:15:54 -07:00
michael
9c84f9e7b0 feat(rawcli): command catalog + function-style commands (Phase 4)
- catalog.py: declarative per-transponder command sets (name, params,
  build(args)->bytes, help). Seeds: Type 2 (NTAG/UL: READ, FAST_READ,
  GET_VERSION, READ_SIG, READ_CNT, WRITE), MIFARE Classic (READ/WRITE/
  HALT), ISO 15693 (INVENTORY, READ_BLOCK, WRITE_BLOCK, GET_SYSTEM_INFO).
  catalog_for(session) resolves by protocol + identified transponder.
- app: function-style calls (READ(4) -> build -> raw exchange ->
  annotated trace); catalog-aware help (`help` lists the current tag's
  commands, `help READ` shows one). Bad args print usage.

9 new tests (catalog builds, resolver, call dispatch, help). Command
knowledge is declarative + hardware-free-testable; the exchange is the
user's device test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 12:13:14 -07:00
michael
e024294ec6 feat(rawcli): identify + persistent connection (Phase 3)
- 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>
2026-07-14 12:10:51 -07:00
michael
54e52f029f feat(rawcli): flexible entry + parser (Phase 2)
- 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>
2026-07-14 12:07:21 -07:00
michael
028ce9c21e feat(rawcli): interactive raw-command TUI skeleton (Phase 1)
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>
2026-07-14 11:59:29 -07:00
michael
5d4af2c8e4 feat(hitag): Hitag 2 software-sim transponder + reader (Tier 1)
First piece of Hitag support. Adds the Hitag2 48-bit stream cipher and a
software-sim transponder/reader over SoftwareMedium (parity with the
other LF tags).

- hitag2_crypto.py: pure-Python port of the Proxmark reference cipher
  (firmware tools/hitag2crack/hitag2_gen_nRaR.py). Validated bit-for-bit
  against the classic MIKRON golden vector (keystream 0xD7237FCE, full
  16-byte sequence). Hitag2Cipher wraps a running session (bits() for the
  authenticator, transcrypt() for link encryption).
- hitag2.py: Hitag2Tag (8 x 32-bit pages, UID/key/config/password) and
  Hitag2Reader. Faithful UID request (5-bit START_AUTH), password-mode
  auth (page-1 RWD password), and crypto-mode auth (64-bit nR||aR + aR
  verify + transcrypt). Wrong key is rejected; UID page is read-only.

Software-sim command framing between our own tag/reader; the crypto and
auth handshake are bit-faithful to real tags. 12 tests.

Hitag S / Hitag u, the reader client commands, and sniff/trace decode
follow in subsequent commits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 09:34:24 -07:00
michael
130bcd1a90 feat(registers): ST25DV RF_Ai_SS area-security register
Add St25dvRfAreaSS (Register) for the ST25DV per-area RF security
registers (RFA1SS..RFA4SS): pwd_ctrl (which RF password gates the area)
and rw_protection (read/write protection level). Bound via
tag.rf_area_ss(area) / tag.set_rf_area_ss(area, value). Modelled from
the model's own tested protection logic (single source of truth) and
cross-checked against _read_allowed / _write_allowed. 3 new tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 09:05:55 -07:00
michael
f9e56e32ef feat(registers): ICODE feature-flags decoder (GET NXP SYSTEM INFO)
Add IcodeFeatureFlags (Register) for the 16-bit ICODE feature word
reported by GET NXP SYSTEM INFO (ICODE 3 Table 113; DNA/SLIX2 report a
subset). Read-only capability decoder — decode a captured word to see
what a tag supports. Fields mirror the existing _FEATURE_* constants
one-to-one (asserted in a test). 3 new tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 09:01:52 -07:00
michael
e795b5bb1d feat(registers): NXP AES key-privilege register (ICODE DNA / NTAG5)
Add NxpKeyPrivileges (Register) for the Table 5 AES key-privilege byte
and bind it on the NxpAesAuth mixin (ICODE DNA + NTAG5 Link/Boost):

- read / write / privacy / destroy / eas_afi / crypto_config /
  area1_read / area1_write, each a documented bit
- NxpKeyPrivileges.build(read=True, write=True, ...) keyword builder
- tag.key_privileges(key_id) / tag.set_key_privileges(key_id, value)

Additive: the existing PRIV_* constants and has_privilege() path are
unchanged and see the same bytes. 5 new tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 08:58:24 -07:00
michael
f8d01a9120 feat(registers): NTAG5 STATUS_REG / CONFIG_REG self-describing views
Add Ntag5StatusReg and Ntag5ConfigReg (Register subclasses) and bind
them as tag.status_reg / tag.config_reg on the NTAG 5 platform. Purely
additive: every existing per-field accessor (disable_nfc, arbiter_mode,
sram_enable, ...) stays, and the config_reg setter re-syncs the
_arbiter_mode / _sram_enabled sibling caches the per-field setters keep,
so the sim internals are untouched.

- Ntag5StatusReg (0xA0): field/VCC/boot/lock/EEPROM/SRAM status bits
- Ntag5ConfigReg (0xA1): disable_nfc, arbiter_mode (normal/mirror/
  pass-through/PHDC), sram_enable, config_pt_transfer_dir

Bit layout taken straight from the existing hand-rolled accessors, so
the register view decodes the very same bytes (cross-checked in tests).
5 new tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 08:54:24 -07:00
michael
0f597bc933 feat(registers): NTAG I2C plus config/session/PT_I2C registers
Add self-describing registers for the NTAG I2C plus (NT3H2111/2211),
decoded bit-exact from the NT3H2111 datasheet (Tables 10-14):

- NtagI2CConfig   config registers E8h/E9h (NC_REG: pass-through, FD_ON/
                  FD_OFF, SRAM mirror, transfer dir; LAST_NDEF_BLOCK;
                  SRAM_MIRROR_BLOCK; watchdog; I2C_CLOCK_STR; REG_LOCK)
- NtagI2CSession  session registers ECh/EDh — same NC_REG copy plus the
                  NS_REG status byte and NEG_AUTH_REACHED
- NtagI2CPtI2C    PT_I2C (E7h): 2K_PROT / SRAM_PROT / I2C_PROT

Config and session share one _NtagI2CRegBlock base (byte 6 differs:
REG_LOCK vs NS_REG). Bound via tag.config / tag.session / tag.pt_i2c;
the fresh-tag decode matches the shipped defaults. 7 new tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 08:45:39 -07:00
michael
2063a33a2b feat(registers): self-describing config/frame registers
Add a BitField descriptor + Register base and per-IC config/frame
registers so opaque words decode themselves and build correctly:

- T5577Config          LF block-0 config word, mode-dependent data rate
- EM4100Code           64-bit frame + all parities (EM4102/EM4200 too)
- Type2Config          shared NXP CFG0/CFG1 base (auth0/prot/cfglck/authlim)
- Ntag21xConfig        + mirror / MIRROR_PAGE / NFC counter fields
- UltralightEV1Config  EV1 CFG0/CFG1
- UltralightCConfig    UL-C AUTH0 + AUTH1 protect scope
- MifareAccessConditions  per-block C1/C2/C3 -> English, inverted-copy
                          validation, build() that fills the inverted copy

Fields are documented attributes, so `<tab>` completes them in the shell
(bpython/ipython show each field's doc as the tooltip) and `repr` prints
the decode table. Bound to tags via `tag.config` / `tag.access(sector)`
/ `tag.code`. Bit layouts cross-checked against the firmware submodule
(cmdlft55xx, mifare4.c) and the NXP/EM datasheets. 40 new tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 08:36:40 -07:00
michael
3aabb64402 pyws plugin P2: live sim resource
- Sim facade (bound as `sim`): start(tag) arms the sim, dispatching 14a/15693 by tag
  type; stop(), push(tag) -> tag.sync(), frames -> decoded trace
- SimResource: opens SimSession on load, closes on exit; a missing device is non-fatal
- one connection mode per workspace (reader OR sim) — the second is refused (serial contention)
- sim.push added to the write effect patterns (withheld on autoreplay)

Arming stays explicit. Hardware-free tests via injected sessions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 18:50:02 -07:00
michael
042f49a9ec fix(sim): bound 14a sim abort latency + detect teardown host-side
Bump firmware pin to 3b6201d97, which bounds the 14a sim button / host-stop
latency under continuous reader traffic (was multi-second, now ~10ms).

SimSession now treats an HF_MIFARE_SIMULATE (0x0610) reply as end-of-sim for
14a: the sim starts with HF_ISO14443A_SIMULATE but the firmware tears down
with HF_MIFARE_SIMULATE, so _trace_reader and _relay_loop watch for it and
flip _active off on a button-driven stop (previously the host never noticed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 10:04:44 -07:00
michael
829f254194 Add pyws workspace plugin (P1: reader resource + namespace + effects)
Exposes pm3py as a pyws workspace, discovered via the pyws.plugins entry point:
- ReaderResource: connects/disconnects a Proxmark3 as `reader`; a missing device is
  non-fatal (reports disconnected)
- namespace injection of the transponder/sim classes, SimSession and Proxmark3
- effect declarations so autoreplay/replay withhold writes/field/sim/destructive while
  reads and pure model edits replay

Hardware-free tests via AsyncMock. The live `sim` resource is P2.
See docs/plans/2026-07-05-pm3py-pyws-plugin-plan.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 07:56:13 -07:00
michael
0ba5256d02 docs: Pi install + pm3flash firmware flashing (README, CLAUDE, roadmap)
- README: rewrite Install (from source, submodules, deps, Pi/aarch64); new
  'Flashing firmware (pm3flash)' section — CLI usage, --build, image resolution,
  cross-compile + PLATFORM note, Raspberry Pi bring-up, programmatic API; point
  Firmware Compatibility at it; add a License section (MIT).
- CLAUDE.md: flash.py/_firmware.py/flash_cli.py in the structure; a 'Firmware
  flashing' section with the maintainer gotchas (OLD frame, fullimage-only,
  page-merge, platform != is_rdv4, the pin).
- roadmap: new 2026-07-05-firmware-flasher plan doc + a Firmware-flashing row.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 22:20:15 -07:00
michael
c0ae7b90c8 feat(flash): pm3flash --build PLATFORM (build from the submodule, then flash)
build_firmware()/find_firmware_src() cross-compile fullimage.elf via
'make -C firmware PLATFORM=<explicit> armsrc/all' and pm3flash --build flashes it.
PLATFORM is explicit (never inferred from is_rdv4 — that's the running firmware's
flag, not the board).

Hardened per an adversarial multi-agent review of the change:
- --build now implies --force: it must flash the image it just built. A dirty
  rebuild keeps the same base SHA, so matches_pin can't distinguish it from the
  old firmware and would otherwise silently skip the flash (exit 0).
- make missing/not-executable -> FlashError (was an uncaught OSError traceback).
- find_firmware_src no longer falls back to a CWD-relative ./firmware — running
  make on a stray Makefile is arbitrary code execution; only explicit arg /
  $PM3PY_FIRMWARE_SRC / the package repo root. Suite 1045 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 22:20:04 -07:00
michael
9f926a176c fix(flash): merge segments sharing a flash page
Found on the first real-hardware flash (PM3 Easy). The fullimage ELF has two
adjacent PT_LOAD segments (seg1 ends at 0x15E5AC, seg2 starts there) that share
the 0x15E400 flash page. build_blocks aligned each segment independently, so the
shared page was written twice — seg2's 0xFF-padded head, written last,
erase-programmed the page and wiped seg1's real bytes there (the SAM7 programs a
whole page atomically). That region holds version_information, so the flashed OS
booted but reported 'Missing/Invalid version information'.

Fix: merge segments whose block-aligned spans touch into one buffer, so every
shared page is written once with both segments' real data; 0xFF only fills gaps
and aligned outer edges. Re-flash verified on hardware: 3df2ed2e8 -> 303bd5873,
valid version, matches_pin true. Bootrom untouched throughout (fullimage-only).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 21:59:31 -07:00
michael
b0b46b3919 feat(flash): firmware pin + matches_pin detection + pm3flash CLI
Adds the 'is the device running the right firmware, and flash it if not'
layer on top of the flasher.

- _firmware.py: FirmwarePin (pinned to the firmware/ submodule commit) and
  matches() — a device version-string test against the pinned git SHA (or a
  release tag, once tagged releases exist).
- FirmwareInfo gains expected_firmware + matches_pin, computed on connect, so
  pm3.firmware.matches_pin tells you whether the fork build is present.
- pm3flash console-script: auto-detect, compare to the pin, resolve the image
  (--image / $PM3PY_FIRMWARE / local build), confirm, flash with progress,
  then reopen and verify the new version. Flashes only on mismatch unless
  --force; fullimage-only unless --allow-bootrom.

Detection + CLI are unit-tested; the auto-download layer (fetch the pinned
release asset) and hardware validation are still to come. Suite 1034 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 21:37:46 -07:00
michael
762e305dd9 feat(flash): pure-Python firmware flasher over the OLD-frame protocol
Drives the full flash flow over the existing USB-serial link — enter
bootloader, arm window, stream the ELF block-by-block, reset — with no
external pm3-flash/DFU/JTAG.

- core/flash.py: Flasher (device_info, auto OS->bootloader handover,
  flash_image block loop with ACK/NACK+EFC status, reset, end-to-end flash);
  ELF32-ARM PT_LOAD parsing; 0xFF-padded block alignment; image resolver.
- core/transport.py: OLD 544-byte frame encode/decode, send_old /
  send_old_no_response, magic-branching frame reader, reopen() for USB
  re-enumeration.
- core/protocol.py: DEVICE_INFO flags, START_FLASH_MAGIC, flash geometry,
  BL version helpers.
- Wired as pm3.flasher; reachable through the sync proxy.

Safety: fullimage-only by default; bootrom region refused unless
allow_bootrom=True; every block bounds-checked against the flash window.
Verification is per-block ACK/NACK (as the C flasher does); read-back verify
and hardware validation are follow-ups. 22 tests; suite 1026 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 21:33:01 -07:00
michael
42a9146e6c chore(packaging): MIT license + consolidate metadata into pyproject
- Add MIT LICENSE (PEP 639 'license = "MIT"' + license-files).
- Move all metadata into a [project] table (description, readme, authors,
  keywords, classifiers); drop the now-redundant setup.py.
- Declare bitarray and pycryptodome: used by pm3py.sim and the transponder
  crypto paths but previously undeclared, so a clean install failed on the
  README's own 'from pm3py.sim import SimSession'.
- gitignore build/ and dist/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 21:32:45 -07:00
michael
6e77beea1e docs: firmware-fork DT-Gitea migration runbook + roadmap entry
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 18:29:18 -07:00
michael
14844bc700 chore(firmware): bump submodule to live-sim EML host-poll (303bd5873)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 18:29:18 -07:00
michael
fb3dc5cd68 feat(sim): load 14a tag memory into emulator RAM (mfu_dump build + tag.sync)
- NfcType2Tag._build_mfu_dump(): assemble the firmware mfu_dump_t (56-byte prefix
  version/pages/signature + page data) so the sim serves real memory (UID/CC/NDEF/
  PWD/PACK). Firmware is data-driven for tagtype 2/7, so an NTAG213 dump presents
  correctly under tagtype 7.
- NfcType2Tag.sync(): push the image via CMD_HF_MIFARE_EML_MEMSET (same primitive
  as the client's hf mfu esetblk), chunked. Updates a live sim with the fw host-poll.
- start_14a: load the dump before HF_ISO14443A_SIMULATE so the reader sees real
  page data instead of an empty buffer (works on current fw; no reflash for this).
- fix: set_ndef/clear_ndef cleared memory to the end, wiping the lock/config/PWD/
  PACK pages on NTAG parts. Bounded to the CC-declared NDEF area now.
- tests: mfu_dump layout, config-page preservation, EML_MEMSET framing/chunking.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 18:29:12 -07:00
michael
3123d4bc6a fix(submodule): repoint firmware at git.dngr.us (was dead GitHub URL)
The .gitmodules url pointed at github.com/dangerous-tac0s/proxmark3-pm3py, which
lacks our commits and the pinned gitlink — fresh recursive clones broke. Point it
at the real fork on the DT Gitea over SSH.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 17:35:02 -07:00
michael
615d4a6115 docs: roadmap index + custom-14a-command plans + backfill plan docs
- Add docs/plans/README.md as the roadmap index cataloguing all design/plan docs.
- Add the custom ISO14443-A command handling design + plan (L3 NTAG I2C
  SECTOR_SELECT / cross-sector reads + native auth; L4 static APDUs + WTX relay),
  produced from a multi-agent design workflow.
- Backfill previously-uncommitted plan docs (trace-formatter, ndef-trace-decode,
  live-sniff, firmware-upstream-rebase, 14a-live-trace) and NTAG5_SECURITY.md.
- Sync CLAUDE.md package structure with the committed transponder models.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 12:58:48 -07:00
michael
a24f0f803f feat(sim): reject wrong-protocol tags in start_14a/start_15693; export tags; uid getter
- start_14a now raises TypeError for a non-Tag14443A transponder, start_15693
  likewise for a non-Tag15693 (local imports to avoid the base<->sim circular
  import). Prevents silently starting a sim with an incompatible tag model.
- Re-export the full 14a/14b/15693 tag lineup (NTAG21x, Ultralight family, NTAG
  I2C, ST25*, OPTIGA, ISO14443B, TI parts) from pm3py.sim for `from pm3py.sim
  import NTAG213`-style use.
- Add a read-only `uid` property on the Transponder base (mirrors set_uid()).
- Tests for both guards.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 12:58:37 -07:00
michael
77ff1bb076 chore(firmware): bump submodule to 14a sim-trace fixes (3df2ed2e8)
Full-drain flush + EmLogTrace central answer capture (READ/FAST_READ/ACK/NAK now
traced). Builds clean for PM3GENERIC.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 12:58:23 -07:00
michael
9843ecdc30 chore: ignore .venv/.claude/.vscode/.mcp.json, untrack committed pyc
.gitignore had `venv/` but the project's virtualenv is `.venv/`; add it plus
local-only agent/editor state so a `git add -A` can't sweep them in. Also stop
tracking four .pyc files committed before the __pycache__ ignore rule existed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 12:57:56 -07:00
michael
dc626faa60 Big transponder support update 2026-07-05 09:53:52 -07:00
michael
950628ef4e feat(sim): ISO14443-A live sim trace consumer (Y1-Y3 + T1)
Y1: Cmd.HF_ISO14443A_SIM_TRACE (0x0339).
Y2: generalize SimSession._trace_reader dispatch to a {cmd: protocol} map so
    14a (proto 1) and 15693 (proto 0) share one reader; the ADC field-strength
    branch is now guarded to 15693.
Y3: rewrite start_14a as a synchronous trace path mirroring start_15693 (raw
    pyserial + the _trace_reader thread). Sends the correct HF_ISO14443A_SIMULATE
    NG payload (same layout as hf.iso14a.sim()), sets FLAG_SIM_TRACE when
    streaming, and takes trace/on_frame/quiet/tagtype/exit_after kwargs. Loading
    tag memory into emulator RAM stays a separate (hardware) step; the trace
    streams every reader<->tag frame regardless. The WTX _relay_loop is retained
    for the future 14a-4 path.
T1: mocked-transport unit tests for the 14a trace path (tests/test_sim_trace_14a.py).

Requires the bumped firmware submodule. 996 tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 08:36:39 -07:00
michael
64cc6794d9 chore(firmware): bump submodule to 14a sim-trace firmware (14655dbb4)
Lands F1 (CMD_HF_ISO14443A_SIM_TRACE 0x0339 + FLAG_SIM_TRACE) and F2+F3
(trace ring + hooks in SimulateIso14443aTagEx). Builds clean for PM3GENERIC.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 08:36:20 -07:00
michael
d2ac64f100 chore(firmware): bump submodule to LED-ported firmware (39c4ef319)
Adds CMD_LED_CONTROL (0x011B): payload_led_control_t + PWM/effect helpers + appmain dispatch, on top of the rebased 5530c1269.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 02:04:37 -07:00
michael
b87f12253d feat(hw): repoint Cmd.LED_CONTROL to 0x011B (firmware LED port), hw.led now works
The firmware fork now provides CMD_LED_CONTROL at 0x011B (payload_led_control_t). hw.led already packs the matching <BBBHH> struct + action codes, so repointing the id makes it functional. Adds Cmd.SET_HF_FIELD_TIMEOUT (0x011A, upstream's use of that slot). README caveat updated: hw.led works with the fork, no-op on stock.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 02:04:37 -07:00
michael
21107e8de7 docs: align sniff entries dict shape with data_hex/decoded fields
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 01:52:00 -07:00
michael
fa31c3ad1a docs: document scriptable trace access (on_frame, quiet, entries)
Adds a 'Structured / scriptable access' subsection under Live Sniff & Simulation covering the on_frame callback, quiet mode, the ANSI-free frame dict shape, and push vs pull (entries) usage for both SniffSession and SimSession.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 01:51:40 -07:00
michael
382732b881 feat(trace): scriptable on_frame callback + quiet mode + SimSession entries
SniffSession/SimSession.start_15693 gain on_frame(frame) and quiet kwargs (defaults preserve today's print behavior). Each frame is a plain dict {protocol, direction, timestamp, duration, data, data_hex, decoded[, crc_fail]} with no ANSI. SimSession now accumulates frames (entries property + clear(), mirroring SniffSession) and auto-streams when trace or on_frame is set; printing is gated on trace and not quiet. TraceFormatter.decode() exposes the annotation without color; format()/print() untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 01:50:39 -07:00
michael
3aab155004 docs: rewrite README against current API (live sniff/sim, mfu, inventory)
Full refresh of the stale 2026-03 README. Fixes sub-module names (iso14a not a14, mf not mfc), adds MIFARE Ultralight/NTAG (mfu) and iso15 inventory() with AFI/DSFID filtering, documents live SniffSession/SimSession streaming + tag.sync() live edits + the transponder/sim framework, and the sync-proxy dir()/help()/tab-completion introspection. Every code example verified against the source; hw.led carries a caveat that it needs the pending CMD_LED_CONTROL firmware port.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 01:27:10 -07:00
michael
3f42ca7b70 chore(firmware): bump submodule to rebased upstream (5530c1269)
Firmware rebased onto RfidResearchGroup/proxmark3 b8911a29a as 5 clean per-file commits (sim_table+sim_crypto modules, command IDs + iso15 fields, appmain dispatch, 15693 sim handler + streaming sniff, build glue). Replaces the old 33-commit branch based on the 2026-02-20 merge-base. Old firmware main preserved as tag pre-rebase-main-2026-07-05 (35db99fa5).
2026-07-05 00:53:17 -07:00