Files
pm3py/README.md
michael b87f12253d feat(hw): repoint Cmd.LED_CONTROL to 0x011B (firmware LED port), hw.led now works
The firmware fork now provides CMD_LED_CONTROL at 0x011B (payload_led_control_t). hw.led already packs the matching <BBBHH> struct + action codes, so repointing the id makes it functional. Adds Cmd.SET_HF_FIELD_TIMEOUT (0x011A, upstream's use of that slot). README caveat updated: hw.led works with the fork, no-op on stock.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 02:04:37 -07:00

554 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# pm3py
Pure-Python async client for the [Proxmark3](https://github.com/RfidResearchGroup/proxmark3), speaking its NG wire protocol directly over USB serial. Structured Python dicts instead of parsed terminal text, with a sync proxy for REPL use.
- **Pure Python** — no dependency on the C client binary; speaks the wire protocol directly over USB serial.
- **Structured results** — every command returns a Python dict, not scraped terminal output.
- **Async-first, REPL-friendly** — `async`/`await` core with a synchronous proxy (`Proxmark3.sync()`) that supports `dir()`, `help()`, and tab-completion.
- **Live sniff & simulation** — stream decoded reader↔tag frames into your REPL in real time; edit an emulated tag and push it live.
- **Software-defined tags** — pure-Python transponder models (ISO 14443-A, ISO 15693, MIFARE Classic, NTAG/Type-2, NXP ICODE/SLIX2/NTAG5, LF) served from a firmware response table.
## Install
```bash
cd pm3py
pip install -e .
```
Requires Python 3.10+ and `pyserial-asyncio>=0.6` (installed automatically).
## Quick Start
pm3py is async-first, but the sync REPL proxy is the easiest way to start. `Proxmark3.sync()` auto-detects the serial port, connects, probes the firmware, and returns a proxy where every command runs synchronously:
```python
from pm3py import Proxmark3
pm3 = Proxmark3.sync() # auto-detect; or Proxmark3.sync("/dev/ttyACM0")
print(pm3.firmware) # version + compatibility, populated on connect
print(pm3.hw.ping()) # {'success': True, 'length': 32, 'data': '00010203...'}
print(pm3.hw.version()["version_string"])
print(pm3.hf.iso14a.scan()) # scan a 14443-A card
pm3.close() # disconnect + close the event loop
```
The sync proxy is REPL-friendly: `dir(pm3)`, `dir(pm3.hf)`, tab-completion, and `help(pm3.hf.iso15.rdbl)` all work and report each command's real signature (the proxy uses `functools.wraps` + `__dir__`, so wrapped methods keep their `__doc__` and parameters instead of showing `(*args, **kwargs)`).
Prefer async? Use the context manager directly:
```python
import asyncio
from pm3py import Proxmark3
async def main():
async with Proxmark3() as pm3: # auto-detect, or Proxmark3("/dev/ttyACM0")
print(await pm3.hw.ping())
print(await pm3.hf.iso14a.scan())
asyncio.run(main())
```
All command methods return structured Python dicts, not parsed terminal text. The command tree mirrors the C client:
| Sub-module | Protocol |
|------------|----------|
| `pm3.hw.*` | Hardware / device control |
| `pm3.lf.*`, `pm3.lf.t55.*` | Low frequency + T55xx |
| `pm3.hf.*` | HF core (tune, search, sniff, dropfield) |
| `pm3.hf.iso14a.*` | ISO 14443-A |
| `pm3.hf.iso15.*` | ISO 15693 |
| `pm3.hf.mf.*` | MIFARE Classic |
| `pm3.hf.mfu.*` | MIFARE Ultralight / NTAG |
## API
### Hardware — `pm3.hw.*`
```python
pm3.hw.ping() # {'success': True, 'length': 32, 'data': '...'}
pm3.hw.ping(length=64) # ping with a custom echo payload length
pm3.hw.version() # {'chip_id': '0x...', 'section_size': ..., 'version_string': '...'}
pm3.hw.status() # {'status': 0}
pm3.hw.capabilities() # {'version', 'baudrate', 'bigbuf_size', 'is_rdv4',
# 'compiled_with_lf', 'compiled_with_iso14443a',
# 'compiled_with_iso15693'}
pm3.hw.tune() # full antenna tuning — nested {'lf': {...}, 'hf': {...}}
pm3.hw.dbg() # get debug level -> {'level': N}
pm3.hw.dbg(level=2) # set debug level 0-4
pm3.hw.led("green", on=True) # LED control by color name
pm3.hw.led("all", off=True) # turn everything off
pm3.hw.break_loop() # stop a long-running firmware operation
pm3.hw.reset() # reset the device (fire-and-forget)
```
`hw.tune()` runs the full LF+HF antenna sweep and returns both bands at once:
```python
t = pm3.hw.tune()
print(t["lf"]["125kHz_V"], t["lf"]["134kHz_V"], t["lf"]["peak_freq_kHz"])
print(t["hf"]["13.56MHz_V"])
```
> **`hw.led` needs our firmware fork** (`CMD_LED_CONTROL` @ `0x011B`). It is not in
> stock upstream firmware — there `0x011A` is `CMD_SET_HF_FIELD_TIMEOUT` and `hw.led`
> is a no-op. With the fork flashed (the `firmware/` submodule) it drives the LEDs as below.
**LED control** is platform-aware — color names map to the correct physical LED on both PM3 Easy and RDV4. Accepts color names (`"green"`, `"red"`, `"orange"`, `"blue"`/`"red2"`, `"all"`), letters (`"a"`-`"d"`), comma-separated combos (`"green,red"`), or a bitmask int. Pick one action kwarg:
```python
pm3.hw.led("red", on=True) # on
pm3.hw.led("a", toggle=True) # toggle
pm3.hw.led("green", blink=True, speed=300, count=10) # blink (all LEDs)
pm3.hw.led("green", pulse=True) # PWM pulse — PWM-capable LEDs only
pm3.hw.led("green", brightness=50) # PWM brightness 0-100
```
PWM effects (`brightness`, `pulse`, `fade`) validate hardware support and raise `PM3Error` if the target LED can't do PWM (Easy: A,B; RDV4: A,D).
Also available: `pm3.hw.tearoff(delay_us=..., on=True)` and `pm3.hw.standalone(mode="14a")` for tear-off timing and standalone modes.
### Low Frequency — `pm3.lf.*`
```python
pm3.lf.tune() # LF antenna voltage @125kHz (divisor 95)
pm3.lf.tune(divisor=88) # @134kHz — see return shape below
pm3.lf.read(samples=12288) # capture ADC samples into device buffer
pm3.lf.sniff() # alias for read()
pm3.lf.config() # get current sampling config
pm3.lf.config(divisor=88) # set config (134 kHz); fire-and-forget then re-read
pm3.lf.search() # capture + return an LFSearchResult
pm3.lf.sim(gap=0, data=b"...") # simulate an LF tag from the sample buffer
```
`lf.tune(divisor=...)` sends the START then MEASURE steps and returns the antenna voltage:
```python
pm3.lf.tune(divisor=95)
# {'voltage_mV': 24875, 'voltage_V': 24.875, 'frequency_kHz': 125.0, 'divisor': 95}
```
Frequency is `12000 / (divisor + 1)` kHz — `95` = 125 kHz, `88` = 134 kHz.
`lf.search()` returns an `LFSearchResult` with callable next steps (it captures raw samples; full EM410x/HID/AWID demodulation still needs the C client's DSP stack):
```python
result = pm3.lf.search()
print(result) # prints available actions + sample count
raw = result.download() # raw ADC samples as bytes (1 byte/sample, 0-255)
result.tune() # check antenna voltage (tag present?)
result.read_t55xx(block=0) # try reading it as a T55xx
```
**T55xx tags — `pm3.lf.t55.*`:**
```python
pm3.lf.t55.readbl(block=0) # -> {'status', 'data': hex, 'raw': bytes}
pm3.lf.t55.readbl(block=1, password=0x12345678) # password read
pm3.lf.t55.writebl(block=1, data=0xDEADBEEF) # write a 32-bit block
pm3.lf.t55.writebl(block=1, data=0x00, password=0x12345678)
pm3.lf.t55.wakeup(password=0x12345678) # password wakeup
pm3.lf.t55.config() # downlink timing table (client-side)
```
### High Frequency — `pm3.hf.*`
Core HF operations. `tune()` runs the firmware START/MEASURE/STOP state machine internally and reports the antenna voltage; `search()` probes 14443-A then 15693.
```python
pm3.hf.tune() # → {'voltage_mV': 4521, 'voltage_V': 4.521}
pm3.hf.search() # try 14a then 15693 → {'iso14443a': {...}, 'found': True}
pm3.hf.sniff() # sniff raw HF, returns {'status', 'samples'}
pm3.hf.dropfield() # turn the HF field off (fire-and-forget)
```
`search()` returns a dict with an `iso14443a` and/or `iso15693` key when a tag answers, plus `found: bool`. `sniff()` accepts `samples_to_skip`, `triggers_to_skip`, `skip_mode`, `skip_ratio` and returns the captured sample count; for decoded, protocol-aware sniffing (including live streaming) use `SniffSession` instead.
### ISO 14443-A — `pm3.hf.iso14a.*`
```python
pm3.hf.iso14a.scan() # → {'found', 'uid', 'atqa', 'sak', 'ats', ...}
pm3.hf.iso14a.raw(b"\x30\x00") # raw frame → {'status', 'data', 'raw'}
pm3.hf.iso14a.sniff() # sniff 14443-A, returns {'status'}
pm3.hf.iso14a.sim(tagtype=1, uid=b"\x01\x02\x03\x04") # simulate a tag
```
`scan()` selects the card and returns UID/ATQA/SAK plus `ats` and a `select_status` (1=OK+ATS, 2=OK no ATS, 3=proprietary). `raw()` sends a raw frame with the RAW + NO_DISCONNECT flags already set; pass extra `flags` and a `timeout_14a` if needed. `sim()` emulates a tag — `tagtype` picks the model (1=MIFARE Classic 1k, 2=Ultralight, 3=DESFire, 4=ISO14443-4, 7=MFU EV1/NTAG215, 8=MFC 4k, 11=JCOP, 13=ULC, 14=ULAES, ...), `uid` is 4/7/10 raw bytes (empty = take UID from emulator memory), and `exit_after` stops after N reader reads.
For a full software-defined tag served from the firmware response table (NDEF, custom commands, live trace), use `SimSession` with an `NfcType2Tag` — see the simulation section.
### ISO 15693 — `pm3.hf.iso15.*`
```python
pm3.hf.iso15.scan() # quick single-tag → {'found', 'uid', 'dsfid', 'response_flags'}
pm3.hf.iso15.inventory() # 16-slot anti-collision → list of tag dicts
pm3.hf.iso15.rdbl(0) # read block 0
pm3.hf.iso15.wrbl(0, "00112233") # write block 0
pm3.hf.iso15.sniff() # sniff 15693 (blocks until button/timeout)
```
`scan()` is the "is one tag present" convenience and returns a single dict. `inventory()` runs a real anti-collision round:
```python
# 16-slot round (default) → LIST of tags (empty list if none)
pm3.hf.iso15.inventory()
# → [{'uid': 'e0040150...', 'dsfid': 0, 'flags': 0, 'slot': 3}, ...]
# single slot → ONE tag dict (or None)
pm3.hf.iso15.inventory(slots=1)
# → {'uid': 'e0040150...', 'dsfid': 0, 'flags': 0}
# filter by AFI (request-level, only matching tags answer)
pm3.hf.iso15.inventory(afi=0x00)
# filter by DSFID (client-side, on the returned records)
pm3.hf.iso15.inventory(dsfid=0)
```
UIDs come back in display order (already reversed from wire order). Block I/O optionally takes a UID for addressed mode:
```python
pm3.hf.iso15.rdbl(4) # unaddressed read
pm3.hf.iso15.rdbl(4, uid="e004015012345678") # addressed read
# → {'success': True, 'block': 4, 'locked': False, 'data': '00112233', 'raw': b'...'}
pm3.hf.iso15.wrbl(4, "00112233") # unaddressed write (bytes or hex str)
pm3.hf.iso15.wrbl(4, b"\x00\x11\x22\x33", uid="e004015012345678")
# → {'success': True, 'block': 4}
```
`raw_inventory(inv_flags=0x26)` is a diagnostic that returns the unparsed device response (`0x26` = 1 slot, `0x06` = 16 slots). For decoded/live sniffing use `SniffSession`.
### MIFARE Classic — `pm3.hf.mf.*`
> **Renamed:** the MIFARE Classic sub-module used to be `pm3.hf.mfc`. It is now
> `pm3.hf.mf` (the backing module is still `pm3py/core/hf_mfc.py`). Update any
> old scripts.
Keys are 6 bytes, given as a hex string or `bytes`. `key_type` is `0` for
key A (default) or `1` for key B.
```python
pm3 = Proxmark3.sync()
# Read one block (default key FFFFFFFFFFFF, key A)
pm3.hf.mf.rdbl(0)
# → {'success': True, 'block': 0, 'data': '0403...', 'raw': b'\x04\x03...'}
pm3.hf.mf.rdbl(4, key="A0A1A2A3A4A5", key_type=1) # key B
# Write a block (data must be 16 bytes)
pm3.hf.mf.wrbl(4, data="00112233445566778899AABBCCDDEEFF",
key="FFFFFFFFFFFF", key_type=0)
# → {'success': True, 'block': 4}
# Read a whole sector (sectors 031 = 4 blocks, 3239 = 16 blocks)
pm3.hf.mf.rdsc(0) # → {'sector': 0, 'first_block': 0, 'num_blocks': 4, 'blocks': {...}}
# Key-check a block against a list of candidate keys
pm3.hf.mf.chk(0, keys=["FFFFFFFFFFFF", "A0A1A2A3A4A5", "D3F7D3F7D3F7"])
# → {'found': True, 'key': 'ffffffffffff', 'key_type': 0} (or {'found': False})
```
**Nested attack** — recover an unknown key using one known key on the card:
```python
pm3.hf.mf.nested(block=0, key="FFFFFFFFFFFF", key_type=0,
target_block=4, target_key_type=1, calibrate=True)
# → {'success': True, 'is_ok': 0, 'block': 4, 'key_type': 1,
# 'cuid': '...', 'nt_a': '...', 'ks_a': '...', 'nt_b': '...', 'ks_b': '...'}
```
**Magic-card identification** — detect gen1/gen2 "magic" cards:
```python
pm3.hf.mf.cident()
# → {'status': 0, 'magic': 0, 'is_magic': False}
```
**Sniff / simulate:**
```python
pm3.hf.mf.sniff() # routed through the ISO14443-A sniffer
# Simulate a card. size ∈ {"mini","1k","2k","4k"}; UID is 4, 7, or 10 bytes.
pm3.hf.mf.sim(uid="01020304", size="1k")
pm3.hf.mf.sim(uid="04AABBCCDDEE80", size="4k",
atqa="0044", sak=0x18, exit_after=1, interactive=True)
```
For full software-defined MIFARE Classic simulation (response table served from
firmware, live trace), see the sim framework section and `MifareClassicTag`.
### MIFARE Ultralight / NTAG — `pm3.hf.mfu.*`
New sub-module for ISO14443-3A tags with 4-byte pages (MIFARE Ultralight,
UL-C, UL-EV1, UL-AES, and the NTAG21x family). A `READ` returns 4 consecutive
pages (16 bytes) at once; a `WRITE` sets a single 4-byte page.
`keytype` selects the auth scheme (pass the matching `key`):
| keytype | scheme | key length |
|---------|--------|-----------|
| `0` | none (default) | — |
| `1` | UL-C (3DES) | 16 bytes |
| `2` | UL-EV1 / NTAG password | 4 bytes |
| `3` | UL-AES | 16 bytes |
```python
pm3 = Proxmark3.sync()
# Read starting at page 4 → 16 bytes (pages 4,5,6,7)
pm3.hf.mfu.rdbl(4)
# → {'success': True, 'block': 4, 'data': '01020304...', 'raw': b'\x01\x02...'}
# Password-authenticated read (UL-EV1 / NTAG, 4-byte pwd)
pm3.hf.mfu.rdbl(4, key="FFFFFFFF", keytype=2)
# Write one 4-byte page
pm3.hf.mfu.wrbl(4, data="DEADBEEF")
# → {'success': True, 'block': 4}
# Dump pages (each underlying READ pulls 4 pages; stops early on failure)
pm3.hf.mfu.dump(start=0, pages=16)
# → {'success': True, 'pages': 16, 'data': '...', 'raw': b'...'}
```
For software-defined NTAG/Type-2 simulation (NDEF, served from a firmware
response table with live trace), use `NfcType2Tag` with `SimSession` — see the
sim framework section.
## Live Sniff & Simulation
pm3py can drive the Proxmark3 as a **live sniffer** and as a **software-defined card simulator**, with reader↔tag traffic streamed into your REPL in real time. Both use custom firmware (the `firmware/` submodule / `proxmark3-pm3py`) and open the serial port directly, so they run from the plain sync REPL — no asyncio needed.
### Live sniff — `SniffSession`
`SniffSession` starts a firmware sniff and streams decoded frames to the console from a background thread. The Proxmark3 button **toggles pause/resume** while streaming, so you can walk a card up to the field, pause, inspect, and resume without touching the keyboard. Captured frames accumulate across pause/resume cycles in `session.entries`.
```python
from pm3py.sniff.session import SniffSession
s = SniffSession.open() # opens /dev/ttyACM0 by default
s.start_15693(live=True) # streams ISO 15693 trace live; PM3 button pauses/resumes
# ... present a tag to the antenna; decoded reader/tag frames print as they arrive ...
s.stop() # end the sniff (sends BREAK_LOOP)
print(len(s.entries)) # all captured frames across pause/resume cycles
print(s.state) # 'sniffing' | 'paused' | 'stopped'
s.entries[0] # {'protocol','direction','duration','timestamp','data','data_hex','decoded'}
s.close() # stop (if needed) and close the serial port
```
- `SniffSession.open(port="/dev/ttyACM0", baudrate=115200)` — open the port.
- `start_15693(live=True, idle_timeout_ms=400)` — live streaming with button-toggle. Pass `live=False` for the legacy BigBuf batch path (for batch decoding you can also use `pm3.hf.iso15.sniff()`).
- `s.state`, `s.entries`, `s.clear()` — inspect and reset accumulated capture.
- `s.stop()` / `s.close()` — halt streaming / release the port.
Status transitions (Started / Paused / Resumed / Stopped) print inline as the button is pressed.
### Live sim — `SimSession`
`SimSession` compiles a Python tag model into a firmware **response table**, uploads it, and starts the tag simulation. The firmware answers standard commands (inventory, read, write) autonomously at wire speed. With `trace=True`, every reader↔tag frame is decoded and streamed live from a background thread. You can then **edit the tag in the REPL and push the changes live** with `tag.sync()` — no restart of the sim required.
```python
from pm3py.sim import SimSession, IcodeSlix2Tag, ndef_uri
# Build a tag model (uid=None auto-generates a valid E0 04 02.. SLIX2 UID)
tag = IcodeSlix2Tag(uid=None, ndef_message=ndef_uri("https://dngr.us"))
s = SimSession.open() # opens /dev/ttyACM0 by default
s.start_15693(tag, compile=True, trace=True) # upload table, start sim, stream frames live
# ... a phone / reader now reads the emulated tag; frames print as they arrive ...
# Live edit: change the NDEF payload and push it to the firmware emulator memory
tag.set_ndef(ndef_uri("https://example.org/new"))
tag.sync() # firmware now serves the updated content
s.stop() # stop the sim (sends BREAK_LOOP)
s.close() # stop (if needed) and close the port
```
- `SimSession.open(port="/dev/ttyACM0", baudrate=115200)` — open the port for a synchronous sim.
- `start_15693(tag, compile=True, trace=False)` — compile+upload the response table and start the 15693 sim; `trace=True` streams decoded reader↔tag traffic (and reports RF field strength via the optional `on_field_strength` callback).
- `tag.sync()` — push in-REPL edits (UID, memory, NDEF, access conditions) to firmware emulator memory. It does a smart sync: EML memory push for data changes, response-table recompile for access-condition changes. The tag must be bound to a live session (`start_15693` binds it automatically).
- `s.stop()` / `s.close()` — halt the sim / release the port.
A 14443-A sim path (`await s.start_14a(tag)`, WTX relay) also exists but is **async and experimental** — it requires the async `PM3Transport` (not the synchronous `SimSession.open()` port above) and is still under development.
### Structured / scriptable access
For scripting you usually want the frames as **data**, not printed colored text. Both
`start_15693(...)` methods take two optional kwargs (defaults preserve the console
behavior above):
- **`on_frame=callback`** — invoked per decoded frame (from the reader thread) with a
plain dict. Passing it also auto-enables trace streaming on the sim.
- **`quiet=True`** — don't print frames to the console (the callback and the `entries`
accumulator still fill).
Every frame is an ANSI-free dict:
```python
{'protocol': int, 'direction': int, # 0 = reader→tag, 1 = tag→reader
'timestamp': int|None, 'duration': int|None,
'data': bytes, 'data_hex': str,
'decoded': str|None} # e.g. 'INVENTORY', 'READ BLOCK 4'
```
*(SimSession frames also carry `'crc_fail': bool`; sniff frames carry `timestamp`/`duration`.)*
**Push model** — react to frames as they arrive, no printing:
```python
from pm3py.sniff.session import SniffSession
reads = []
s = SniffSession.open()
s.start_15693(on_frame=lambda f: reads.append(f["decoded"]), quiet=True)
# ... run a reader against a tag ...
s.stop(); s.close()
print(reads) # ['INVENTORY', 'READ BLOCK 0', ...] — pure data, no ANSI
```
**Pull model** — run quiet, then read the accumulator afterward:
```python
from pm3py.sim import SimSession, IcodeSlix2Tag
s = SimSession.open()
s.start_15693(IcodeSlix2Tag(uid=None), trace=True, quiet=True) # capture, no printing
# ... phone reads the tag ...
s.stop()
for f in s.entries: # list of frame dicts (a copy)
print(f["direction"], f["data_hex"], f["decoded"])
s.clear(); s.close()
```
`SimSession.entries` mirrors `SniffSession.entries`; both have `.clear()`.
### Card simulation
The simulation framework (`pm3py/sim/` infrastructure, `pm3py/transponders/` tag models) provides pure-Python transponder models served from the firmware response table. Import everything from `pm3py.sim`.
**ISO 15693 (`start_15693`)**
- `NfcType5Tag` — generic NFC Forum Type 5 tag with NDEF (`ndef_text`, `ndef_uri`, `ndef_mime`).
- `IcodeSlix2Tag` (and `IcodeSlixTag`, `Icode3Tag`, `IcodeDnaTag`, `NxpIcodeTag`) — NXP ICODE family with passwords, privacy mode, EAS/AFI, originality signature.
- `Ntag5SwitchTag`, `Ntag5LinkTag`, `Ntag5BoostTag` (base `Ntag5PlatformTag`) — NXP NTAG 5 dual-interface family.
**ISO 14443-A (`start_14a`, async/experimental)**
- `NfcType2Tag` — NFC Forum Type 2 (MIFARE Ultralight / NTAG-style) with NDEF.
- `NfcType4Tag`, `MifareClassicTag`, `DesfireTag` — Type 4, MIFARE Classic (Crypto1), and DESFire models.
```python
from pm3py.sim import SimSession, NfcType5Tag, ndef_uri
# uid=None auto-generates a valid E0-prefixed 15693 UID
tag = NfcType5Tag(uid=None, ndef_message=ndef_uri("https://dngr.us"))
s = SimSession.open()
s.start_15693(tag, trace=True) # emulate an NFC Type 5 tag to a 15693 reader
s.stop(); s.close()
```
NDEF helpers `ndef_text(text, lang="en")`, `ndef_uri(uri)`, and `ndef_mime(mime_type, data)` build the message bytes passed as `ndef_message=` (or via `tag.set_ndef(...)` for live edits). `uid=None` on the 15693 models auto-generates a valid `E0`-prefixed UID with the correct manufacturer/IC-type bytes so phones identify the IC correctly.
## Raw Commands
For anything not yet wrapped, drop down to the raw NG/MIX frame senders. Both are
regular client methods, so they work through the sync REPL proxy or `await` in async
code:
```python
from pm3py import Cmd
# NG frame: cmd + raw payload bytes
resp = pm3.send_ng(Cmd.STATUS)
resp = pm3.send_ng(Cmd.CAPABILITIES)
# MIX frame: cmd + three uint64 args + optional payload
resp = pm3.send_mix(Cmd.HF_ISO14443A_READER, arg0=0x0103)
# resp is a PM3Response: resp.cmd, resp.status, resp.data (bytes)
print(resp.status, resp.data.hex(), len(resp.data))
```
Signatures: `send_ng(cmd, payload=b"", timeout=2.0)` and
`send_mix(cmd, arg0=0, arg1=0, arg2=0, payload=b"", timeout=2.0)`. Both return a
`PM3Response` (fields: `cmd`, `status`, `reason`, `ng`, `data`, `oldarg`). Use these
only for commands the firmware replies to — fire-and-forget commands (e.g.
`HF_DROPFIELD`) send no response and would block until the timeout; call the wrapped
method (`pm3.hf.dropfield()`) for those.
> **REPL discovery:** the sync proxy returned by `Proxmark3.sync()` implements
> `__dir__` and uses `functools.wraps` on every wrapped method, so `dir(pm3)`,
> tab-completion, and `help(pm3.hf.iso15.rdbl)` all surface the real sub-modules,
> method names, and signatures (e.g. `rdbl(block, uid=None)`) instead of an opaque
> `(*args, **kwargs)` wrapper.
## Firmware Compatibility
On connect, pm3py pings the device and fetches the firmware version. The result is on
`pm3.firmware`:
```python
pm3 = Proxmark3.sync()
print(pm3.firmware.compatible) # True if the NG protocol handshake worked
print(pm3.firmware.version_string) # firmware version string
print(pm3.firmware.chip_id) # e.g. "0x270B0A40"
print(pm3.firmware.warnings) # any issues detected during probe
```
Core reader/sniff/HW commands work against **stock RfidResearchGroup/proxmark3**
firmware. The **live streaming** features (streaming sniff and the response-table /
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.
## Architecture
```
pm3py/
├── core/ # wire protocol + device command modules
│ ├── protocol.py # constants, CRC-16/A, Cmd enum, PM3Status
│ ├── transport.py # NG/MIX frame encode/decode, PM3Transport (async serial)
│ ├── client.py # Proxmark3, _SyncProxy, FirmwareInfo
│ ├── hw.py # hw.* (ping, version, tune, LEDs, dbg, reset)
│ ├── hf.py # hf.* core (tune, search, sniff, dropfield)
│ ├── hf_iso14a.py # hf.iso14a.* (ISO 14443-A)
│ ├── hf_iso15.py # hf.iso15.* (ISO 15693)
│ ├── hf_mfc.py # hf.mf.* (MIFARE Classic)
│ ├── hf_mfu.py # hf.mfu.* (MIFARE Ultralight / NTAG)
│ └── lf.py # lf.* + lf.t55.* (T55xx) + LFSearchResult
├── trace/ # firmware trace parsing + annotation (shared by sniff & sim)
│ ├── trace.py # parse_tracelog
│ ├── decode_iso14a.py / decode_iso15.py # per-protocol decoders
│ ├── ndef.py # NDEF TLV/record decode
│ └── format.py # ANSI color trace formatting
├── sniff/ # SniffSession — protocol detection, live streaming lifecycle
├── sim/ # card-sim infrastructure: SimSession, response-table compiler,
│ # Medium/Reader models, MCU bridge, replay/relay/fuzzer
└── transponders/ # pure-Python tag models
├── hf/iso14443a/ # Tag14443A, MifareClassic, DESFire, NfcType2/4
├── hf/iso15693/ # Tag15693, NfcType5, ICODE/SLIX2/DNA/NTAG5
└── lf/ # EM4100, HID, T5577
```
- **`core/`** is the wire stack: `transport.py` handles serial I/O and NG/MIX frame
encode/decode; command modules send structured commands and parse responses into
dicts. USB connections skip CRC (matching the C client); FPC UART uses CRC-16/A with
byte-swapped wire encoding.
- **`trace/`** parses the firmware trace log and annotates it (per-protocol decoders +
NDEF), used by both sniff and sim.
- **`sniff/`** orchestrates protocol detection and the live `CMD_HF_SNIFF_STREAM`
streaming loop.
- **`sim/`** + **`transponders/`** implement software-defined tags served from a
firmware BigBuf response table, with a WTX relay for real-time Layer-4 crypto.
## Tests
```bash
cd pm3py
python -m pytest tests/ -v
```
All tests use mock transports — **no hardware required**.