identify_lf built its label from emitted['id_hex'], which only EM4100 provides — a
native HID/AWID/FDX-B tag (no T5577 config) would KeyError. Add credential_label() to
format each decoder's own fields: "HID Prox H10301 FC.. Card..", "AWID-26 FC.. Card..",
"FDX-B 124-000000000555 (animal)", "EM4100 <id>". Tests for native FDX-B + HID labels.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
protocols.decode_awid: FSK2a (no Manchester) via the ported detectAWID path, then
removeParity — the 88 bits after the preamble are 22 groups of 3 data + 1 odd-parity
bit, so stripping parity gives 66 bits whose first byte is the format length
(26/34/36/37/50) and the rest the facility/card fields (cmdlfawid.c index map). The 22
parity checks stand in for AWID's missing CRC, so it can't false-positive. Added to the
identify decoder chain.
Tests: AWID-26 round-trip across fc/card, parity gate rejects noise/EM4100. Full suite green.
LF stack now covers EM4100, HID, AWID, FDX-B + T55xx config. Indala (PSK) queued — the
one path needing real PSK phase-tracking (DetectPSKClock/pskRawDemod), best tuned against
a hardware capture.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- dsp.biphase_raw_decode: faithful port of lfdemod.c BiphaseRawDecode (pairwise
differential-Manchester decode with phase-fault offset nudge + error markers).
- protocols.decode_fdxb: ASK + biphase, find the 00000000001 preamble, de-interleave
the 8-bit data groups (every 9th bit a control 1), reassemble the 10-bit country +
38-bit national code. Accepted only when the CRC-16 over the eight data bytes matches
the stored one, so it can't false-positive. Both biphase polarities tried.
- protocols.crc16 / crc16_fdxb: parametric bitwise CRC-16 (Rocksoft model) matching the
firmware crc16_fast; FDX-B is poly 0x1021, init 0, refin=false, refout=true.
Added to the identify decoder chain (FDX-B is common in implants).
Tests: FDX-B round-trip (country/national/animal), CRC gate rejects noise/EM4100, and
a CRC known-answer test pinning the impl to CRC-16/XMODEM (0x31C3) and KERMIT (0x2189).
Full suite green.
Indala (PSK) + AWID queued.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New pm3py/lf/fsk.py — a function-for-function Python port of the Proxmark FSK
stack (firmware/common/lfdemod.c): fsk_wave_demod (count samples between 0->1
transitions: short wave = fc/8, long = fc/10) + aggregate_bits (runs -> clock-
resolution bits) = fskdemod, plus preamble_search, HIDdemodFSK, detectAWID hooks.
Ported to track the C reference (and thus real-tag output), not reinvented.
protocols.decode_hid: FSK2a + Manchester -> Wiegand. Format is told by frame
structure, not parity alone — the 26-bit H10301 wire frame carries a 0x20 header
at bit 37 + a sentinel 1 at bit 26 (per firmware add_HID_preamble); the 37-bit
H10304 frame is header-less (37-bit Manchester count, no 0x20). Each candidate is
still parity-validated by re-encoding through the shipped HIDProxTag, so a false
alignment can't slip through. Added to the identify decoder chain after EM4100.
Synthetic HID tests build the real add_HID_preamble framing (header+sentinel for
26-bit, bare 37-bit) and render fc/8/fc/10 square waves, so a green test means the
demod inverts what a real HID card / PM3 sim emits. Full suite 1232 green.
AWID (96-bit) framing decode + PSK/Indala + biphase/FDX-B queued next.
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>
Live-hardware fixes:
- Commands after identify returned nothing because the tag selection went
stale (and a prior no-CRC frame could drop it). Re-select the tag before
each command (silent — not traced) via _ensure_selected(). Note: this
resets auth, so multi-step PWD_AUTH needs a later keep-session mode.
- Auto-append the ISO14443-A CRC on 14a frames so a bare `60` works —
except the frames that carry no CRC (REQA/WUPA short frames, 0x9x 0x20
anticollision), via _needs_crc(). Applies to catalog commands and manual
raw alike.
- Drop the now-redundant GET_VERSION padding trim (raw() trims properly).
- help text: function-style commands are here, not "next phase".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
hf.iso14a.raw() returned the entire receive buffer the firmware replies
with (sizeof(buf)), so a tag that didn't answer came back as a run of
zeros (~40 bytes) rather than empty. The firmware puts the real received
length in oldarg[0]; trim resp.data to it. No response -> empty (raw=b"",
data=None); adds a `received` count. 2 regression tests.
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>
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>
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>
`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>
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>
`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>
- 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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
- 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>
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>
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>
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>
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>
- 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>
- 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>
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>
- 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>
- 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>
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>
.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>