Compare commits
1 Commits
docs/readm
...
d7cd79b7c7
| Author | SHA1 | Date | |
|---|---|---|---|
| d7cd79b7c7 |
27
README.md
27
README.md
@@ -13,7 +13,7 @@ ESP32-S3 + PN532 + a serious web UI — no desktop app, no dongle software, no m
|
|||||||
|
|
||||||
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.
|
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 **SoftAP**: SSID **`PN532-Toolkit`**, **open network** (no password, lab default). **mDNS** hostname **`pn532tool.local`** (HTTP port **80**). No cloud, no account, no telemetry.
|
4. **Self-hosted on the device** — Default **SoftAP**: SSID **`PN532-Toolkit`**, password **`nfc-toolkit`** (WPA2). **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`), **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.
|
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.
|
||||||
|
|
||||||
@@ -38,12 +38,9 @@ This stack is **not** a Proxmark replacement (no LF, no raw carrier manipulation
|
|||||||
| **Session RAM** | **~48 KB** NDJSON capture buffer; mutex-protected; **full** flag stops polling until you clear. |
|
| **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). |
|
| **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. |
|
| **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. |
|
| **OTA endpoint** | **`POST /api/ota`** returns **501** with a stub message — there is **no** in-field OTA in this repo yet; use **`idf.py flash`** (or the included scripts). |
|
||||||
| **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. |
|
| **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. |
|
| **Status** | **`/api/status`**: uptime, free heap, Wi‑Fi mode, **PN532 firmware version** (when reachable), scan flag, 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)
|
### Web UI (each page)
|
||||||
|
|
||||||
@@ -92,24 +89,16 @@ This stack is **not** a Proxmark replacement (no LF, no raw carrier manipulation
|
|||||||
| Method | Path | Purpose |
|
| Method | Path | Purpose |
|
||||||
|--------|------|---------|
|
|--------|------|---------|
|
||||||
| `POST` | `/api/nfc/poll` | One-shot poll; body may be `{}` — returns `present`, optional `tag` |
|
| `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 |
|
| `POST` | `/api/nfc/scan` | Body `{"enable": true}` or `false` — continuous background scan |
|
||||||
| `GET` | `/api/pn532/general-status` | Raw PN532 **general status** byte array |
|
| `GET` | `/api/pn532/general-status` | Raw PN532 **general status** byte array |
|
||||||
| `POST` | `/api/mifare/read-block` | `block`, `key` (12 hex), `keyB` |
|
| `POST` | `/api/mifare/read-block` | `block`, `key` (12 hex), `keyB` |
|
||||||
| `POST` | `/api/mifare/write-block` | `block`, `key`, `keyB`, `data` (32 hex) |
|
| `POST` | `/api/mifare/write-block` | `block`, `key`, `keyB`, `data` (32 hex) |
|
||||||
| `POST` | `/api/ul/read-page` | `page` |
|
| `POST` | `/api/ul/read-page` | `page` |
|
||||||
| `POST` | `/api/ul/write-page` | `page`, `data` (8 hex) |
|
| `POST` | `/api/ul/write-page` | `page`, `data` (8 hex) |
|
||||||
| `POST` | `/api/raw/pn532` | `frame` — PN532 command bytes (not a full transport frame) |
|
| `POST` | `/api/raw/pn532` | `frame` — hex command bytes |
|
||||||
| `POST` | `/api/mifare/dictionary-attack` | `readerType` (`classic1k` / `classic4k`), `variations`, optional `keysHex[]`, optional `sectorFirst` / `sectorLast` |
|
| `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/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` |
|
| `POST` | `/api/ota` | **Not implemented** — returns **501** |
|
||||||
|
|
||||||
**WebSocket:** `GET /ws` — JSON text frames as above; send **`ping`**, receive **`pong`**.
|
**WebSocket:** `GET /ws` — JSON text frames as above; send **`ping`**, receive **`pong`**.
|
||||||
|
|
||||||
@@ -123,7 +112,7 @@ The SPA keeps the **last 5 000** WebSocket events in **sessionStorage**. **Dow
|
|||||||
|
|
||||||
### Card emulation (PN532 target mode)
|
### Card emulation (PN532 target mode)
|
||||||
|
|
||||||
**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.
|
**Emulate** sends raw **`TgInitAsTarget` (0x8C)** payloads via **`POST /api/nfc/emulate-raw`**. You own the bytes (see NXP **UM0701**). This is **experimental**; a bad sequence can leave the front end wedged until **power cycle**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -156,10 +145,6 @@ idf.py menuconfig # PN532 Host: SPI / I2C / UART + pins; Board indicators (RGB
|
|||||||
idf.py build flash monitor
|
idf.py build flash monitor
|
||||||
```
|
```
|
||||||
|
|
||||||
### 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.
|
|
||||||
|
|
||||||
From `firmware/`, with IDF already in your environment, you can also **`./flash.sh`** (uses **`ESPPORT`** or the first argument as the serial device).
|
From `firmware/`, with IDF already in your environment, you can also **`./flash.sh`** (uses **`ESPPORT`** or the first argument as the serial device).
|
||||||
|
|
||||||
See [docs/FLASHING.md](docs/FLASHING.md) and [docs/PINOUT.md](docs/PINOUT.md). Limitations and scope notes: [docs/LIMITATIONS.md](docs/LIMITATIONS.md).
|
See [docs/FLASHING.md](docs/FLASHING.md) and [docs/PINOUT.md](docs/PINOUT.md). Limitations and scope notes: [docs/LIMITATIONS.md](docs/LIMITATIONS.md).
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
The PN532 is a **hosted NFC controller**, not a low-level RF lab instrument. This project exposes PN532 features honestly:
|
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.
|
- **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 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.
|
- **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.”
|
||||||
- **Key recovery**: on-device “brute force” at Proxmark scale is **infeasible**. The UI provides **dictionary / manual** key workflows.
|
- **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.
|
- **Signal / RF metrics**: diagnostics use **PN532 status / timings / retries**, not calibrated dBm.
|
||||||
- **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.
|
- **OTA via UI**: `POST /api/ota` is a **stub** (`501`) — ship OTA with `esp_https_ota` + signed images when you need production updates.
|
||||||
|
|||||||
@@ -20,13 +20,3 @@ The firmware Kconfig ships **example** GPIOs:
|
|||||||
## Transport selection
|
## 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**).
|
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.
|
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ idf_component_register(
|
|||||||
SRCS "app_net.c"
|
SRCS "app_net.c"
|
||||||
INCLUDE_DIRS "include"
|
INCLUDE_DIRS "include"
|
||||||
REQUIRES esp_http_server http_parser esp_wifi esp_netif nvs_flash mdns esp_timer
|
REQUIRES esp_http_server http_parser esp_wifi esp_netif nvs_flash mdns esp_timer
|
||||||
json spiffs vfs freertos nfc_engine pn532_host esp_https_ota app_update mbedtls
|
json spiffs vfs freertos nfc_engine pn532_host
|
||||||
)
|
)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -26,26 +26,12 @@ typedef struct {
|
|||||||
bool key_b;
|
bool key_b;
|
||||||
} nfc_mifare_key_t;
|
} 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);
|
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);
|
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(const nfc_tag_info_t *tag);
|
||||||
bool nfc_tag_is_mifare_classic_4k(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);
|
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,
|
esp_err_t nfc_mifare_authenticate_block(const nfc_tag_info_t *tag, uint8_t block_no,
|
||||||
const nfc_mifare_key_t *key);
|
const nfc_mifare_key_t *key);
|
||||||
@@ -56,7 +42,6 @@ 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_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_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). */
|
/** Build JSON snapshot of last seen tag + optional blocks (caller frees cJSON). */
|
||||||
cJSON *nfc_tag_to_json(const nfc_tag_info_t *tag);
|
cJSON *nfc_tag_to_json(const nfc_tag_info_t *tag);
|
||||||
|
|||||||
@@ -170,7 +170,6 @@ cJSON *nfc_mifare_dictionary_attack(nfc_tag_info_t *tag, uint8_t sector_first, u
|
|||||||
attempts++;
|
attempts++;
|
||||||
if (try_key_on_trailer(tag, trailer, trial, false)) {
|
if (try_key_on_trailer(tag, trailer, trial, false)) {
|
||||||
if (!add_sector_hit(hits, sec, trial, "A")) {
|
if (!add_sector_hit(hits, sec, trial, "A")) {
|
||||||
cJSON_Delete(hits);
|
|
||||||
cJSON_Delete(root);
|
cJSON_Delete(root);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
@@ -179,7 +178,6 @@ 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 (try_key_on_trailer(tag, trailer, trial, true)) {
|
||||||
if (!add_sector_hit(hits, sec, trial, "B")) {
|
if (!add_sector_hit(hits, sec, trial, "B")) {
|
||||||
cJSON_Delete(hits);
|
|
||||||
cJSON_Delete(root);
|
cJSON_Delete(root);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
@@ -204,7 +202,6 @@ cJSON *nfc_mifare_dictionary_attack(nfc_tag_info_t *tag, uint8_t sector_first, u
|
|||||||
attempts++;
|
attempts++;
|
||||||
if (try_key_on_trailer(tag, trailer, trial, false)) {
|
if (try_key_on_trailer(tag, trailer, trial, false)) {
|
||||||
if (!add_sector_hit(hits, sec, trial, "A")) {
|
if (!add_sector_hit(hits, sec, trial, "A")) {
|
||||||
cJSON_Delete(hits);
|
|
||||||
cJSON_Delete(root);
|
cJSON_Delete(root);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
@@ -213,7 +210,6 @@ 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 (try_key_on_trailer(tag, trailer, trial, true)) {
|
||||||
if (!add_sector_hit(hits, sec, trial, "B")) {
|
if (!add_sector_hit(hits, sec, trial, "B")) {
|
||||||
cJSON_Delete(hits);
|
|
||||||
cJSON_Delete(root);
|
cJSON_Delete(root);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
@@ -230,7 +226,6 @@ cJSON *nfc_mifare_dictionary_attack(nfc_tag_info_t *tag, uint8_t sector_first, u
|
|||||||
if (!got) {
|
if (!got) {
|
||||||
cJSON *h = cJSON_CreateObject();
|
cJSON *h = cJSON_CreateObject();
|
||||||
if (!h) {
|
if (!h) {
|
||||||
cJSON_Delete(hits);
|
|
||||||
cJSON_Delete(root);
|
cJSON_Delete(root);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,6 @@
|
|||||||
static const char *TAG = "nfc_engine";
|
static const char *TAG = "nfc_engine";
|
||||||
|
|
||||||
static uint8_t s_tg = 1;
|
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)
|
static void hint_type(nfc_tag_info_t *t)
|
||||||
{
|
{
|
||||||
@@ -21,12 +18,7 @@ static void hint_type(nfc_tag_info_t *t)
|
|||||||
t->type_hint = 1;
|
t->type_hint = 1;
|
||||||
break;
|
break;
|
||||||
case 0x00:
|
case 0x00:
|
||||||
/* Typical Type 2 inventory tuple is ATQA 0x0044 and 7-byte UID. */
|
t->type_hint = 2;
|
||||||
if ((t->atqa == 0x4400 || t->atqa == 0x0044) && t->uid_len == 7) {
|
|
||||||
t->type_hint = 2;
|
|
||||||
} else {
|
|
||||||
t->type_hint = 0;
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
t->type_hint = 0;
|
t->type_hint = 0;
|
||||||
@@ -49,45 +41,6 @@ bool nfc_tag_is_type2(const nfc_tag_info_t *tag)
|
|||||||
return tag && tag->type_hint == 2;
|
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 nfc_engine_init(void)
|
||||||
{
|
{
|
||||||
esp_err_t e = pn532_core_init();
|
esp_err_t e = pn532_core_init();
|
||||||
@@ -105,51 +58,9 @@ esp_err_t nfc_engine_init(void)
|
|||||||
if (pn532_rf_max_retries() != ESP_OK) {
|
if (pn532_rf_max_retries() != ESP_OK) {
|
||||||
ESP_LOGW(TAG, "RF max retries config failed");
|
ESP_LOGW(TAG, "RF max retries config failed");
|
||||||
}
|
}
|
||||||
s_pn532_ready = true;
|
|
||||||
return ESP_OK;
|
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)
|
esp_err_t nfc_poll_passive_target(nfc_tag_info_t *out)
|
||||||
{
|
{
|
||||||
if (!out) {
|
if (!out) {
|
||||||
@@ -337,25 +248,6 @@ esp_err_t nfc_ul_fast_read(uint8_t start_page, uint8_t *out, size_t out_max, siz
|
|||||||
return ESP_OK;
|
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)
|
cJSON *nfc_tag_to_json(const nfc_tag_info_t *tag)
|
||||||
{
|
{
|
||||||
if (!tag || tag->uid_len > NFC_MAX_UID_LEN) {
|
if (!tag || tag->uid_len > NFC_MAX_UID_LEN) {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
#include "nfc_engine/session_capture.h"
|
#include "nfc_engine/session_capture.h"
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/semphr.h"
|
#include "freertos/semphr.h"
|
||||||
#include "esp_log.h"
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#define SESSION_CAPTURE_BYTES (48 * 1024)
|
#define SESSION_CAPTURE_BYTES (48 * 1024)
|
||||||
@@ -16,10 +15,6 @@ static SemaphoreHandle_t s_mu;
|
|||||||
void session_capture_init(void)
|
void session_capture_init(void)
|
||||||
{
|
{
|
||||||
s_mu = xSemaphoreCreateMutex();
|
s_mu = xSemaphoreCreateMutex();
|
||||||
if (!s_mu) {
|
|
||||||
ESP_LOGE("session_capture", "mutex create failed — aborting");
|
|
||||||
abort();
|
|
||||||
}
|
|
||||||
session_capture_clear();
|
session_capture_clear();
|
||||||
s_deep = false;
|
s_deep = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,20 +57,6 @@ esp_err_t pn532_in_communicate_thru(const uint8_t *data, size_t data_len,
|
|||||||
uint8_t *response, size_t response_max,
|
uint8_t *response, size_t response_max,
|
||||||
size_t *response_len);
|
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 */
|
/** RF field on/off via RFConfiguration (0x32) item 0x01, RF field */
|
||||||
esp_err_t pn532_rf_field(bool on);
|
esp_err_t pn532_rf_field(bool on);
|
||||||
|
|
||||||
|
|||||||
@@ -190,81 +190,3 @@ esp_err_t pn532_in_communicate_thru(const uint8_t *data, size_t data_len, uint8_
|
|||||||
*response_len = payload_len;
|
*response_len = payload_len;
|
||||||
return ESP_OK;
|
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)
|
#if defined(CONFIG_PN532_TRANSPORT_SPI)
|
||||||
ESP_RETURN_ON_ERROR(spi_write_frame(frame, pos), TAG, "spi wr");
|
ESP_RETURN_ON_ERROR(spi_write_frame(frame, pos), TAG, "spi wr");
|
||||||
#elif defined(CONFIG_PN532_TRANSPORT_I2C)
|
#elif defined(CONFIG_PN532_TRANSPORT_I2C)
|
||||||
ESP_RETURN_ON_ERROR(i2c_wakeup(), TAG, "i2c wake");
|
i2c_wakeup();
|
||||||
ESP_RETURN_ON_ERROR(i2c_write_raw(frame, pos), TAG, "i2c wr");
|
ESP_RETURN_ON_ERROR(i2c_write_raw(frame, pos), TAG, "i2c wr");
|
||||||
#elif defined(CONFIG_PN532_TRANSPORT_HSU)
|
#elif defined(CONFIG_PN532_TRANSPORT_HSU)
|
||||||
ESP_RETURN_ON_ERROR(hsu_write_raw(frame, pos), TAG, "hsu wr");
|
ESP_RETURN_ON_ERROR(hsu_write_raw(frame, pos), TAG, "hsu wr");
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
firmware/data/assets/index-hoMg1Qkq.css
Normal file
1
firmware/data/assets/index-hoMg1Qkq.css
Normal file
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"
|
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"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
<script type="module" crossorigin src="/assets/index-BmIGATlK.js"></script>
|
<script type="module" crossorigin src="/assets/index-9oJp152C.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-Bb0xTRCn.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-hoMg1Qkq.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-bubble-950 text-slate-200 antialiased selection:bg-bubble-accent/40 selection:text-bubble-950">
|
<body class="bg-bubble-950 text-slate-200 antialiased selection:bg-bubble-accent/40 selection:text-bubble-950">
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "esp_ota_ops.h"
|
|
||||||
#include "board_rgb_off.h"
|
#include "board_rgb_off.h"
|
||||||
#include "nfc_engine/nfc_engine.h"
|
#include "nfc_engine/nfc_engine.h"
|
||||||
#include "nfc_engine/session_capture.h"
|
#include "nfc_engine/session_capture.h"
|
||||||
@@ -9,15 +8,10 @@ static const char *TAG = "main";
|
|||||||
|
|
||||||
void app_main(void)
|
void app_main(void)
|
||||||
{
|
{
|
||||||
(void)esp_ota_mark_app_valid_cancel_rollback();
|
|
||||||
board_rgb_led_quiet();
|
board_rgb_led_quiet();
|
||||||
ESP_LOGI(TAG, "PN532 NFC Toolkit starting");
|
ESP_LOGI(TAG, "PN532 NFC Toolkit starting");
|
||||||
nfc_engine_try_init(); /* non-fatal: logs warning if PN532 absent, AP starts regardless */
|
ESP_ERROR_CHECK(nfc_engine_init());
|
||||||
session_capture_init();
|
session_capture_init();
|
||||||
ESP_ERROR_CHECK(app_net_init());
|
ESP_ERROR_CHECK(app_net_init());
|
||||||
if (nfc_engine_is_ready()) {
|
ESP_LOGI(TAG, "Open AP SSID PN532-Toolkit — http://192.168.4.1");
|
||||||
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,6 +31,3 @@ CONFIG_LOG_MAXIMUM_LEVEL_VERBOSE=y
|
|||||||
|
|
||||||
# mDNS
|
# mDNS
|
||||||
CONFIG_MDNS_MAX_SERVICES=10
|
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 BrowserLogBar from "./BrowserLogBar";
|
||||||
import FlashBackdrop from "./FlashBackdrop";
|
import FlashBackdrop from "./FlashBackdrop";
|
||||||
import ScanCashFlourish from "./ScanCashFlourish";
|
import ScanCashFlourish from "./ScanCashFlourish";
|
||||||
import { NfcWsProvider, useNfcWs } from "./NfcWsContext";
|
import { NfcWsProvider } from "./NfcWsContext";
|
||||||
import { ToastHost } from "./toast";
|
import { ToastHost } from "./toast";
|
||||||
import Dashboard from "./pages/Dashboard";
|
import Dashboard from "./pages/Dashboard";
|
||||||
import ReadAnalyze from "./pages/ReadAnalyze";
|
import ReadAnalyze from "./pages/ReadAnalyze";
|
||||||
@@ -17,29 +17,6 @@ import Brute from "./pages/Brute";
|
|||||||
import Emulate from "./pages/Emulate";
|
import Emulate from "./pages/Emulate";
|
||||||
import KeyLab from "./pages/KeyLab";
|
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 = [
|
const nav = [
|
||||||
["/", "Dash"],
|
["/", "Dash"],
|
||||||
["/capture", "Read-all"],
|
["/capture", "Read-all"],
|
||||||
@@ -93,7 +70,12 @@ export default function App() {
|
|||||||
MAXIMAL
|
MAXIMAL
|
||||||
</span>
|
</span>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<HeaderBadge />
|
<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>
|
||||||
</div>
|
</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 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]) => (
|
{nav.map(([to, label]) => (
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
import React, { createContext, useCallback, useContext, useMemo, useRef, useState } from "react";
|
||||||
import { apiGet, type Status, type Tag } from "./api";
|
import type { Tag } from "./api";
|
||||||
import { useToolkitWs } from "./useWebSocket";
|
import { useToolkitWs } from "./useWebSocket";
|
||||||
|
|
||||||
export type BrowserLogEntry = {
|
export type BrowserLogEntry = {
|
||||||
@@ -12,7 +12,6 @@ export type CashVariant = "tag" | "vault";
|
|||||||
|
|
||||||
type NfcCtx = {
|
type NfcCtx = {
|
||||||
wsOk: boolean;
|
wsOk: boolean;
|
||||||
pn532Connected: boolean;
|
|
||||||
lastTag: Tag | null;
|
lastTag: Tag | null;
|
||||||
tagPresent: boolean;
|
tagPresent: boolean;
|
||||||
log: BrowserLogEntry[];
|
log: BrowserLogEntry[];
|
||||||
@@ -90,7 +89,6 @@ function isCaptureRecorded(payload: unknown): boolean {
|
|||||||
export function NfcWsProvider({ children }: { children: React.ReactNode }) {
|
export function NfcWsProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [lastTag, setLastTag] = useState<Tag | null>(null);
|
const [lastTag, setLastTag] = useState<Tag | null>(null);
|
||||||
const [tagPresent, setTagPresent] = useState(false);
|
const [tagPresent, setTagPresent] = useState(false);
|
||||||
const [pn532Connected, setPn532Connected] = useState(false);
|
|
||||||
const [log, setLog] = useState<BrowserLogEntry[]>(() => loadPersisted());
|
const [log, setLog] = useState<BrowserLogEntry[]>(() => loadPersisted());
|
||||||
const [cashWave, setCashWave] = useState(0);
|
const [cashWave, setCashWave] = useState(0);
|
||||||
const [cashVariant, setCashVariant] = useState<CashVariant>("tag");
|
const [cashVariant, setCashVariant] = useState<CashVariant>("tag");
|
||||||
@@ -123,11 +121,6 @@ export function NfcWsProvider({ children }: { children: React.ReactNode }) {
|
|||||||
}
|
}
|
||||||
} else if (ch === "capture" && isCaptureRecorded(o.payload)) {
|
} else if (ch === "capture" && isCaptureRecorded(o.payload)) {
|
||||||
bump("vault");
|
bump("vault");
|
||||||
} else if (ch === "pn532") {
|
|
||||||
const p = o.payload as { connected?: boolean } | undefined;
|
|
||||||
if (typeof p?.connected === "boolean") {
|
|
||||||
setPn532Connected(p.connected);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
/* ignore */
|
||||||
@@ -138,19 +131,6 @@ export function NfcWsProvider({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
const wsOk = useToolkitWs(onMsg);
|
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(
|
const applyScanPoll = useCallback(
|
||||||
(present: boolean, tag?: Tag) => {
|
(present: boolean, tag?: Tag) => {
|
||||||
if (!present) {
|
if (!present) {
|
||||||
@@ -185,7 +165,6 @@ export function NfcWsProvider({ children }: { children: React.ReactNode }) {
|
|||||||
const v = useMemo(
|
const v = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
wsOk,
|
wsOk,
|
||||||
pn532Connected,
|
|
||||||
lastTag,
|
lastTag,
|
||||||
tagPresent,
|
tagPresent,
|
||||||
log,
|
log,
|
||||||
@@ -195,7 +174,7 @@ export function NfcWsProvider({ children }: { children: React.ReactNode }) {
|
|||||||
exportBrowserLog,
|
exportBrowserLog,
|
||||||
applyScanPoll,
|
applyScanPoll,
|
||||||
}),
|
}),
|
||||||
[wsOk, pn532Connected, lastTag, tagPresent, log, cashWave, cashVariant, clearBrowserLog, exportBrowserLog, applyScanPoll],
|
[wsOk, lastTag, tagPresent, log, cashWave, cashVariant, clearBrowserLog, exportBrowserLog, applyScanPoll],
|
||||||
);
|
);
|
||||||
|
|
||||||
return <Ctx.Provider value={v}>{children}</Ctx.Provider>;
|
return <Ctx.Provider value={v}>{children}</Ctx.Provider>;
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ export type Status = {
|
|||||||
uptimeMs: number;
|
uptimeMs: number;
|
||||||
freeHeap: number;
|
freeHeap: number;
|
||||||
wifiMode: number;
|
wifiMode: number;
|
||||||
pn532Connected: boolean;
|
|
||||||
pn532?: { ic: number; fwHi: number; fwLo: number };
|
pn532?: { ic: number; fwHi: number; fwLo: number };
|
||||||
scanning: boolean;
|
scanning: boolean;
|
||||||
session?: SessionInfo;
|
session?: SessionInfo;
|
||||||
|
|||||||
@@ -19,14 +19,7 @@ export default function Brute() {
|
|||||||
const keysHex = extraKeys
|
const keysHex = extraKeys
|
||||||
.split(/\r?\n/)
|
.split(/\r?\n/)
|
||||||
.map((l) => l.replace(/\s/g, "").toUpperCase())
|
.map((l) => l.replace(/\s/g, "").toUpperCase())
|
||||||
.filter((l) => l.length === 12 && /^[0-9A-F]+$/.test(l));
|
.filter((l) => l.length === 12);
|
||||||
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 = {
|
const body = {
|
||||||
readerType: reader,
|
readerType: reader,
|
||||||
variations,
|
variations,
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ export default function Capture() {
|
|||||||
const refresh = useCallback(() => {
|
const refresh = useCallback(() => {
|
||||||
apiGet<Status>("/api/status")
|
apiGet<Status>("/api/status")
|
||||||
.then(setSt)
|
.then(setSt)
|
||||||
.catch(() => toast("Status unreachable", "err"));
|
.catch(() => {});
|
||||||
}, [toast]);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refresh();
|
refresh();
|
||||||
|
|||||||
@@ -152,22 +152,9 @@ export default function Dashboard() {
|
|||||||
</li>
|
</li>
|
||||||
<li className="flex justify-between">
|
<li className="flex justify-between">
|
||||||
<span className="text-slate-500">PN532</span>
|
<span className="text-slate-500">PN532</span>
|
||||||
{st.pn532Connected ? (
|
<span className="text-bubble-accent">
|
||||||
<span className="flex items-center gap-2">
|
{st.pn532 ? `IC${st.pn532.ic} v${st.pn532.fwHi}.${st.pn532.fwLo}` : "n/a"}
|
||||||
{st.pn532 && (
|
</span>
|
||||||
<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>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -11,26 +11,13 @@ export default function Emulate() {
|
|||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
|
|
||||||
const send = async () => {
|
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);
|
setBusy(true);
|
||||||
setOut("");
|
setOut("");
|
||||||
try {
|
try {
|
||||||
const r = await fetch(apiUrl("/api/nfc/emulate-raw"), {
|
const r = await fetch(apiUrl("/api/nfc/emulate-raw"), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ hex: cleanHex }),
|
body: JSON.stringify({ hex: hex.replace(/\s/g, "") }),
|
||||||
});
|
});
|
||||||
const t = await r.text();
|
const t = await r.text();
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ export default function RawConsole() {
|
|||||||
push(`RX ${j.response || j.error || "?"}`);
|
push(`RX ${j.response || j.error || "?"}`);
|
||||||
if (j.response) {
|
if (j.response) {
|
||||||
toast("Frame OK");
|
toast("Frame OK");
|
||||||
} else if (j.error) {
|
|
||||||
toast(j.error, "err");
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
push(`ERR ${String(e)}`);
|
push(`ERR ${String(e)}`);
|
||||||
|
|||||||
@@ -31,12 +31,10 @@ export default function ReadAnalyze() {
|
|||||||
|
|
||||||
const readUl = async () => {
|
const readUl = async () => {
|
||||||
try {
|
try {
|
||||||
const j = await apiPost<{ data?: string; error?: string }>("/api/ul/read-page", { page: block });
|
const j = await apiPost<{ data?: string }>("/api/ul/read-page", { page: block });
|
||||||
if (j.data) {
|
if (j.data) {
|
||||||
setHex(j.data + " (UL page)");
|
setHex(j.data + " (UL page)");
|
||||||
toast("UL read OK");
|
toast("UL read OK");
|
||||||
} else {
|
|
||||||
toast(j.error ?? "no data returned", "err");
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast(String(e), "err");
|
toast(String(e), "err");
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ export default function WriteClone() {
|
|||||||
const [labBlob, setLabBlob] = useState<BinaryCardFixture | null>(null);
|
const [labBlob, setLabBlob] = useState<BinaryCardFixture | null>(null);
|
||||||
const [ulPage, setUlPage] = useState(4);
|
const [ulPage, setUlPage] = useState(4);
|
||||||
const [ulData, setUlData] = useState("00000000");
|
const [ulData, setUlData] = useState("00000000");
|
||||||
const [busy, setBusy] = useState(false);
|
|
||||||
|
|
||||||
const write = async () => {
|
const write = async () => {
|
||||||
const cleanKey = key.replace(/\s/g, "");
|
const cleanKey = key.replace(/\s/g, "");
|
||||||
@@ -37,14 +36,11 @@ export default function WriteClone() {
|
|||||||
if (!confirm("Write will modify tag memory. Continue?")) {
|
if (!confirm("Write will modify tag memory. Continue?")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setBusy(true);
|
|
||||||
try {
|
try {
|
||||||
await apiPost("/api/mifare/write-block", { block, key: cleanKey, keyB, data: cleanData });
|
await apiPost("/api/mifare/write-block", { block, key: cleanKey, keyB, data: cleanData });
|
||||||
toast("Write OK");
|
toast("Write OK");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast(String(e), "err");
|
toast(String(e), "err");
|
||||||
} finally {
|
|
||||||
setBusy(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -57,14 +53,11 @@ export default function WriteClone() {
|
|||||||
if (!confirm("Ultralight page write — can brick OTP/lock bytes if misused. Continue?")) {
|
if (!confirm("Ultralight page write — can brick OTP/lock bytes if misused. Continue?")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setBusy(true);
|
|
||||||
try {
|
try {
|
||||||
await apiPost("/api/ul/write-page", { page: ulPage, data: h });
|
await apiPost("/api/ul/write-page", { page: ulPage, data: h });
|
||||||
toast("UL page write OK");
|
toast("UL page write OK");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast(String(e), "err");
|
toast(String(e), "err");
|
||||||
} finally {
|
|
||||||
setBusy(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -137,10 +130,9 @@ export default function WriteClone() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={write}
|
onClick={write}
|
||||||
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"
|
||||||
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"
|
|
||||||
>
|
>
|
||||||
{busy ? "Writing…" : "Write block"}
|
Write block
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="border-t border-white/10 pt-8">
|
<div className="border-t border-white/10 pt-8">
|
||||||
@@ -170,10 +162,9 @@ export default function WriteClone() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={writeUl}
|
onClick={writeUl}
|
||||||
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"
|
||||||
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"
|
|
||||||
>
|
>
|
||||||
{busy ? "Writing…" : "Write UL page"}
|
Write UL page
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const Ctx = createContext<(msg: string, kind?: Toast["kind"]) => void>(() => {})
|
|||||||
export function ToastHost({ children }: { children: React.ReactNode }) {
|
export function ToastHost({ children }: { children: React.ReactNode }) {
|
||||||
const [list, setList] = useState<Toast[]>([]);
|
const [list, setList] = useState<Toast[]>([]);
|
||||||
const push = useCallback((msg: string, kind: Toast["kind"] = "info") => {
|
const push = useCallback((msg: string, kind: Toast["kind"] = "info") => {
|
||||||
const id = Date.now() + Math.random();
|
const id = Date.now();
|
||||||
setList((x) => [...x, { id, msg, kind }]);
|
setList((x) => [...x, { id, msg, kind }]);
|
||||||
setTimeout(() => setList((x) => x.filter((t) => t.id !== id)), 4200);
|
setTimeout(() => setList((x) => x.filter((t) => t.id !== id)), 4200);
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
Reference in New Issue
Block a user