Files
pm3py/pm3py/flash_cli.py
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

116 lines
4.2 KiB
Python

"""``pm3flash`` — check the device firmware and flash the pinned fork build.
Auto-detects the Proxmark3, compares its firmware against the build this pm3py is
pinned to, and (with confirmation) flashes the matching ``fullimage.elf``. By
default it only flashes when the device does not already match the pin.
pm3flash # flash the pinned firmware if the device differs
pm3flash --image fw.elf # flash a specific image
pm3flash --force # flash even if already up to date
pm3flash --yes # no confirmation prompt
Image source (in order): --image, then $PM3PY_FIRMWARE, then a local build
(firmware/armsrc/obj/fullimage.elf). Auto-download of the pinned release is a
later layer that also feeds this same resolution.
"""
import argparse
import asyncio
import sys
from .core.client import Proxmark3
from .core.transport import PM3Error
from .core.flash import FlashError, resolve_image
from ._firmware import matches
def _print_progress(ev: dict) -> None:
pct = 100 * ev["block"] // ev["total"]
sys.stdout.write(f"\r flashing block {ev['block']}/{ev['total']} ({pct}%)")
sys.stdout.flush()
if ev["block"] == ev["total"]:
sys.stdout.write("\n")
async def _run(args) -> int:
# Connect and read the current firmware.
try:
pm3 = Proxmark3(args.port)
await pm3.connect()
except PM3Error as e:
print(f"error: {e}", file=sys.stderr)
return 2
fw = pm3.firmware
print(f"device firmware: {fw.version_string or '(unknown — bootloader mode?)'}")
print(f"pinned firmware: {fw.expected_firmware}")
if fw.matches_pin and not args.force:
print("device already runs the pinned firmware — nothing to do.")
await pm3.disconnect()
return 0
# Resolve the image up front so a missing file fails before we prompt/flash.
try:
image = resolve_image(args.image)
except FlashError as e:
print(f"error: {e}", file=sys.stderr)
await pm3.disconnect()
return 2
if not args.yes and sys.stdin.isatty():
reason = "forced" if fw.matches_pin else "firmware differs from pin"
ans = input(f"Flash {fw.expected_firmware} onto {pm3._transport.port} "
f"({reason})? [y/N] ").strip().lower()
if ans not in ("y", "yes"):
print("aborted.")
await pm3.disconnect()
return 1
try:
result = await pm3.flasher.flash(
image, allow_bootrom=args.allow_bootrom, on_progress=_print_progress)
except (PM3Error, FlashError) as e:
print(f"\nflash failed: {e}", file=sys.stderr)
return 2
print(f"wrote {result['bytes']} bytes in {result['blocks']} blocks; device reset.")
# Verify: the device rebooted into the new OS — reopen and re-read the version.
try:
await pm3._transport.reopen()
ver = await pm3.hw.version()
vstr = ver.get("version_string", "")
if matches(vstr):
print(f"verified: now running {vstr}")
rc = 0
else:
print(f"warning: post-flash firmware '{vstr}' does not match the pin",
file=sys.stderr)
rc = 3
except PM3Error as e:
print(f"note: flashed OK but could not re-read the version ({e})")
rc = 0
await pm3.disconnect()
return rc
def main(argv=None) -> int:
p = argparse.ArgumentParser(
prog="pm3flash",
description="Flash the pinned Proxmark3 fork firmware.")
p.add_argument("--port", help="serial port (default: auto-detect)")
p.add_argument("--image", help="path to fullimage.elf "
"(default: $PM3PY_FIRMWARE or local build)")
p.add_argument("--force", action="store_true",
help="flash even if the device already matches the pin")
p.add_argument("--allow-bootrom", action="store_true",
help="permit writing the bootrom region (brick risk)")
p.add_argument("-y", "--yes", action="store_true",
help="do not prompt for confirmation")
args = p.parse_args(argv)
return asyncio.run(_run(args))
if __name__ == "__main__":
sys.exit(main())