Files
pm3py/CLAUDE.md
michael ab3d37e7b3 docs: sync CLAUDE.md with worktree, fix PYTHON_SIM_DESIGN line count
- Add table compiler match pattern rules to main CLAUDE.md (was only in worktree)
- Update sim framework description (750+ tests, add DNA/NTAG5 to IC list)
- Update "planned" → "in progress" for Python-driven card sim
- Fix firmware patch size: ~320 lines (was inconsistently ~340 on line 49)
2026-03-18 19:07:26 -07:00

5.3 KiB

pm3py — Development Guide

What is this?

A pure-Python async library that speaks the Proxmark3 NG wire protocol directly over USB serial. Returns structured dicts, not text. No dependency on the C client binary.

Quick reference

# Run tests (no hardware needed, all mocked)
cd /home/work/pm3py
python -m pytest tests/ -v

# Install for development
pip install -e .

Architecture

  • protocol.py — Wire constants, CRC-16/A, Cmd enum, PM3Status enum. Source of truth for command IDs and frame formats.
  • transport.py — Frame encode/decode (encode_ng_frame, encode_mix_frame, decode_response_frame), PM3Transport async serial class. USB skips CRC (uses magic 0x3361), FPC UART uses CRC with byte-swapped wire format ((low << 8) | high).
  • client.pyProxmark3 class (async-first, Proxmark3.sync() for REPL), _SyncProxy, FirmwareInfo, port auto-detection.
  • hw.py — Hardware commands. LED API is platform-aware (Easy vs RDV4 have different color-to-pin mappings). PWM validation fetches capabilities on first use.
  • lf.py — LF commands + T55xxCommands + LFSearchResult. Full tag demod (EM410x, HID, etc.) is NOT implemented — that requires the C client's DSP stack.
  • hf.py — HF core (tune, search, sniff, dropfield).
  • hf_14a.py — ISO 14443-A (scan, raw, sniff, sim). Uses MIX frames.
  • hf_15.py — ISO 15693 (scan, rdbl, wrbl, sniff). Uses NG frames with ISO command payloads.
  • hf_mf.py — MIFARE Classic (rdbl, wrbl, rdsc, chk, sniff, sim, nested, cident). Read uses NG, write uses MIX.

Wire protocol essentials

  • NG frame (client→device): magic 0x61334d50 + length|0x8000 + cmd + payload + crc/nocrc
  • MIX frame: Same but ng bit unset, payload starts with 3x uint64 args (24 bytes)
  • Response frame: magic 0x62334d50 + length|ng + status + reason + cmd + payload + crc/nocrc
  • USB: no CRC (postamble = 0x3361 cmd / 0x3362 resp). C client sets send_with_crc_on_usb = false.
  • FPC UART: CRC-16/A with byte-swapped wire encoding

Key patterns

  • All command methods are async. The sync wrapper in _SyncProxy intercepts via __getattr__ and calls loop.run_until_complete().
  • Command classes hold a self._t reference to PM3Transport (or mock in tests).
  • Tests use AsyncMock for transport. Set hw._is_rdv4 = False to skip capabilities fetch in LED tests.
  • capabilities() response parsed at known byte/bit offsets from the C struct (version=7 format).

Platform differences (PM3 Easy vs RDV4)

Color Easy RDV4
green A (0x01) B (0x02)
red B (0x02) C (0x04)
orange C (0x04) A (0x01)
blue D (0x08) (none)
red2 (none) D (0x08)

PWM-capable: Easy = A,B. RDV4 = A,D.

Sim framework (worktree feature/sim-framework)

Software-defined transponder/reader simulation framework — 750+ tests. Pure-Python models for ISO 14443-A, 15693, MIFARE Classic, DESFire, JCOP, LF (EM4100, HID, T5577), NDEF, NXP ICODE/SLIX2/DNA/NTAG5, access control (Wiegand, OSDP), implant profiles. See .worktrees/sim-framework/docs/SIM_FRAMEWORK_STATUS.md for full status.

Table compiler: proprietary command match patterns

Critical: For NXP custom commands (0xA0+), table entry match patterns must NOT include the manufacturer code byte (0x04). The firmware's UID addressing logic consumes the mfg byte as part of UID parsing, so after normalization the mfg byte is absent from the command passed to table lookup.

Addressing flow for 22 AB 04 <uid_8_bytes>:

  1. Firmware sees cmd[0] & ADDRESS → addressed mode
  2. cmd[2] (mfg code 0x04) doesn't match UID → tries cmd[3:11] → UID matches
  3. cmdCpt advances past mfg + UID → cmdCpt = 11
  4. Normalization: norm = [flags & ~ADDRESS, cmd] + cmd[cmdCpt:]02 AB (no mfg code!)
  5. Table lookup on 02 AB → match pattern must be [0x02, 0xAB] (PREFIX), NOT [0x02, 0xAB, 0x04]

For unaddressed commands (02 AB 04), mfg code stays → 02 AB 04. PREFIX match on [0x02, 0xAB] matches both forms.

Rule: All NXP custom command table entries use match=bytes([flags, cmd_byte]) with MATCH_PREFIX. Never include 0x04 in the match.

Python-driven card simulation (in progress)

Design doc: docs/PYTHON_SIM_DESIGN.md. Firmware patch (~320 lines of C) + Python extensions to enable Python-controlled card simulation on unmodified PM3 Easy/RDV4 hardware. Two mechanisms:

  • Response table in BigBuf — pre-compiled by Python, served by firmware at wire speed (86µs FDT for 14443-A Layer 3)
  • WTX relay — firmware sends S(WTX) on Layer 4 table miss, relays APDU to Python over USB for real-time crypto (DESFire, JCOP, EMV)
  • 15693 retry relay — reader retry-based relay for unknown commands

Firmware maintenance: atomic single-file commits for easy rebase against upstream PM3. See design doc for CI workflow.

Adding new commands

  1. Find the CMD_* constant in include/pm3_cmd.h and add to Cmd enum in protocol.py
  2. Check if the C client uses SendCommandNG (→ send_ng) or SendCommandMIX (→ send_mix)
  3. Check the payload struct in pm3_cmd.h and use struct.pack to build it
  4. Parse the response using struct.unpack_from on resp.data
  5. Return a dict with human-readable keys
  6. Write test with AsyncMock transport — no hardware needed