Compare commits
3 Commits
3be2d3f936
...
docs/readm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2da1515f8d | ||
|
|
63db10d400 | ||
|
|
ad814c6a12 |
167
README.md
167
README.md
@@ -7,61 +7,123 @@ ESP32-S3 + PN532 + a serious web UI — no desktop app, no dongle software, no m
|
||||
|
||||
## Why this is the best tool for PN532 workflows
|
||||
|
||||
1. **Full remote control in the browser** — Dashboard, live scan, deep capture, read/write helpers, raw PN532 frames, and status — all over HTTP + WebSocket. You can stand across the room with your phone while the hardware sits on the bench.
|
||||
1. **Full remote control in the browser** — Dashboard, live scan, deep capture, read/write helpers, dictionary attack, raw PN532 commands, and live status — all over **HTTP + WebSocket**. You can stand across the room with your phone while the hardware sits on the bench.
|
||||
|
||||
2. **“Deep capture” is actually deep (for a PN532)** — On each new tag we don’t stop at UID: we pull **PN532 general status bytes**, **full inventory (ATQA/SAK/UID)**, then either a **MIFARE Classic sector sweep** (default key set, Key A and Key B per sector trailer) with **every readable block as hex**, or an **Ultralight/NTAG-style page sweep** until the tag stops responding. Unknown SAKs still get inventory + controller status so nothing is silently dropped.
|
||||
2. **“Deep capture” is actually deep (for a PN532)** — On each **new UID** the firmware doesn’t stop at inventory: it builds a **JSON profile** with **MIFARE Classic** sector sweeps (default keys, Key **A** then **B** per sector trailer, **1K and 4K layouts**) with **every readable block as hex**, or an **Ultralight/NTAG-style page sweep** (up to **240 pages** in firmware). Unknown or non-Classic paths still get solid **ATQA/SAK/UID** plus whatever the stack can return — nothing is silently dropped.
|
||||
|
||||
3. **RAM session buffer with hard stop + phone download** — Captures are stored in **on-chip RAM** (default **48 KB** of NDJSON lines). When the buffer is full, **RF polling pauses** so you never lose data to silent overflow. You tap **Download NDJSON** on your phone, get a single file with every profile, then **Clear** to resume. That workflow is purpose-built for field audits and clone/research sessions.
|
||||
3. **RAM session buffer with a hard stop + phone download** — Captures live in **on-chip RAM** (**48 KB** buffer, NDJSON lines). When the buffer is full, **RF polling pauses** so you never lose data to silent overflow. You tap **Download NDJSON**, get one file with every profile, then **Clear** to resume. That workflow is built for field audits, bench sessions, and anything where “I need the dump on my phone, now” matters.
|
||||
|
||||
4. **Self-hosted on the device** — Default **open SoftAP** SSID `PN532-Toolkit` (no password, lab default), **mDNS** `pn532tool.local`. No cloud, no account, no telemetry.
|
||||
4. **Self-hosted on the device** — Default **SoftAP**: SSID **`PN532-Toolkit`**, **open network** (no password, lab default). **mDNS** hostname **`pn532tool.local`** (HTTP port **80**). No cloud, no account, no telemetry.
|
||||
|
||||
5. **Honest architecture** — ESP-IDF, explicit components (`pn532_host`, `nfc_engine`, `net_service`), embedded Vite/React UI flashed to SPIFFS. You can extend it like real firmware, not a black-box sketch.
|
||||
5. **Honest architecture** — **ESP-IDF**, explicit components (`pn532_host`, `nfc_engine`, `net_service`), **Vite/React** UI embedded in **SPIFFS**. You extend it like real firmware, not a black-box sketch. PN532 talks **SPI, I2C, or UART (HSU)** — pins and bus are **menuconfig**, not guesses.
|
||||
|
||||
6. **Raw frame escape hatch** — When the high-level UI isn’t enough, hit **Raw** and send PN532 command bytes (frame wrapper handled in firmware). That’s how you stay aligned with the real chip, not a toy abstraction.
|
||||
6. **Raw PN532 path** — When the high-level UI isn’t enough, **Raw** sends **command bytes**; the stack handles transport framing. **`GET /api/pn532/general-status`** exposes the controller’s **general status bytes** for debugging. That’s how you stay aligned with the real chip, not a toy abstraction.
|
||||
|
||||
This stack is **not** a Proxmark replacement (no LF, no raw carrier manipulation). For **hosted NFC with PN532**, it’s built to be the **most complete pocket operator**: remote UI, deep reads, session export, and a path to grow.
|
||||
7. **UI that matches the ambition** — Live **WebSocket** stream (scan + capture), **browser-side log** (thousands of events, exportable JSON), optional **`apiBase`** for talking to the box from another host, **HashRouter** so refreshes don’t fight the server, and **lab fixtures** (synthetic card blobs, keys, SHA checks) so you can validate the UI without burning tags.
|
||||
|
||||
This stack is **not** a Proxmark replacement (no LF, no raw carrier manipulation, no FPGA tricks). For **hosted NFC with a PN532**, it’s the **full pocket operator**: remote UI, deep reads, session export, Classic dictionary work, Ultralight read/write, card emulation hooks, and a straight line to the command set when you need it.
|
||||
|
||||
---
|
||||
|
||||
## Every feature, in one pass
|
||||
|
||||
### Firmware (what the chip actually does)
|
||||
|
||||
| Capability | What you get |
|
||||
|------------|----------------|
|
||||
| **Transport** | PN532 over **SPI** (default pins in Kconfig), **I2C**, or **UART (HSU)** — bit rate and GPIOs set in `menuconfig`. |
|
||||
| **Continuous scan** | Background task polls passive targets; **~65 ms** when not in deep mode, **~220 ms** when deep capture is on (more time per tag for heavy work). Toggle via **`POST /api/nfc/scan`** or the Dashboard. |
|
||||
| **RF sensitivity** | On init, **high passive-activation retries** (`RFConfiguration` **0x05**) so marginal tags get more chances to answer. |
|
||||
| **Deep profile** | Per new UID: Classic sector layout detection (1K / 4K), per-sector auth with built-in default keys, block hex arrays; Ultralight/NTAG page reads up to firmware limit; structured **JSON** for export. |
|
||||
| **Session RAM** | **~48 KB** NDJSON capture buffer; mutex-protected; **full** flag stops polling until you clear. |
|
||||
| **HTTP API** | JSON in/out on documented routes; **CORS** headers on API responses; **`OPTIONS /*`** preflight for cross-origin clients. POST bodies are read **completely** (chunked TCP-safe). |
|
||||
| **WebSocket** | **`/ws`**: JSON envelopes `{"channel":"…","payload":…}` — **`scan`** (inventory / `present:false` when tag leaves), **`capture`** (`recorded`, `bufferFull`, etc.), **`pong`** for keepalive. |
|
||||
| **OTA endpoint** | **`POST /api/ota`** now performs a real **HTTPS OTA** with the ESP-IDF certificate bundle; successful updates reboot into the new slot and rollback support is enabled. |
|
||||
| **Onboard RGB (DevKit-style)** | Optional **boot-time** shutdown of a **WS2812/SK6812** on **GPIO 48** (Kconfig) so the addressable LED isn’t left on a random color — **not** on all boards; power LEDs are hardware. |
|
||||
| **Status** | **`/api/status`**: uptime, free heap, Wi‑Fi mode, **PN532 firmware version** (when reachable), scan flag, `targetActive`, and session stats. |
|
||||
| **Probe API** | **`POST /api/nfc/probe`** builds a richer capability profile for the present tag, including structured-clone suitability and Type 2 `GET_VERSION` when supported. |
|
||||
| **Structured clone flow** | **`POST /api/clone/capture`** normalizes MIFARE Classic and Type 2 tags into JSON snapshots; **`POST /api/clone/program`** writes supported snapshots back to a destination tag with safe defaults. |
|
||||
| **Target mode** | **`POST /api/nfc/target/*`** provides `status`, `start`, `recv`, `send`, and `stop` around **`TgInitAsTarget` / `TgGetData` / `TgSetData`**, with a raw-parameter escape hatch when you need exact bytes. |
|
||||
|
||||
### Web UI (each page)
|
||||
|
||||
| Page | Purpose |
|
||||
|------|---------|
|
||||
| **Dash** | Live **status** poll, **continuous scan** on/off, **manual poll** (uses live tag from WebSocket when present), hero readout. |
|
||||
| **Read-all (Capture)** | Toggle **deep capture**, **export** NDJSON from device RAM, **clear** buffer, progress vs **48 KB** budget. |
|
||||
| **Read** | **MIFARE** block read with key + Key A/B; **Ultralight** page read; paste a **sector trailer** to decode **access bits** in the browser. |
|
||||
| **Write** | **MIFARE** block write (16-byte hex, confirm dialog); **Ul** page write; **lab fixture** loader for offline testing flows; ties into **live UID** from WebSocket when available. |
|
||||
| **Brute** | **Classic 1K / 4K** dictionary attack: firmware’s built-in key set + your lines, optional **bounded variations**; shows JSON result. |
|
||||
| **Emu** | **`TgInitAsTarget`** and friends via **`POST /api/nfc/emulate-raw`** — you supply hex; **experimental** (bad sequences can require power cycle). |
|
||||
| **KeyLab** | **XOR span** across up to three 6-byte base keys (all combinations → candidates for Brute/Keys); **Shannon entropy** on arbitrary hex blobs. **Stage for Keys** copies candidates into **sessionStorage** for the Keys page. |
|
||||
| **Lib** | **Saved hex snippets** in localStorage; **seed** from built-in **lab catalog** samples; fixture loader for repeatable demos. |
|
||||
| **Keys** | **Browser-local** key dictionary (defaults + your lines); **merge lab keys**; **import Key Lab stash** from staged session. Used when you copy keys into Brute manually — **not** auto-uploaded to flash. |
|
||||
| **Raw** | Hex **command** payload → **`/api/raw/pn532`**; **general status** dump from **`/api/pn532/general-status`**; local TX/RX log. |
|
||||
| **Set** | **`apiBase`** (empty = same host), **theme** (light/dark), reminder of SoftAP + mDNS. |
|
||||
|
||||
### Browser extras (no extra server)
|
||||
|
||||
- **Top bar** — **Download log JSON**: last **5 000** WebSocket messages from **sessionStorage** (scan + capture + anything else the firmware sends).
|
||||
- **Scan feedback** — Visual/audio “hit” flourishes on new tag / capture events (debounced so it doesn’t spam).
|
||||
- **Validation fixtures** — Shared **lab** blobs and notes (`validationFixtures.ts`) for UI testing and teaching.
|
||||
|
||||
---
|
||||
|
||||
## Remote deep capture (how to use it)
|
||||
|
||||
1. **Power on** → firmware starts **continuous scan** automatically (you can still toggle it on the Dashboard).
|
||||
2. **Read-all** (Capture page) → enable **passive read-all** (`POST /api/session/deep`) when you want automatic full dumps per new tag into device RAM.
|
||||
1. **Power on** → firmware starts **continuous scan** by default (toggle on the Dashboard).
|
||||
2. Open **Read-all (Capture)** → enable **passive read-all** (`POST /api/session/deep`).
|
||||
3. Present tags; each **new UID** appends one **NDJSON** line: full deep profile JSON.
|
||||
4. When the **progress bar hits the end**, scanning **pauses** automatically.
|
||||
5. Tap **Download NDJSON** on your phone (or laptop) — browser saves `pn532-deep-capture-*.ndjson`.
|
||||
6. Tap **Clear buffer** to free RAM and **resume** scanning.
|
||||
5. Tap **Download NDJSON** — browser saves `pn532-deep-capture-*.ndjson`.
|
||||
6. Tap **Clear buffer** to free RAM and **resume**.
|
||||
|
||||
API (for automation):
|
||||
### Session API (automation)
|
||||
|
||||
| Method | Path | Purpose |
|
||||
|--------|------|---------|
|
||||
| `GET` | `/api/status` | Includes `session.usedBytes`, `maxBytes`, `lines`, `full`, `deepCapture` |
|
||||
| `POST` | `/api/session/deep` | Body `{"enable":true}` — toggle deep capture |
|
||||
| `GET` | `/api/session/export` | Attachment: all captured lines |
|
||||
| `POST` | `/api/session/clear` | Wipe buffer, clear `full` |
|
||||
| `GET` | `/api/status` | App name, uptime, heap, Wi‑Fi mode, PN532 version, `scanning`, `session` (`usedBytes`, `maxBytes`, `lines`, `full`, `deepCapture`) |
|
||||
| `POST` | `/api/session/deep` | Body `{"enable": true}` or `false` |
|
||||
| `GET` | `/api/session/export` | `text/plain` / NDJSON attachment |
|
||||
| `POST` | `/api/session/clear` | Clears RAM buffer |
|
||||
|
||||
WebSocket `/ws`: channels `scan` (inventory) and `capture` (recorded / bufferFull events).
|
||||
### Live NFC API
|
||||
|
||||
### Browser memory log (nothing missed in the UI)
|
||||
| Method | Path | Purpose |
|
||||
|--------|------|---------|
|
||||
| `POST` | `/api/nfc/poll` | One-shot poll; body may be `{}` — returns `present`, optional `tag` |
|
||||
| `POST` | `/api/nfc/probe` | Rich capability probe for the current tag; adds clone/program hints and Type 2 `GET_VERSION` when available |
|
||||
| `POST` | `/api/nfc/scan` | Body `{"enable": true}` or `false` — continuous background scan |
|
||||
| `GET` | `/api/pn532/general-status` | Raw PN532 **general status** byte array |
|
||||
| `POST` | `/api/mifare/read-block` | `block`, `key` (12 hex), `keyB` |
|
||||
| `POST` | `/api/mifare/write-block` | `block`, `key`, `keyB`, `data` (32 hex) |
|
||||
| `POST` | `/api/ul/read-page` | `page` |
|
||||
| `POST` | `/api/ul/write-page` | `page`, `data` (8 hex) |
|
||||
| `POST` | `/api/raw/pn532` | `frame` — PN532 command bytes (not a full transport frame) |
|
||||
| `POST` | `/api/mifare/dictionary-attack` | `readerType` (`classic1k` / `classic4k`), `variations`, optional `keysHex[]`, optional `sectorFirst` / `sectorLast` |
|
||||
| `POST` | `/api/clone/capture` | Optional `mode` (`auto` / `classic` / `type2`), optional `keysHex[]`, `variations`, `maxPages` — returns a structured snapshot for supported tags |
|
||||
| `POST` | `/api/clone/program` | `snapshot` plus optional `includeTrailers` / `includeLockPages` — programs a supported destination tag with safe skips by default |
|
||||
| `POST` | `/api/nfc/target/status` | Returns whether PN532 target mode is active |
|
||||
| `POST` | `/api/nfc/target/start` | Start target mode from semantic JSON fields or `rawParamsHex` |
|
||||
| `POST` | `/api/nfc/target/recv` | Wait for initiator bytes and return status + payload hex |
|
||||
| `POST` | `/api/nfc/target/send` | Send target response bytes back to the initiator |
|
||||
| `POST` | `/api/nfc/target/stop` | Return PN532 to normal SAM mode |
|
||||
| `POST` | `/api/nfc/emulate-raw` | `hex` — raw command bytes (e.g. **0x8C** sequences) |
|
||||
| `POST` | `/api/ota` | `url` (HTTPS), optional `reboot` — performs a real OTA update via `esp_https_ota` |
|
||||
|
||||
The SPA keeps the **last 5 000** WebSocket events in **sessionStorage** (scan + capture + anything else the firmware pushes). Use the top bar **Download log JSON** to save a pretty file on your phone or PC without touching device RAM. Clear the log separately from the device capture buffer.
|
||||
**WebSocket:** `GET /ws` — JSON text frames as above; send **`ping`**, receive **`pong`**.
|
||||
|
||||
### “Maximum sensitivity” (firmware)
|
||||
### Browser memory log
|
||||
|
||||
On init the PN532 is configured for **high passive-activation retries** (`RFConfiguration` 0x05), and the poll loop runs at **~65 ms** when not doing deep capture — weak coupling / marginal tags get more chances to answer.
|
||||
The SPA keeps the **last 5 000** WebSocket events in **sessionStorage**. **Download log JSON** saves a pretty file on your phone or PC **without** using device RAM. Clear that log separately from the device capture buffer.
|
||||
|
||||
### Dictionary “brute” (Classic)
|
||||
|
||||
**Brute** page → pick **Classic 1K / 4K map**, optional **variations**, paste extra keys. Firmware runs **`POST /api/mifare/dictionary-attack`**: built-in **public default keys** (subset of community lists such as the [Proxmark3 mfc_default_keys.dic](https://github.com/RfidResearchGroup/proxmark3/blob/master/client/dictionaries/mfc_default_keys.dic)) plus your lines, Key **A** then **B** per sector trailer. **Variations** add bounded XOR / nibble tweaks — **not** a full 2⁴⁸ keyspace search. You will **not** magically open every door; you will systematically try keys people actually leak online. ### Emulation
|
||||
**Brute** page → **Classic 1K / 4K**, optional **variations**, paste extra keys. Firmware runs **`POST /api/mifare/dictionary-attack`**: built-in **public default keys** (same family as community lists like [Proxmark3 `mfc_default_keys.dic`](https://github.com/RfidResearchGroup/proxmark3/blob/master/client/dictionaries/mfc_default_keys.dic)) plus your lines, Key **A** then **B** per sector trailer. **Variations** add bounded XOR / nibble-style tweaks — **not** a full 2⁴⁸ search. You won’t magically open every sector; you **systematically** try keys people actually leak in the wild.
|
||||
|
||||
**Emulate** sends raw **`TgInitAsTarget` (0x8C)** payloads via **`POST /api/nfc/emulate-raw`**. You are responsible for correct bytes (NXP UM0701). This is **experimental**; bad sequences can require a power cycle.
|
||||
### Card emulation (PN532 target mode)
|
||||
|
||||
| Method | Path | Purpose |
|
||||
|--------|------|---------|
|
||||
| `POST` | `/api/mifare/dictionary-attack` | Body: `readerType`, `variations`, optional `keysHex[]` |
|
||||
| `POST` | `/api/nfc/emulate-raw` | Body: `{ "hex": "8C..." }` |
|
||||
| `POST` | `/api/ul/write-page` | Body: `{ "page": 4, "data": "AABBCCDD" }` (8 hex) |
|
||||
**Emulate** now has two layers: a higher-level **`/api/nfc/target/*`** subsystem for `TgInitAsTarget` / `TgGetData` / `TgSetData`, plus **`POST /api/nfc/emulate-raw`** when you want to drive the exact byte stream yourself. The structured path is safer; raw mode is still experimental and bad sequences can wedge the front end until re-init or power cycle.
|
||||
|
||||
---
|
||||
|
||||
@@ -73,21 +135,11 @@ On init the PN532 is configured for **high passive-activation retries** (`RFConf
|
||||
./start-firmware.sh
|
||||
```
|
||||
|
||||
What it does:
|
||||
What it does (see script for flags): bootstraps ESP-IDF under a **local prefix** if needed, builds the web UI into **`firmware/data`**, targets **esp32s3**, auto-detects serial when possible, **builds and flashes**.
|
||||
|
||||
- bootstraps ESP-IDF locally if needed
|
||||
- builds the web UI into `firmware/data`
|
||||
- configures target `esp32s3`
|
||||
- auto-detects the serial port
|
||||
- builds and flashes the device
|
||||
Options include **`--build-only`**, **`--monitor`**, **`--port`**, etc.
|
||||
|
||||
Useful options:
|
||||
|
||||
- `./start-firmware.sh --build-only`
|
||||
- `./start-firmware.sh --monitor`
|
||||
- `./start-firmware.sh --port /dev/cu.usbmodemXXXX`
|
||||
|
||||
### Web UI → flash image
|
||||
### Web UI → SPIFFS image
|
||||
|
||||
```bash
|
||||
cd web
|
||||
@@ -100,35 +152,17 @@ npm run build:fw
|
||||
```bash
|
||||
cd firmware
|
||||
idf.py set-target esp32s3
|
||||
idf.py menuconfig # PN532 Host: SPI / I2C / UART + pins
|
||||
idf.py menuconfig # PN532 Host: SPI / I2C / UART + pins; Board indicators (RGB) if you use DevKitC-1
|
||||
idf.py build flash monitor
|
||||
```
|
||||
|
||||
See [docs/FLASHING.md](docs/FLASHING.md) and [docs/PINOUT.md](docs/PINOUT.md).
|
||||
### Common red PN532 module notes
|
||||
|
||||
---
|
||||
The common red Elechouse-style PN532 board usually selects its bus with two onboard switches: **HSU = OFF/OFF**, **I2C = ON/OFF**, **SPI = OFF/ON**. HSU and I2C usually share header pins, and many boards ship in **HSU** by default, so a “dead” SPI/I2C setup is often just a switch mismatch rather than bad firmware.
|
||||
|
||||
## 9 meaningful upgrades we don’t have yet (roadmap)
|
||||
From `firmware/`, with IDF already in your environment, you can also **`./flash.sh`** (uses **`ESPPORT`** or the first argument as the serial device).
|
||||
|
||||
1. **FeliCa / Type B surfaces** — More first-class UI for non–Type A paths the PN532 can speak.
|
||||
|
||||
2. **Configurable RAM budget + optional PSRAM** — Compile-time or NVS `maxBytes`, and external SPIRAM for **multi‑hundred‑KB** sessions on N8R8 modules.
|
||||
|
||||
3. **Chunked / resumable export** — HTTP range or multipart export so **multi‑MB** captures don’t require one giant `httpd_resp_send`.
|
||||
|
||||
4. **User-supplied key dictionary on device** — Upload common keys file to flash and run **automatic sector retries** without typing keys in the UI.
|
||||
|
||||
5. **NDEF record editor** — Parse TLV/NDEF in the browser, edit records, write back through page/block APIs with lock-byte warnings.
|
||||
|
||||
6. **ISO14443-4 / Type B automation** — Higher-layer APDU helpers where PN532 allows; clearer UI for “Type A only” vs “RATS/PPS” paths.
|
||||
|
||||
7. **Wi-Fi STA onboarding** — Captive portal or dedicated SSID scan + save credentials to NVS (today defaults to AP).
|
||||
|
||||
8. **Real OTA from the UI** — Replace `501` stub with signed `esp_https_ota` + rollback partition sanity checks.
|
||||
|
||||
9. **Installable PWA + richer diagnostics** — `manifest.json`, icons, `theme-color`, and UI for **error histograms / timing** from the PN532 status path.
|
||||
|
||||
10. **Magic UID / Gen2** — Not exposed; PN532 is a legitimate reader/writer, not a UID-spoofing modem.
|
||||
See [docs/FLASHING.md](docs/FLASHING.md) and [docs/PINOUT.md](docs/PINOUT.md). Limitations and scope notes: [docs/LIMITATIONS.md](docs/LIMITATIONS.md).
|
||||
|
||||
---
|
||||
|
||||
@@ -136,6 +170,7 @@ See [docs/FLASHING.md](docs/FLASHING.md) and [docs/PINOUT.md](docs/PINOUT.md).
|
||||
|
||||
| Path | Role |
|
||||
|------|------|
|
||||
| `firmware/` | ESP-IDF: PN532 transport, NFC engine, **session RAM buffer**, **deep profile**, HTTP/WS |
|
||||
| `web/` | React UI: Dashboard, **Capture**, Read/Write, Library, Keys, Raw, Settings |
|
||||
| `firmware/` | ESP-IDF: PN532 transport, NFC engine, **session buffer**, **deep profile**, HTTP/WebSocket, SPIFFS |
|
||||
| `web/` | React UI: Dashboard, Capture, Read, Write, Brute, Emulate, KeyLab, Library, Keys, Raw, Settings |
|
||||
| `docs/` | Flashing, pinout, workflows, limitations |
|
||||
| `start-firmware.sh` | One-shot IDF bootstrap + build + flash |
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
The PN532 is a **hosted NFC controller**, not a low-level RF lab instrument. This project exposes PN532 features honestly:
|
||||
|
||||
- **ISO14443-B**: reader support exists with **chip-level caveats** (anticollision / stack-dependent behavior). No promises of full mobile/PICC coverage.
|
||||
- **Card emulation / TG modes**: PN532 firmware supports target commands; real-world mimicry depends on timing, UID size, and reader expectations — expose experimentally, not as “propable MIFARE magic.”
|
||||
- **Card emulation / TG modes**: PN532 firmware supports target commands and this repo now exposes a structured target-mode API, but real-world mimicry still depends on timing, UID size, ATS/general bytes, and reader expectations. Treat it as a controllable subsystem, not magic full-card impersonation.
|
||||
- **Key recovery**: on-device “brute force” at Proxmark scale is **infeasible**. The UI provides **dictionary / manual** key workflows.
|
||||
- **Signal / RF metrics**: diagnostics use **PN532 status / timings / retries**, not calibrated dBm.
|
||||
- **OTA via UI**: `POST /api/ota` is a **stub** (`501`) — ship OTA with `esp_https_ota` + signed images when you need production updates.
|
||||
- **OTA via UI**: `POST /api/ota` now performs real HTTPS OTA with the ESP-IDF certificate bundle. For production fleets, add your own release signing, manifest control, and hardware validation gates before broad rollout.
|
||||
|
||||
@@ -20,3 +20,13 @@ The firmware Kconfig ships **example** GPIOs:
|
||||
## Transport selection
|
||||
|
||||
Start with the bus your breakout is jumpered for — **I2C is often simplest** on ESP32 for bring-up; SPI may require **lower clock** initially (e.g. **100 kHz**).
|
||||
|
||||
## Common red module switch matrix
|
||||
|
||||
For the common Elechouse-style red PN532 board, the onboard two-position switch matrix is typically:
|
||||
|
||||
- **HSU**: `OFF/OFF`
|
||||
- **I2C**: `ON/OFF`
|
||||
- **SPI**: `OFF/ON`
|
||||
|
||||
Many of these boards boot in **HSU** by default, and the **I2C** and **HSU** labels often refer to the same physical header pins from opposite sides of the PCB. If the bus looks dead, check the switches before changing firmware.
|
||||
|
||||
BIN
firmware/.cache/clangd/index/app_net.c.8976D557EC300024.idx
Normal file
BIN
firmware/.cache/clangd/index/app_net.c.8976D557EC300024.idx
Normal file
Binary file not shown.
BIN
firmware/.cache/clangd/index/app_net.h.379D685E6D39744F.idx
Normal file
BIN
firmware/.cache/clangd/index/app_net.h.379D685E6D39744F.idx
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
firmware/.cache/clangd/index/jobs.c.32B64D759D53BD2C.idx
Normal file
BIN
firmware/.cache/clangd/index/jobs.c.32B64D759D53BD2C.idx
Normal file
Binary file not shown.
BIN
firmware/.cache/clangd/index/jobs.h.D44D4CC819830F66.idx
Normal file
BIN
firmware/.cache/clangd/index/jobs.h.D44D4CC819830F66.idx
Normal file
Binary file not shown.
BIN
firmware/.cache/clangd/index/led_strip.h.E0D4D2D09150BA23.idx
Normal file
BIN
firmware/.cache/clangd/index/led_strip.h.E0D4D2D09150BA23.idx
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
firmware/.cache/clangd/index/main.c.B516995A6C0A4F7B.idx
Normal file
BIN
firmware/.cache/clangd/index/main.c.B516995A6C0A4F7B.idx
Normal file
Binary file not shown.
BIN
firmware/.cache/clangd/index/mdns.h.F79EBFE9494D08D5.idx
Normal file
BIN
firmware/.cache/clangd/index/mdns.h.F79EBFE9494D08D5.idx
Normal file
Binary file not shown.
BIN
firmware/.cache/clangd/index/mdns_browser.c.844EE27ECDD370B3.idx
Normal file
BIN
firmware/.cache/clangd/index/mdns_browser.c.844EE27ECDD370B3.idx
Normal file
Binary file not shown.
BIN
firmware/.cache/clangd/index/mdns_browser.h.4C9C0DA24E273588.idx
Normal file
BIN
firmware/.cache/clangd/index/mdns_browser.h.4C9C0DA24E273588.idx
Normal file
Binary file not shown.
BIN
firmware/.cache/clangd/index/mdns_console.c.4A45177451EC0E51.idx
Normal file
BIN
firmware/.cache/clangd/index/mdns_console.c.4A45177451EC0E51.idx
Normal file
Binary file not shown.
BIN
firmware/.cache/clangd/index/mdns_debug.c.AA7109521C3B6657.idx
Normal file
BIN
firmware/.cache/clangd/index/mdns_debug.c.AA7109521C3B6657.idx
Normal file
Binary file not shown.
BIN
firmware/.cache/clangd/index/mdns_debug.h.1967329A83A6D539.idx
Normal file
BIN
firmware/.cache/clangd/index/mdns_debug.h.1967329A83A6D539.idx
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
firmware/.cache/clangd/index/mdns_netif.c.15DF3D448C6EF1FA.idx
Normal file
BIN
firmware/.cache/clangd/index/mdns_netif.c.15DF3D448C6EF1FA.idx
Normal file
Binary file not shown.
BIN
firmware/.cache/clangd/index/mdns_netif.h.5B3BB947FC74DB09.idx
Normal file
BIN
firmware/.cache/clangd/index/mdns_netif.h.5B3BB947FC74DB09.idx
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
firmware/.cache/clangd/index/mdns_pcb.c.7AEB4688ECE9D08A.idx
Normal file
BIN
firmware/.cache/clangd/index/mdns_pcb.c.7AEB4688ECE9D08A.idx
Normal file
Binary file not shown.
BIN
firmware/.cache/clangd/index/mdns_pcb.h.95AD3ECE0DDA239D.idx
Normal file
BIN
firmware/.cache/clangd/index/mdns_pcb.h.95AD3ECE0DDA239D.idx
Normal file
Binary file not shown.
BIN
firmware/.cache/clangd/index/mdns_private.h.F18457B0D1445EE2.idx
Normal file
BIN
firmware/.cache/clangd/index/mdns_private.h.F18457B0D1445EE2.idx
Normal file
Binary file not shown.
BIN
firmware/.cache/clangd/index/mdns_querier.c.0578019256BDEEA7.idx
Normal file
BIN
firmware/.cache/clangd/index/mdns_querier.c.0578019256BDEEA7.idx
Normal file
Binary file not shown.
BIN
firmware/.cache/clangd/index/mdns_querier.h.2D0ECFD0DB6BD816.idx
Normal file
BIN
firmware/.cache/clangd/index/mdns_querier.h.2D0ECFD0DB6BD816.idx
Normal file
Binary file not shown.
BIN
firmware/.cache/clangd/index/mdns_receive.c.B5E60DD8BC9F0846.idx
Normal file
BIN
firmware/.cache/clangd/index/mdns_receive.c.B5E60DD8BC9F0846.idx
Normal file
Binary file not shown.
BIN
firmware/.cache/clangd/index/mdns_receive.h.7D5DBD82E2E64FE4.idx
Normal file
BIN
firmware/.cache/clangd/index/mdns_receive.h.7D5DBD82E2E64FE4.idx
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
firmware/.cache/clangd/index/mdns_send.c.59BD33003D102D61.idx
Normal file
BIN
firmware/.cache/clangd/index/mdns_send.c.59BD33003D102D61.idx
Normal file
Binary file not shown.
BIN
firmware/.cache/clangd/index/mdns_send.h.4F6F65E6B9467994.idx
Normal file
BIN
firmware/.cache/clangd/index/mdns_send.h.4F6F65E6B9467994.idx
Normal file
Binary file not shown.
BIN
firmware/.cache/clangd/index/mdns_service.c.3ED53626DAC5EF3D.idx
Normal file
BIN
firmware/.cache/clangd/index/mdns_service.c.3ED53626DAC5EF3D.idx
Normal file
Binary file not shown.
BIN
firmware/.cache/clangd/index/mdns_service.h.C2CF3A06E06F9BB7.idx
Normal file
BIN
firmware/.cache/clangd/index/mdns_service.h.C2CF3A06E06F9BB7.idx
Normal file
Binary file not shown.
BIN
firmware/.cache/clangd/index/mdns_utils.c.65695015EB907823.idx
Normal file
BIN
firmware/.cache/clangd/index/mdns_utils.c.65695015EB907823.idx
Normal file
Binary file not shown.
BIN
firmware/.cache/clangd/index/mdns_utils.h.F6BBFFB10960D7CE.idx
Normal file
BIN
firmware/.cache/clangd/index/mdns_utils.h.F6BBFFB10960D7CE.idx
Normal file
Binary file not shown.
BIN
firmware/.cache/clangd/index/nfc_brute.c.5CE5CA4075A21387.idx
Normal file
BIN
firmware/.cache/clangd/index/nfc_brute.c.5CE5CA4075A21387.idx
Normal file
Binary file not shown.
BIN
firmware/.cache/clangd/index/nfc_brute.h.357ACE396DF621D3.idx
Normal file
BIN
firmware/.cache/clangd/index/nfc_brute.h.357ACE396DF621D3.idx
Normal file
Binary file not shown.
BIN
firmware/.cache/clangd/index/nfc_deep.c.84695751986FA510.idx
Normal file
BIN
firmware/.cache/clangd/index/nfc_deep.c.84695751986FA510.idx
Normal file
Binary file not shown.
BIN
firmware/.cache/clangd/index/nfc_deep.h.CD94CBA950807F37.idx
Normal file
BIN
firmware/.cache/clangd/index/nfc_deep.h.CD94CBA950807F37.idx
Normal file
Binary file not shown.
BIN
firmware/.cache/clangd/index/nfc_engine.c.DF9FC1E131D8D184.idx
Normal file
BIN
firmware/.cache/clangd/index/nfc_engine.c.DF9FC1E131D8D184.idx
Normal file
Binary file not shown.
BIN
firmware/.cache/clangd/index/nfc_engine.h.364F51431207B3F8.idx
Normal file
BIN
firmware/.cache/clangd/index/nfc_engine.h.364F51431207B3F8.idx
Normal file
Binary file not shown.
BIN
firmware/.cache/clangd/index/pn532_core.c.980577ACBAE216BC.idx
Normal file
BIN
firmware/.cache/clangd/index/pn532_core.c.980577ACBAE216BC.idx
Normal file
Binary file not shown.
BIN
firmware/.cache/clangd/index/pn532_core.h.9A8DA7CCA3043FA0.idx
Normal file
BIN
firmware/.cache/clangd/index/pn532_core.h.9A8DA7CCA3043FA0.idx
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -2,5 +2,5 @@ idf_component_register(
|
||||
SRCS "app_net.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES esp_http_server http_parser esp_wifi esp_netif nvs_flash mdns esp_timer
|
||||
json spiffs vfs freertos nfc_engine pn532_host
|
||||
json spiffs vfs freertos nfc_engine pn532_host esp_https_ota app_update mbedtls
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -26,12 +26,26 @@ typedef struct {
|
||||
bool key_b;
|
||||
} nfc_mifare_key_t;
|
||||
|
||||
/** Legacy: full init including transport — aborts on failure via caller's ESP_ERROR_CHECK. */
|
||||
esp_err_t nfc_engine_init(void);
|
||||
|
||||
/** Non-fatal first-boot init. Returns true if PN532 is present and configured. */
|
||||
bool nfc_engine_try_init(void);
|
||||
|
||||
/** Soft re-attach after transport is already open — skips bus re-init, tries chip commands.
|
||||
* Call from a background retry loop; always safe to call even if already ready. */
|
||||
bool nfc_engine_try_reattach(void);
|
||||
|
||||
/** Returns true when the PN532 was successfully initialised (or re-attached). */
|
||||
bool nfc_engine_is_ready(void);
|
||||
|
||||
esp_err_t nfc_poll_passive_target(nfc_tag_info_t *out);
|
||||
bool nfc_tag_is_mifare_classic(const nfc_tag_info_t *tag);
|
||||
bool nfc_tag_is_mifare_classic_4k(const nfc_tag_info_t *tag);
|
||||
bool nfc_tag_is_type2(const nfc_tag_info_t *tag);
|
||||
int nfc_mifare_sector_count(const nfc_tag_info_t *tag);
|
||||
bool nfc_mifare_sector_layout(const nfc_tag_info_t *tag, int sector, int *first_block, int *num_blocks,
|
||||
uint8_t *trailer_block);
|
||||
|
||||
esp_err_t nfc_mifare_authenticate_block(const nfc_tag_info_t *tag, uint8_t block_no,
|
||||
const nfc_mifare_key_t *key);
|
||||
@@ -42,6 +56,7 @@ esp_err_t nfc_ultralight_read_page(uint8_t page, uint8_t data[4]);
|
||||
esp_err_t nfc_ultralight_write_page(uint8_t page, const uint8_t data[4]);
|
||||
|
||||
esp_err_t nfc_ul_fast_read(uint8_t start_page, uint8_t *out, size_t out_max, size_t *got);
|
||||
esp_err_t nfc_type2_get_version(uint8_t version[8]);
|
||||
|
||||
/** Build JSON snapshot of last seen tag + optional blocks (caller frees cJSON). */
|
||||
cJSON *nfc_tag_to_json(const nfc_tag_info_t *tag);
|
||||
|
||||
@@ -170,6 +170,7 @@ cJSON *nfc_mifare_dictionary_attack(nfc_tag_info_t *tag, uint8_t sector_first, u
|
||||
attempts++;
|
||||
if (try_key_on_trailer(tag, trailer, trial, false)) {
|
||||
if (!add_sector_hit(hits, sec, trial, "A")) {
|
||||
cJSON_Delete(hits);
|
||||
cJSON_Delete(root);
|
||||
return NULL;
|
||||
}
|
||||
@@ -178,6 +179,7 @@ cJSON *nfc_mifare_dictionary_attack(nfc_tag_info_t *tag, uint8_t sector_first, u
|
||||
}
|
||||
if (try_key_on_trailer(tag, trailer, trial, true)) {
|
||||
if (!add_sector_hit(hits, sec, trial, "B")) {
|
||||
cJSON_Delete(hits);
|
||||
cJSON_Delete(root);
|
||||
return NULL;
|
||||
}
|
||||
@@ -202,6 +204,7 @@ cJSON *nfc_mifare_dictionary_attack(nfc_tag_info_t *tag, uint8_t sector_first, u
|
||||
attempts++;
|
||||
if (try_key_on_trailer(tag, trailer, trial, false)) {
|
||||
if (!add_sector_hit(hits, sec, trial, "A")) {
|
||||
cJSON_Delete(hits);
|
||||
cJSON_Delete(root);
|
||||
return NULL;
|
||||
}
|
||||
@@ -210,6 +213,7 @@ cJSON *nfc_mifare_dictionary_attack(nfc_tag_info_t *tag, uint8_t sector_first, u
|
||||
}
|
||||
if (try_key_on_trailer(tag, trailer, trial, true)) {
|
||||
if (!add_sector_hit(hits, sec, trial, "B")) {
|
||||
cJSON_Delete(hits);
|
||||
cJSON_Delete(root);
|
||||
return NULL;
|
||||
}
|
||||
@@ -226,6 +230,7 @@ cJSON *nfc_mifare_dictionary_attack(nfc_tag_info_t *tag, uint8_t sector_first, u
|
||||
if (!got) {
|
||||
cJSON *h = cJSON_CreateObject();
|
||||
if (!h) {
|
||||
cJSON_Delete(hits);
|
||||
cJSON_Delete(root);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
static const char *TAG = "nfc_engine";
|
||||
|
||||
static uint8_t s_tg = 1;
|
||||
static volatile bool s_pn532_ready = false;
|
||||
|
||||
bool nfc_engine_is_ready(void) { return s_pn532_ready; }
|
||||
|
||||
static void hint_type(nfc_tag_info_t *t)
|
||||
{
|
||||
@@ -18,7 +21,12 @@ static void hint_type(nfc_tag_info_t *t)
|
||||
t->type_hint = 1;
|
||||
break;
|
||||
case 0x00:
|
||||
t->type_hint = 2;
|
||||
/* Typical Type 2 inventory tuple is ATQA 0x0044 and 7-byte UID. */
|
||||
if ((t->atqa == 0x4400 || t->atqa == 0x0044) && t->uid_len == 7) {
|
||||
t->type_hint = 2;
|
||||
} else {
|
||||
t->type_hint = 0;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
t->type_hint = 0;
|
||||
@@ -41,6 +49,45 @@ bool nfc_tag_is_type2(const nfc_tag_info_t *tag)
|
||||
return tag && tag->type_hint == 2;
|
||||
}
|
||||
|
||||
int nfc_mifare_sector_count(const nfc_tag_info_t *tag)
|
||||
{
|
||||
if (!nfc_tag_is_mifare_classic(tag)) {
|
||||
return 0;
|
||||
}
|
||||
return nfc_tag_is_mifare_classic_4k(tag) ? 40 : 16;
|
||||
}
|
||||
|
||||
bool nfc_mifare_sector_layout(const nfc_tag_info_t *tag, int sector, int *first_block, int *num_blocks,
|
||||
uint8_t *trailer_block)
|
||||
{
|
||||
if (!nfc_tag_is_mifare_classic(tag) || !first_block || !num_blocks || !trailer_block) {
|
||||
return false;
|
||||
}
|
||||
if (!nfc_tag_is_mifare_classic_4k(tag)) {
|
||||
if (sector < 0 || sector > 15) {
|
||||
return false;
|
||||
}
|
||||
*first_block = sector * 4;
|
||||
*num_blocks = 4;
|
||||
*trailer_block = (uint8_t)(sector * 4 + 3);
|
||||
return true;
|
||||
}
|
||||
if (sector < 0 || sector > 39) {
|
||||
return false;
|
||||
}
|
||||
if (sector <= 31) {
|
||||
*first_block = sector * 4;
|
||||
*num_blocks = 4;
|
||||
*trailer_block = (uint8_t)(sector * 4 + 3);
|
||||
} else {
|
||||
int r = sector - 32;
|
||||
*first_block = 128 + r * 16;
|
||||
*num_blocks = 16;
|
||||
*trailer_block = (uint8_t)(128 + r * 16 + 15);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
esp_err_t nfc_engine_init(void)
|
||||
{
|
||||
esp_err_t e = pn532_core_init();
|
||||
@@ -58,9 +105,51 @@ esp_err_t nfc_engine_init(void)
|
||||
if (pn532_rf_max_retries() != ESP_OK) {
|
||||
ESP_LOGW(TAG, "RF max retries config failed");
|
||||
}
|
||||
s_pn532_ready = true;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
bool nfc_engine_try_init(void)
|
||||
{
|
||||
esp_err_t e = pn532_core_init();
|
||||
if (e != ESP_OK) {
|
||||
ESP_LOGW(TAG, "PN532 not found (%s) — AP running, will retry every 5 s", esp_err_to_name(e));
|
||||
s_pn532_ready = false;
|
||||
return false;
|
||||
}
|
||||
e = pn532_sam_config_normal();
|
||||
if (e != ESP_OK) {
|
||||
ESP_LOGW(TAG, "SAM config: %s", esp_err_to_name(e));
|
||||
}
|
||||
uint8_t ic = 0, hi = 0, lo = 0;
|
||||
if (pn532_get_firmware_version(&ic, &hi, &lo) == ESP_OK) {
|
||||
ESP_LOGI(TAG, "PN532 fw ic=0x%02x %u.%u", ic, hi, lo);
|
||||
}
|
||||
if (pn532_rf_max_retries() != ESP_OK) {
|
||||
ESP_LOGW(TAG, "RF max retries config failed");
|
||||
}
|
||||
s_pn532_ready = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool nfc_engine_try_reattach(void)
|
||||
{
|
||||
/* Transport already open — just ping the chip and re-apply configuration. */
|
||||
esp_err_t e = pn532_sam_config_normal();
|
||||
if (e != ESP_OK) {
|
||||
return false;
|
||||
}
|
||||
uint8_t ic = 0, hi = 0, lo = 0;
|
||||
if (pn532_get_firmware_version(&ic, &hi, &lo) == ESP_OK) {
|
||||
ESP_LOGI(TAG, "PN532 reattached ic=0x%02x %u.%u", ic, hi, lo);
|
||||
}
|
||||
if (pn532_rf_max_retries() != ESP_OK) {
|
||||
ESP_LOGW(TAG, "RF max retries config failed");
|
||||
}
|
||||
s_pn532_ready = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
esp_err_t nfc_poll_passive_target(nfc_tag_info_t *out)
|
||||
{
|
||||
if (!out) {
|
||||
@@ -248,6 +337,25 @@ esp_err_t nfc_ul_fast_read(uint8_t start_page, uint8_t *out, size_t out_max, siz
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t nfc_type2_get_version(uint8_t version[8])
|
||||
{
|
||||
if (!version) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
uint8_t d[] = {0x60};
|
||||
uint8_t resp[32];
|
||||
size_t rlen = 0;
|
||||
esp_err_t e = in_data_tg(d, sizeof(d), resp, sizeof(resp), &rlen);
|
||||
if (e != ESP_OK) {
|
||||
return e;
|
||||
}
|
||||
if (rlen < 2 + 8 || resp[0] != (uint8_t)(PN532_CMD_INDATAEXCHANGE + 1) || resp[1] != 0x00) {
|
||||
return ESP_ERR_INVALID_RESPONSE;
|
||||
}
|
||||
memcpy(version, resp + 2, 8);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
cJSON *nfc_tag_to_json(const nfc_tag_info_t *tag)
|
||||
{
|
||||
if (!tag || tag->uid_len > NFC_MAX_UID_LEN) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "nfc_engine/session_capture.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "esp_log.h"
|
||||
#include <string.h>
|
||||
|
||||
#define SESSION_CAPTURE_BYTES (48 * 1024)
|
||||
@@ -15,6 +16,10 @@ static SemaphoreHandle_t s_mu;
|
||||
void session_capture_init(void)
|
||||
{
|
||||
s_mu = xSemaphoreCreateMutex();
|
||||
if (!s_mu) {
|
||||
ESP_LOGE("session_capture", "mutex create failed — aborting");
|
||||
abort();
|
||||
}
|
||||
session_capture_clear();
|
||||
s_deep = false;
|
||||
}
|
||||
|
||||
@@ -57,6 +57,20 @@ esp_err_t pn532_in_communicate_thru(const uint8_t *data, size_t data_len,
|
||||
uint8_t *response, size_t response_max,
|
||||
size_t *response_len);
|
||||
|
||||
/** Enter PN532 target mode using TgInitAsTarget parameters (mode + target descriptors + optional GT). */
|
||||
esp_err_t pn532_tg_init_as_target(const uint8_t *params, size_t params_len,
|
||||
uint8_t *response, size_t response_max,
|
||||
size_t *response_len, int timeout_ms);
|
||||
|
||||
/** Receive bytes from the initiator while PN532 is in target mode. */
|
||||
esp_err_t pn532_tg_get_data(uint8_t *response, size_t response_max,
|
||||
size_t *response_len, int timeout_ms);
|
||||
|
||||
/** Send bytes back to the initiator while PN532 is in target mode. */
|
||||
esp_err_t pn532_tg_set_data(const uint8_t *data, size_t data_len,
|
||||
uint8_t *response, size_t response_max,
|
||||
size_t *response_len, int timeout_ms);
|
||||
|
||||
/** RF field on/off via RFConfiguration (0x32) item 0x01, RF field */
|
||||
esp_err_t pn532_rf_field(bool on);
|
||||
|
||||
|
||||
@@ -190,3 +190,81 @@ esp_err_t pn532_in_communicate_thru(const uint8_t *data, size_t data_len, uint8_
|
||||
*response_len = payload_len;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t pn532_tg_init_as_target(const uint8_t *params, size_t params_len, uint8_t *response,
|
||||
size_t response_max, size_t *response_len, int timeout_ms)
|
||||
{
|
||||
if (!params || !response || !response_len || params_len == 0 || params_len > PN532_EEPROM_MAX_CMD_PAYLOAD - 1) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
uint8_t buf[PN532_EEPROM_MAX_CMD_PAYLOAD];
|
||||
buf[0] = PN532_CMD_TGINITASTARGET;
|
||||
memcpy(buf + 1, params, params_len);
|
||||
uint8_t raw[PN532_EEPROM_MAX_CMD_PAYLOAD];
|
||||
size_t raw_len = 0;
|
||||
esp_err_t e = pn532_send_cmd(buf, 1 + params_len, raw, sizeof(raw), &raw_len, timeout_ms);
|
||||
if (e != ESP_OK) {
|
||||
return e;
|
||||
}
|
||||
if (raw_len < 2 || raw[0] != (uint8_t)(PN532_CMD_TGINITASTARGET + 1)) {
|
||||
return ESP_ERR_INVALID_RESPONSE;
|
||||
}
|
||||
size_t payload_len = raw_len - 1;
|
||||
if (payload_len > response_max) {
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
memcpy(response, raw + 1, payload_len);
|
||||
*response_len = payload_len;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t pn532_tg_get_data(uint8_t *response, size_t response_max, size_t *response_len, int timeout_ms)
|
||||
{
|
||||
if (!response || !response_len) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
uint8_t cmd = PN532_CMD_TGGETDATA;
|
||||
uint8_t raw[PN532_EEPROM_MAX_CMD_PAYLOAD];
|
||||
size_t raw_len = 0;
|
||||
esp_err_t e = pn532_send_cmd(&cmd, 1, raw, sizeof(raw), &raw_len, timeout_ms);
|
||||
if (e != ESP_OK) {
|
||||
return e;
|
||||
}
|
||||
if (raw_len < 2 || raw[0] != (uint8_t)(PN532_CMD_TGGETDATA + 1)) {
|
||||
return ESP_ERR_INVALID_RESPONSE;
|
||||
}
|
||||
size_t payload_len = raw_len - 1;
|
||||
if (payload_len > response_max) {
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
memcpy(response, raw + 1, payload_len);
|
||||
*response_len = payload_len;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t pn532_tg_set_data(const uint8_t *data, size_t data_len, uint8_t *response, size_t response_max,
|
||||
size_t *response_len, int timeout_ms)
|
||||
{
|
||||
if (!data || !response || !response_len || data_len == 0 || data_len > PN532_EEPROM_MAX_CMD_PAYLOAD - 1) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
uint8_t buf[PN532_EEPROM_MAX_CMD_PAYLOAD];
|
||||
buf[0] = PN532_CMD_TGSETDATA;
|
||||
memcpy(buf + 1, data, data_len);
|
||||
uint8_t raw[PN532_EEPROM_MAX_CMD_PAYLOAD];
|
||||
size_t raw_len = 0;
|
||||
esp_err_t e = pn532_send_cmd(buf, 1 + data_len, raw, sizeof(raw), &raw_len, timeout_ms);
|
||||
if (e != ESP_OK) {
|
||||
return e;
|
||||
}
|
||||
if (raw_len < 2 || raw[0] != (uint8_t)(PN532_CMD_TGSETDATA + 1)) {
|
||||
return ESP_ERR_INVALID_RESPONSE;
|
||||
}
|
||||
size_t payload_len = raw_len - 1;
|
||||
if (payload_len > response_max) {
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
memcpy(response, raw + 1, payload_len);
|
||||
*response_len = payload_len;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
@@ -352,7 +352,7 @@ esp_err_t pn532_transport_exchange(const uint8_t *tx_body, size_t tx_body_len, u
|
||||
#if defined(CONFIG_PN532_TRANSPORT_SPI)
|
||||
ESP_RETURN_ON_ERROR(spi_write_frame(frame, pos), TAG, "spi wr");
|
||||
#elif defined(CONFIG_PN532_TRANSPORT_I2C)
|
||||
i2c_wakeup();
|
||||
ESP_RETURN_ON_ERROR(i2c_wakeup(), TAG, "i2c wake");
|
||||
ESP_RETURN_ON_ERROR(i2c_write_raw(frame, pos), TAG, "i2c wr");
|
||||
#elif defined(CONFIG_PN532_TRANSPORT_HSU)
|
||||
ESP_RETURN_ON_ERROR(hsu_write_raw(frame, pos), TAG, "hsu wr");
|
||||
|
||||
1
firmware/data/assets/index-Bb0xTRCn.css
Normal file
1
firmware/data/assets/index-Bb0xTRCn.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -11,8 +11,8 @@
|
||||
href="https://fonts.googleapis.com/css2?family=Audiowide&family=JetBrains+Mono:ital,wght@0,400;0,600;0,700;1,400&family=Orbitron:wght@500;600;700;800&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script type="module" crossorigin src="/assets/index-9oJp152C.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-hoMg1Qkq.css">
|
||||
<script type="module" crossorigin src="/assets/index-BmIGATlK.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Bb0xTRCn.css">
|
||||
</head>
|
||||
<body class="bg-bubble-950 text-slate-200 antialiased selection:bg-bubble-accent/40 selection:text-bubble-950">
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "esp_log.h"
|
||||
#include "esp_ota_ops.h"
|
||||
#include "board_rgb_off.h"
|
||||
#include "nfc_engine/nfc_engine.h"
|
||||
#include "nfc_engine/session_capture.h"
|
||||
@@ -8,10 +9,15 @@ static const char *TAG = "main";
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
(void)esp_ota_mark_app_valid_cancel_rollback();
|
||||
board_rgb_led_quiet();
|
||||
ESP_LOGI(TAG, "PN532 NFC Toolkit starting");
|
||||
ESP_ERROR_CHECK(nfc_engine_init());
|
||||
nfc_engine_try_init(); /* non-fatal: logs warning if PN532 absent, AP starts regardless */
|
||||
session_capture_init();
|
||||
ESP_ERROR_CHECK(app_net_init());
|
||||
ESP_LOGI(TAG, "Open AP SSID PN532-Toolkit — http://192.168.4.1");
|
||||
if (nfc_engine_is_ready()) {
|
||||
ESP_LOGI(TAG, "PN532 ready · AP SSID PN532-Toolkit → http://192.168.4.1");
|
||||
} else {
|
||||
ESP_LOGW(TAG, "PN532 not found at boot — AP running, retrying · http://192.168.4.1");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,3 +31,6 @@ CONFIG_LOG_MAXIMUM_LEVEL_VERBOSE=y
|
||||
|
||||
# mDNS
|
||||
CONFIG_MDNS_MAX_SERVICES=10
|
||||
|
||||
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y
|
||||
CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y
|
||||
|
||||
@@ -3,7 +3,7 @@ import { NavLink, Route, Routes } from "react-router-dom";
|
||||
import BrowserLogBar from "./BrowserLogBar";
|
||||
import FlashBackdrop from "./FlashBackdrop";
|
||||
import ScanCashFlourish from "./ScanCashFlourish";
|
||||
import { NfcWsProvider } from "./NfcWsContext";
|
||||
import { NfcWsProvider, useNfcWs } from "./NfcWsContext";
|
||||
import { ToastHost } from "./toast";
|
||||
import Dashboard from "./pages/Dashboard";
|
||||
import ReadAnalyze from "./pages/ReadAnalyze";
|
||||
@@ -17,6 +17,29 @@ import Brute from "./pages/Brute";
|
||||
import Emulate from "./pages/Emulate";
|
||||
import KeyLab from "./pages/KeyLab";
|
||||
|
||||
function HeaderBadge() {
|
||||
const { wsOk, pn532Connected } = useNfcWs();
|
||||
return (
|
||||
<span className="hidden font-mono text-[9px] text-bubble-mint/40 md:inline lg:text-[10px]">
|
||||
<span className={wsOk ? "text-bubble-accent/90" : "animate-pulse text-bubble-rose/60"}>●</span>{" "}
|
||||
RF_STACK{" "}
|
||||
<span
|
||||
className={
|
||||
!wsOk
|
||||
? "animate-pulse text-bubble-rose/80"
|
||||
: pn532Connected
|
||||
? "font-bold text-bubble-mint"
|
||||
: "animate-pulse text-amber-400"
|
||||
}
|
||||
>
|
||||
{!wsOk ? "WAIT" : pn532Connected ? "LIVE" : "NO RF"}
|
||||
</span>
|
||||
<span className="mx-1.5 text-bubble-mint/25">│</span>
|
||||
<span className="text-bubble-mint/50">ws://stream</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const nav = [
|
||||
["/", "Dash"],
|
||||
["/capture", "Read-all"],
|
||||
@@ -70,12 +93,7 @@ export default function App() {
|
||||
MAXIMAL
|
||||
</span>
|
||||
</motion.div>
|
||||
<span className="hidden font-mono text-[9px] text-bubble-mint/40 md:inline lg:text-[10px]">
|
||||
<span className="text-bubble-accent/90">●</span> RF_STACK{" "}
|
||||
<span className="text-bubble-rose/80">LIVE</span>
|
||||
<span className="mx-1.5 text-bubble-mint/25">│</span>
|
||||
<span className="text-bubble-mint/50">ws://stream</span>
|
||||
</span>
|
||||
<HeaderBadge />
|
||||
</div>
|
||||
<nav className="flex max-w-full flex-wrap justify-end gap-1 text-[10px] font-mono sm:gap-1.5 sm:text-[11px]">
|
||||
{nav.map(([to, label]) => (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { createContext, useCallback, useContext, useMemo, useRef, useState } from "react";
|
||||
import type { Tag } from "./api";
|
||||
import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { apiGet, type Status, type Tag } from "./api";
|
||||
import { useToolkitWs } from "./useWebSocket";
|
||||
|
||||
export type BrowserLogEntry = {
|
||||
@@ -12,6 +12,7 @@ export type CashVariant = "tag" | "vault";
|
||||
|
||||
type NfcCtx = {
|
||||
wsOk: boolean;
|
||||
pn532Connected: boolean;
|
||||
lastTag: Tag | null;
|
||||
tagPresent: boolean;
|
||||
log: BrowserLogEntry[];
|
||||
@@ -89,6 +90,7 @@ function isCaptureRecorded(payload: unknown): boolean {
|
||||
export function NfcWsProvider({ children }: { children: React.ReactNode }) {
|
||||
const [lastTag, setLastTag] = useState<Tag | null>(null);
|
||||
const [tagPresent, setTagPresent] = useState(false);
|
||||
const [pn532Connected, setPn532Connected] = useState(false);
|
||||
const [log, setLog] = useState<BrowserLogEntry[]>(() => loadPersisted());
|
||||
const [cashWave, setCashWave] = useState(0);
|
||||
const [cashVariant, setCashVariant] = useState<CashVariant>("tag");
|
||||
@@ -121,6 +123,11 @@ export function NfcWsProvider({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
} else if (ch === "capture" && isCaptureRecorded(o.payload)) {
|
||||
bump("vault");
|
||||
} else if (ch === "pn532") {
|
||||
const p = o.payload as { connected?: boolean } | undefined;
|
||||
if (typeof p?.connected === "boolean") {
|
||||
setPn532Connected(p.connected);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
@@ -131,6 +138,19 @@ export function NfcWsProvider({ children }: { children: React.ReactNode }) {
|
||||
|
||||
const wsOk = useToolkitWs(onMsg);
|
||||
|
||||
/* Fetch initial PN532 state as soon as WS connects (avoids waiting for first broadcast). */
|
||||
useEffect(() => {
|
||||
if (wsOk) {
|
||||
apiGet<Status>("/api/status")
|
||||
.then((s) => {
|
||||
if (typeof s.pn532Connected === "boolean") {
|
||||
setPn532Connected(s.pn532Connected);
|
||||
}
|
||||
})
|
||||
.catch(() => { /* silently ignore — badge will update on next WS event */ });
|
||||
}
|
||||
}, [wsOk]);
|
||||
|
||||
const applyScanPoll = useCallback(
|
||||
(present: boolean, tag?: Tag) => {
|
||||
if (!present) {
|
||||
@@ -165,6 +185,7 @@ export function NfcWsProvider({ children }: { children: React.ReactNode }) {
|
||||
const v = useMemo(
|
||||
() => ({
|
||||
wsOk,
|
||||
pn532Connected,
|
||||
lastTag,
|
||||
tagPresent,
|
||||
log,
|
||||
@@ -174,7 +195,7 @@ export function NfcWsProvider({ children }: { children: React.ReactNode }) {
|
||||
exportBrowserLog,
|
||||
applyScanPoll,
|
||||
}),
|
||||
[wsOk, lastTag, tagPresent, log, cashWave, cashVariant, clearBrowserLog, exportBrowserLog, applyScanPoll],
|
||||
[wsOk, pn532Connected, lastTag, tagPresent, log, cashWave, cashVariant, clearBrowserLog, exportBrowserLog, applyScanPoll],
|
||||
);
|
||||
|
||||
return <Ctx.Provider value={v}>{children}</Ctx.Provider>;
|
||||
|
||||
@@ -42,6 +42,7 @@ export type Status = {
|
||||
uptimeMs: number;
|
||||
freeHeap: number;
|
||||
wifiMode: number;
|
||||
pn532Connected: boolean;
|
||||
pn532?: { ic: number; fwHi: number; fwLo: number };
|
||||
scanning: boolean;
|
||||
session?: SessionInfo;
|
||||
|
||||
@@ -19,7 +19,14 @@ export default function Brute() {
|
||||
const keysHex = extraKeys
|
||||
.split(/\r?\n/)
|
||||
.map((l) => l.replace(/\s/g, "").toUpperCase())
|
||||
.filter((l) => l.length === 12);
|
||||
.filter((l) => l.length === 12 && /^[0-9A-F]+$/.test(l));
|
||||
const invalidCount = extraKeys
|
||||
.split(/\r?\n/)
|
||||
.map((l) => l.replace(/\s/g, ""))
|
||||
.filter((l) => l.length > 0 && (l.length !== 12 || !/^[0-9A-Fa-f]+$/.test(l))).length;
|
||||
if (invalidCount > 0) {
|
||||
toast(`${invalidCount} line(s) not valid 12-hex keys — skipped, firmware built-ins still run`);
|
||||
}
|
||||
const body = {
|
||||
readerType: reader,
|
||||
variations,
|
||||
|
||||
@@ -12,8 +12,8 @@ export default function Capture() {
|
||||
const refresh = useCallback(() => {
|
||||
apiGet<Status>("/api/status")
|
||||
.then(setSt)
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
.catch(() => toast("Status unreachable", "err"));
|
||||
}, [toast]);
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
|
||||
@@ -152,9 +152,22 @@ export default function Dashboard() {
|
||||
</li>
|
||||
<li className="flex justify-between">
|
||||
<span className="text-slate-500">PN532</span>
|
||||
<span className="text-bubble-accent">
|
||||
{st.pn532 ? `IC${st.pn532.ic} v${st.pn532.fwHi}.${st.pn532.fwLo}` : "n/a"}
|
||||
</span>
|
||||
{st.pn532Connected ? (
|
||||
<span className="flex items-center gap-2">
|
||||
{st.pn532 && (
|
||||
<span className="text-[10px] text-slate-500">
|
||||
IC{st.pn532.ic} v{st.pn532.fwHi}.{st.pn532.fwLo}
|
||||
</span>
|
||||
)}
|
||||
<span className="rounded px-1.5 py-0.5 bg-bubble-mint/20 text-bubble-mint font-bold text-[10px] tracking-widest">
|
||||
READY
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="animate-pulse rounded px-1.5 py-0.5 bg-amber-500/20 text-amber-400 font-bold text-[10px] tracking-widest">
|
||||
SEARCHING
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
) : (
|
||||
|
||||
@@ -11,13 +11,26 @@ export default function Emulate() {
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const send = async () => {
|
||||
const cleanHex = hex.replace(/\s/g, "");
|
||||
if (!cleanHex) {
|
||||
toast("Hex payload is empty", "err");
|
||||
return;
|
||||
}
|
||||
if (cleanHex.length % 2 !== 0) {
|
||||
toast("Hex must have an even number of characters", "err");
|
||||
return;
|
||||
}
|
||||
if (!/^[0-9A-Fa-f]+$/.test(cleanHex)) {
|
||||
toast("Hex must contain only 0-9 A-F characters", "err");
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
setOut("");
|
||||
try {
|
||||
const r = await fetch(apiUrl("/api/nfc/emulate-raw"), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ hex: hex.replace(/\s/g, "") }),
|
||||
body: JSON.stringify({ hex: cleanHex }),
|
||||
});
|
||||
const t = await r.text();
|
||||
if (!r.ok) {
|
||||
|
||||
@@ -19,6 +19,8 @@ export default function RawConsole() {
|
||||
push(`RX ${j.response || j.error || "?"}`);
|
||||
if (j.response) {
|
||||
toast("Frame OK");
|
||||
} else if (j.error) {
|
||||
toast(j.error, "err");
|
||||
}
|
||||
} catch (e) {
|
||||
push(`ERR ${String(e)}`);
|
||||
|
||||
@@ -31,10 +31,12 @@ export default function ReadAnalyze() {
|
||||
|
||||
const readUl = async () => {
|
||||
try {
|
||||
const j = await apiPost<{ data?: string }>("/api/ul/read-page", { page: block });
|
||||
const j = await apiPost<{ data?: string; error?: string }>("/api/ul/read-page", { page: block });
|
||||
if (j.data) {
|
||||
setHex(j.data + " (UL page)");
|
||||
toast("UL read OK");
|
||||
} else {
|
||||
toast(j.error ?? "no data returned", "err");
|
||||
}
|
||||
} catch (e) {
|
||||
toast(String(e), "err");
|
||||
|
||||
@@ -17,6 +17,7 @@ export default function WriteClone() {
|
||||
const [labBlob, setLabBlob] = useState<BinaryCardFixture | null>(null);
|
||||
const [ulPage, setUlPage] = useState(4);
|
||||
const [ulData, setUlData] = useState("00000000");
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const write = async () => {
|
||||
const cleanKey = key.replace(/\s/g, "");
|
||||
@@ -36,11 +37,14 @@ export default function WriteClone() {
|
||||
if (!confirm("Write will modify tag memory. Continue?")) {
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
try {
|
||||
await apiPost("/api/mifare/write-block", { block, key: cleanKey, keyB, data: cleanData });
|
||||
toast("Write OK");
|
||||
} catch (e) {
|
||||
toast(String(e), "err");
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -53,11 +57,14 @@ export default function WriteClone() {
|
||||
if (!confirm("Ultralight page write — can brick OTP/lock bytes if misused. Continue?")) {
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
try {
|
||||
await apiPost("/api/ul/write-page", { page: ulPage, data: h });
|
||||
toast("UL page write OK");
|
||||
} catch (e) {
|
||||
toast(String(e), "err");
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -130,9 +137,10 @@ export default function WriteClone() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={write}
|
||||
className="rounded-2xl bg-gradient-to-r from-bubble-rose to-orange-400 px-6 py-3 font-bold text-white shadow-glow"
|
||||
disabled={busy}
|
||||
className="rounded-2xl bg-gradient-to-r from-bubble-rose to-orange-400 px-6 py-3 font-bold text-white shadow-glow disabled:opacity-50"
|
||||
>
|
||||
Write block
|
||||
{busy ? "Writing…" : "Write block"}
|
||||
</button>
|
||||
|
||||
<div className="border-t border-white/10 pt-8">
|
||||
@@ -162,9 +170,10 @@ export default function WriteClone() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={writeUl}
|
||||
className="mt-4 rounded-2xl border border-bubble-accent/50 bg-bubble-accent/20 px-6 py-3 font-bold text-bubble-accent"
|
||||
disabled={busy}
|
||||
className="mt-4 rounded-2xl border border-bubble-accent/50 bg-bubble-accent/20 px-6 py-3 font-bold text-bubble-accent disabled:opacity-50"
|
||||
>
|
||||
Write UL page
|
||||
{busy ? "Writing…" : "Write UL page"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@ const Ctx = createContext<(msg: string, kind?: Toast["kind"]) => void>(() => {})
|
||||
export function ToastHost({ children }: { children: React.ReactNode }) {
|
||||
const [list, setList] = useState<Toast[]>([]);
|
||||
const push = useCallback((msg: string, kind: Toast["kind"] = "info") => {
|
||||
const id = Date.now();
|
||||
const id = Date.now() + Math.random();
|
||||
setList((x) => [...x, { id, msg, kind }]);
|
||||
setTimeout(() => setList((x) => x.filter((t) => t.id !== id)), 4200);
|
||||
}, []);
|
||||
|
||||
Reference in New Issue
Block a user