diff --git a/CLAUDE.md b/CLAUDE.md index ba18b5a..379c47a 100644 --- a/CLAUDE.md +++ b/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` diff --git a/README.md b/README.md index ec78d9a..afd87ff 100644 --- a/README.md +++ b/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 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). diff --git a/docs/plans/2026-07-05-firmware-flasher.md b/docs/plans/2026-07-05-firmware-flasher.md new file mode 100644 index 0000000..6e9a949 --- /dev/null +++ b/docs/plans/2026-07-05-firmware-flasher.md @@ -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. diff --git a/docs/plans/README.md b/docs/plans/README.md index d786716..d52f221 100644 --- a/docs/plans/README.md +++ b/docs/plans/README.md @@ -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