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>
This commit is contained in:
22
CLAUDE.md
22
CLAUDE.md
@@ -30,6 +30,9 @@ pm3py/
|
||||
hf_iso15.py # ISO 15693 — scan, rdbl, wrbl, thin sniff cmd
|
||||
hf_mfc.py # MIFARE Classic — rdbl, wrbl, rdsc, chk, nested, cident
|
||||
lf.py # LF commands + T55xxCommands + LFSearchResult
|
||||
flash.py # pure-Python firmware flasher (OLD-frame bootloader protocol)
|
||||
_firmware.py # firmware pin (matches_pin) — the fork build pm3py corresponds to
|
||||
flash_cli.py # `pm3flash` console-script (detect → build/resolve → flash → verify)
|
||||
trace/ # firmware trace infrastructure (shared by sniff, sim, reader)
|
||||
trace.py # parse_tracelog, TRACELOG_HDR_SIZE
|
||||
decode_iso15.py # ISO 15693 command/response decoders
|
||||
@@ -137,6 +140,25 @@ Design doc: `docs/PYTHON_SIM_DESIGN.md`. Firmware patch in `firmware/` submodule
|
||||
|
||||
Firmware maintenance: atomic single-file commits for easy rebase against upstream PM3. See design doc for CI workflow.
|
||||
|
||||
## Firmware flashing
|
||||
|
||||
pm3py flashes the fork firmware itself (`pm3.flasher`, `pm3flash` CLI) — no C `pm3-flash`.
|
||||
Key facts for maintainers:
|
||||
- **OLD frame, not NG.** The bootloader speaks the legacy 544-byte fixed frame (no
|
||||
magic/CRC); `core/transport.py` has `send_old`/`reopen`, `core/flash.py` the `Flasher`.
|
||||
- **fullimage-only by default.** The bootrom region is refused unless `allow_bootrom=True` —
|
||||
a bad OS write is recoverable (proven on hardware); a bad bootrom write bricks to JTAG.
|
||||
- **Merge segments sharing a flash page** (`build_blocks`): the SAM7 erase-programs whole
|
||||
pages, so two adjacent PT_LOAD segments landing in one page must be written once with both
|
||||
segments' data (else 0xFF padding clobbers real bytes — the on-hardware bug we hit).
|
||||
- **Platform ≠ is_rdv4.** `is_rdv4` is the running firmware's compile-time flag, not the
|
||||
board, so `--build`/`build_firmware` require an explicit `PLATFORM` (`PM3GENERIC` = Easy,
|
||||
`PM3RDV4`). Build target: `make -C firmware PLATFORM=... armsrc/all`.
|
||||
- **The pin.** `pm3py/_firmware.py` `FIRMWARE_PIN` is the fork build pm3py corresponds to
|
||||
(currently the submodule SHA); bump it when you bump the submodule. `matches()` does a
|
||||
7-char SHA-prefix test on the device version string. Auto-download of the pinned release
|
||||
is the open follow-up (see the DT-Gitea migration plan).
|
||||
|
||||
## Adding new commands
|
||||
|
||||
1. Find the `CMD_*` constant in `include/pm3_cmd.h` and add to `Cmd` enum in `core/protocol.py`
|
||||
|
||||
66
README.md
66
README.md
@@ -10,12 +10,18 @@ Pure-Python async client for the [Proxmark3](https://github.com/RfidResearchGrou
|
||||
|
||||
## Install
|
||||
|
||||
Not on PyPI yet — install from a clone. Use `--recurse-submodules` so the firmware fork
|
||||
(`firmware/`) comes with it (needed for flashing and the live-sim features):
|
||||
|
||||
```bash
|
||||
git clone --recurse-submodules <repo-url> pm3py
|
||||
cd pm3py
|
||||
pip install -e .
|
||||
pip install -e . # or `pip install .`
|
||||
```
|
||||
|
||||
Requires Python 3.10+ and `pyserial-asyncio>=0.6` (installed automatically).
|
||||
Requires **Python 3.10+**; dependencies (`pyserial-asyncio`, `bitarray`, `pycryptodome`)
|
||||
install automatically. It's pure Python + pyserial, so it also runs on a **Raspberry Pi**
|
||||
(aarch64) — see [Flashing firmware](#flashing-firmware-pm3flash) for the Pi bring-up.
|
||||
|
||||
## Quick Start
|
||||
|
||||
@@ -502,6 +508,58 @@ trace card simulation) require our firmware fork, tracked as the `firmware/` git
|
||||
submodule (`proxmark3-pm3py`, rebased onto upstream RfidResearchGroup/proxmark3).
|
||||
Those add the `CMD_HF_SNIFF_STREAM` live-frame path and the BigBuf response-table +
|
||||
WTX relay used by `SimSession`. Everything else degrades gracefully on stock firmware.
|
||||
To put the fork on your device, use [`pm3flash`](#flashing-firmware-pm3flash) (below).
|
||||
|
||||
## Flashing firmware (`pm3flash`)
|
||||
|
||||
The live features (streaming sniff, response-table/trace card sim, `hw.led`) need the
|
||||
**firmware fork** (`proxmark3-pm3py`, the `firmware/` submodule). pm3py flashes it for you
|
||||
over USB — a pure-Python flasher, no C `pm3-flash`/DFU/JTAG. Each pm3py release is pinned to
|
||||
a firmware build, and `pm3.firmware.matches_pin` tells you whether your device runs it.
|
||||
|
||||
```bash
|
||||
pm3flash # detect the device; flash the pinned firmware if it differs
|
||||
pm3flash --image fullimage.elf # flash a specific prebuilt image
|
||||
pm3flash --build PM3GENERIC # build from the submodule (Easy), then flash
|
||||
pm3flash --force # flash even if the device already matches the pin
|
||||
```
|
||||
|
||||
`pm3flash` auto-detects the port, compares the device firmware to the pin, then (fullimage
|
||||
only) flashes and verifies. It's **bootrom-safe** — it never writes the bootloader, so a bad
|
||||
flash is recoverable, not a brick.
|
||||
|
||||
Image resolution order: `--image`, then `$PM3PY_FIRMWARE`, then a local build at
|
||||
`firmware/armsrc/obj/fullimage.elf`. Build one with:
|
||||
|
||||
```bash
|
||||
make -C firmware PLATFORM=PM3GENERIC armsrc/all # PM3 Easy / generic
|
||||
make -C firmware PLATFORM=PM3RDV4 armsrc/all # RDV4
|
||||
```
|
||||
|
||||
The firmware is an ARM **cross-compile** (needs `gcc-arm-none-eabi` + `make`; the FPGA
|
||||
bitstream is committed, so no FPGA toolchain), and the `fullimage.elf` it produces is the
|
||||
same regardless of build host. **Choose `PLATFORM` yourself** — pm3py can't infer it, because
|
||||
a device's `is_rdv4` flag reports the firmware it's running, not the physical board.
|
||||
|
||||
**On a Raspberry Pi:** `pip install` pm3py on the Pi (works on aarch64), then either
|
||||
cross-build on another Linux box and copy the image over —
|
||||
|
||||
```bash
|
||||
make -C firmware PLATFORM=PM3GENERIC armsrc/all # dev box
|
||||
scp firmware/armsrc/obj/fullimage.elf pi@rpi:~/
|
||||
pm3flash --image ~/fullimage.elf # on the Pi
|
||||
```
|
||||
|
||||
— or build on the Pi itself (with `gcc-arm-none-eabi` + `make`): `pm3flash --build PM3GENERIC`.
|
||||
|
||||
**Programmatic** (the CLI wraps `pm3.flasher`):
|
||||
|
||||
```python
|
||||
pm3 = Proxmark3.sync()
|
||||
pm3.firmware.matches_pin # False -> not on the pinned build
|
||||
pm3.firmware.expected_firmware # the pinned firmware id
|
||||
pm3.flasher.flash("firmware/armsrc/obj/fullimage.elf") # enter bootloader, write, reset, verify
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -551,3 +609,7 @@ python -m pytest tests/ -v
|
||||
```
|
||||
|
||||
All tests use mock transports — **no hardware required**.
|
||||
|
||||
## License
|
||||
|
||||
MIT — see [LICENSE](LICENSE).
|
||||
|
||||
50
docs/plans/2026-07-05-firmware-flasher.md
Normal file
50
docs/plans/2026-07-05-firmware-flasher.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Pure-Python firmware flasher + `pm3flash`
|
||||
|
||||
**Status:** ✅ Landed (hardware-validated on a PM3 Easy) · **Date:** 2026-07-05
|
||||
Related: [firmware-fork DT-Gitea migration](2026-07-05-firmware-fork-dt-migration.md)
|
||||
|
||||
## Goal
|
||||
|
||||
Let pm3py get the fork firmware onto a device by itself — no C `pm3-flash`/DFU/JTAG — so
|
||||
`pip install pm3py` + one command flashes the build this pm3py corresponds to. Supports the
|
||||
project "in the meantime", before tagged firmware releases exist on the DT Gitea.
|
||||
|
||||
## What landed
|
||||
|
||||
- **`Flasher`** (`pm3py/core/flash.py`) over the legacy 544-byte **OLD frame** (bootloader
|
||||
protocol, distinct from the NG frames the rest of the client speaks; OLD-frame plumbing in
|
||||
`core/transport.py`). Automatic OS→bootloader handover (`CMD_START_FLASH`, no button),
|
||||
`CMD_FINISH_WRITE` block loop with per-block ACK/NACK, reset, reopen-and-verify.
|
||||
- **Safety:** fullimage-only by default; the bootrom region is refused unless
|
||||
`allow_bootrom=True`. A bad OS write is recoverable (proven — recovered a corrupted-OS
|
||||
device); a bad bootrom write bricks to JTAG.
|
||||
- **Firmware pin** (`pm3py/_firmware.py`): `FIRMWARE_PIN` = the fork build pm3py corresponds
|
||||
to (currently the submodule SHA). `pm3.firmware.matches_pin` / `.expected_firmware` on
|
||||
connect.
|
||||
- **`pm3flash` CLI** (`pm3py/flash_cli.py`): detect → resolve/build image → flash → verify.
|
||||
`--image` (prebuilt), `--build PLATFORM` (`make` from the submodule; platform explicit —
|
||||
never inferred from `is_rdv4`, which reports the running firmware, not the board),
|
||||
`--force`, `--allow-bootrom`.
|
||||
|
||||
## Lessons (found on hardware / by adversarial review)
|
||||
|
||||
- **Merge segments sharing a flash page.** The SAM7 erase-programs whole pages; two adjacent
|
||||
PT_LOAD segments in one page must be written once with both segments' data, else 0xFF
|
||||
padding clobbers real bytes (bug: the flashed OS booted but had invalid version info).
|
||||
- **`--build` must flash what it built,** not gate on the pin — a dirty rebuild keeps the same
|
||||
base SHA, so `matches_pin` can't distinguish it (`--build` therefore implies `--force`).
|
||||
- **No CWD-relative firmware discovery** — running `make` on a stray `./firmware` is arbitrary
|
||||
code execution; only an explicit arg / `$PM3PY_FIRMWARE_SRC` / the package repo root.
|
||||
|
||||
## Open follow-ups
|
||||
|
||||
- **Auto-download** of the pinned release asset (toolchain-free install) — needs a tagged
|
||||
firmware release to exist; rides on the DT-Gitea migration. Drops into `resolve_image`.
|
||||
- **Read-back verify** via `CMD_READ_MEM_DOWNLOAD` (v1 trusts per-block ACK/NACK, like the C
|
||||
flasher).
|
||||
|
||||
## Provenance
|
||||
|
||||
Built + hardware-validated in one session; the `--build` layer was adversarially reviewed by
|
||||
a multi-agent workflow (correctness / subprocess-safety / CLI-UX lenses with a verify pass),
|
||||
which surfaced the pin-skip, make-missing, and CWD-Makefile issues fixed above.
|
||||
@@ -49,6 +49,12 @@ approach, concrete steps, tests, risks.
|
||||
| [2026-07-03-firmware-upstream-rebase-plan](2026-07-03-firmware-upstream-rebase-plan.md) | 📎 | Rebase the `proxmark3-pm3py` fork against upstream PM3 (atomic single-file-commit workflow). |
|
||||
| [2026-07-05-firmware-fork-dt-migration](2026-07-05-firmware-fork-dt-migration.md) | 📋 | Migrate the firmware fork + pm3py to the DT Gitea; clean `master`(RRG-mirror)/`pm3py`(patches) rebase-fork model. |
|
||||
|
||||
## Firmware flashing
|
||||
|
||||
| Plan | Status | Summary |
|
||||
|------|--------|---------|
|
||||
| [2026-07-05-firmware-flasher](2026-07-05-firmware-flasher.md) | ✅ | Pure-Python flasher (OLD-frame bootloader protocol) + `pm3flash` CLI + firmware pin; hardware-validated on a PM3 Easy. Auto-download of the pinned release is the open follow-up (rides on the DT-Gitea migration). |
|
||||
|
||||
---
|
||||
|
||||
### Conventions
|
||||
|
||||
Reference in New Issue
Block a user