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>
116 lines
4.2 KiB
Python
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())
|