# 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 ```bash # 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.py` — `Proxmark3` 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. ## 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