build(rawcli): make prompt_toolkit a core dependency + document rawcli

`pm3py rawcli` is a headline tool, so prompt_toolkit moves from the
[rawcli] extra into core dependencies — a plain `pip install` gets it and
the command just works. The extra is removed; the missing-module guard now
points at a reinstall (it only fires on a broken env).

README: add the rawcli feature bullet, a dedicated "pm3py rawcli"
section (breadcrumb, entry toggle, identify, function-style commands, live
trace, TLV, completion), prompt_toolkit in the dependency list, and cli/
in the architecture tree.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
michael
2026-07-14 12:25:52 -07:00
parent 3e7f47a740
commit 5f57cc4202
3 changed files with 44 additions and 6 deletions

View File

@@ -7,6 +7,7 @@ Pure-Python async client for the [Proxmark3](https://github.com/RfidResearchGrou
- **Async-first, REPL-friendly** — `async`/`await` core with a synchronous proxy (`Proxmark3.sync()`) that supports `dir()`, `help()`, and tab-completion. - **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. - **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. - **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.
- **Interactive raw console** — [`pm3py rawcli`](#pm3py-rawcli--interactive-raw-command-console) drops into a Proxmark-style TUI for raw command entry: a live annotated trace, hex/binary toggle, and function-style transponder commands.
## Install ## Install
@@ -19,8 +20,8 @@ cd pm3py
pip install -e . # or `pip install .` pip install -e . # or `pip install .`
``` ```
Requires **Python 3.10+**; dependencies (`pyserial-asyncio`, `bitarray`, `pycryptodome`) Requires **Python 3.10+**; dependencies (`pyserial-asyncio`, `bitarray`, `pycryptodome`,
install automatically. It's pure Python + pyserial, so it also runs on a **Raspberry Pi** `prompt_toolkit`) install automatically. It's pure Python + pyserial, so it also runs on a **Raspberry Pi**
(aarch64) — see [Flashing firmware](#flashing-firmware-pm3flash) for the Pi bring-up. (aarch64) — see [Flashing firmware](#flashing-firmware-pm3flash) for the Pi bring-up.
## Quick Start ## Quick Start
@@ -489,6 +490,43 @@ method (`pm3.hf.dropfield()`) for those.
> method names, and signatures (e.g. `rdbl(block, uid=None)`) instead of an opaque > method names, and signatures (e.g. `rdbl(block, uid=None)`) instead of an opaque
> `(*args, **kwargs)` wrapper. > `(*args, **kwargs)` wrapper.
## `pm3py rawcli` — interactive raw-command console
`pm3py rawcli` opens an interactive, Proxmark-style console for driving a connected device at the
raw level — a live annotated trace, flexible hex/binary entry, and function-style transponder
commands. It's a **separate tool from the Python REPL**: its own command grammar, not Python.
```bash
pm3py rawcli # auto-detect the device (runs offline if none attached)
```
```text
[raw / 0x] identify # probe the field; sets HF/LF + transponder, keeps the link
NTAG213 (HF / 14a) UID 04A1B2C3
[raw / hf / NTAG213 / 0x] READ(4) # function-style command from the tag's catalog
[Rdr] Reader → Tag: 30 04 READ page 4
[Rdr] Tag → Reader: 01 02 03 04 …
[raw / hf / NTAG213 / 0x] 30 04 # …or send raw bytes directly
[raw / hf / NTAG213 / 0x] tlv 00 03 09 D1 01 05 54 02 65 6E 48 69 FE # decode a TLV / NDEF block
```
- **Breadcrumb prompt** `[raw / hf|lf / <transponder> / 0x|0b]` reflects the field, the identified
tag, and the current numeric entry mode.
- **Entry modes** — type hex (`30 04`) or binary; **Ctrl-/** toggles `0x`/`0b`, digits auto-group
into bytes, and `0x`/`0b` prefixes can be intermixed on one line.
- **`identify`** probes 14a → 15693 → LF and keeps the tag selected; **`transponder`** / **`close`**
manage the connection.
- **Function-style commands** — `READ(4)`, `GET_VERSION()`, `WRITE(4, 0x01020304)` from the
identified tag's catalog (Type 2 / MIFARE Classic / ISO 15693 seeded). `help` lists them,
`help READ` shows one.
- **Live annotated trace** — every exchange prints the reader↔tag frames with decoded annotations.
- **TLV / NDEF** — `tlv <hex>` prints a structured multi-line breakdown; a multi-line editor
composes long TLVs.
- **ipython-style completion** — command/argument menus with each command's help as the tooltip,
plus history auto-suggestion.
## Firmware Compatibility ## Firmware Compatibility
On connect, pm3py pings the device and fetches the firmware version. The result is on On connect, pm3py pings the device and fetches the firmware version. The result is on
@@ -581,6 +619,7 @@ pm3py/
│ ├── decode_iso14a.py / decode_iso15.py # per-protocol decoders │ ├── decode_iso14a.py / decode_iso15.py # per-protocol decoders
│ ├── ndef.py # NDEF TLV/record decode │ ├── ndef.py # NDEF TLV/record decode
│ └── format.py # ANSI color trace formatting │ └── format.py # ANSI color trace formatting
├── cli/ # `pm3py` console-script: rawcli (interactive raw-command TUI)
├── sniff/ # SniffSession — protocol detection, live streaming lifecycle ├── sniff/ # SniffSession — protocol detection, live streaming lifecycle
├── sim/ # card-sim infrastructure: SimSession, response-table compiler, ├── sim/ # card-sim infrastructure: SimSession, response-table compiler,
│ # Medium/Reader models, MCU bridge, replay/relay/fuzzer │ # Medium/Reader models, MCU bridge, replay/relay/fuzzer

View File

@@ -25,9 +25,8 @@ def main(argv: list[str] | None = None) -> int:
import prompt_toolkit # noqa: F401 import prompt_toolkit # noqa: F401
except ModuleNotFoundError: except ModuleNotFoundError:
print( print(
"pm3py rawcli needs prompt_toolkit, which isn't installed.\n" "pm3py rawcli needs prompt_toolkit (a core dependency) but it isn't importable.\n"
" install it with: pip install 'pm3py[rawcli]'\n" " reinstall pm3py, or: pip install prompt_toolkit",
" or directly: pip install prompt_toolkit",
file=sys.stderr, file=sys.stderr,
) )
return 2 return 2

View File

@@ -38,11 +38,11 @@ dependencies = [
"pyserial-asyncio>=0.6", "pyserial-asyncio>=0.6",
"bitarray>=2.0", "bitarray>=2.0",
"pycryptodome>=3.10", "pycryptodome>=3.10",
"prompt_toolkit>=3.0",
] ]
[project.optional-dependencies] [project.optional-dependencies]
dev = ["pytest"] dev = ["pytest"]
rawcli = ["prompt_toolkit>=3.0"]
[project.scripts] [project.scripts]
pm3flash = "pm3py.flash_cli:main" pm3flash = "pm3py.flash_cli:main"