2 Commits

Author SHA1 Message Date
drjones
2da1515f8d feat: resilient boot + full polish pass
Firmware:
- nfc_engine: add nfc_engine_try_init() (non-fatal, sets s_pn532_ready),
  nfc_engine_try_reattach() (soft re-init, skips bus re-init),
  nfc_engine_is_ready() accessor
- main: replace ESP_ERROR_CHECK(nfc_engine_init) with nfc_engine_try_init;
  device boots and serves web UI even with no PN532 connected
- app_net: add reconnect_task — every 5s retries nfc_engine_try_reattach()
  and broadcasts {"channel":"pn532","payload":{"connected":true}} over WS
- app_net: scan_loop_task skips polling when !nfc_engine_is_ready()
- app_net/api_status: always emit pn532Connected bool; null-guard pn532 fw object
- find_sector_hit / program_classic_snapshot_locked: null-guard cJSON array items
- session_capture: abort if xSemaphoreCreateMutex() returns NULL

Web:
- NfcWsContext: track pn532Connected state from WS pn532 channel + status fetch on connect
- App.tsx: live HeaderBadge (LIVE/NO RF/WAIT) replacing static text
- Dashboard: READY/SEARCHING pill with fw version when available
- api.ts: add pn532Connected to Status type
- toast.tsx: fix ID collision (Date.now + Math.random)
- Capture: surface status fetch errors
- ReadAnalyze: add error feedback for readUl when no data returned
- WriteClone: busy state on both write buttons
- RawConsole: toast when frame returns error not response
- Emulate: validate hex before send (non-empty, even length, hex chars only)
- Brute: warn and skip invalid custom key lines

Made-with: Cursor
2026-04-09 15:49:18 -07:00
drjones
63db10d400 fw: robust HTTP body reads, CORS preflight, LED off at boot; update docs
- app_net.c: replace bare httpd_req_recv with recv_body_capped/alloc
  helpers (TCP-safe, full-body reads); add OPTIONS/* CORS preflight
  handler; bump WS broadcast buffer to 2048; add CORS Allow-Methods
- CMakeLists (net_service): add http_parser dep for HTTP_OPTIONS
- nfc_engine/pn532_core: add nfc_access_lock/unlock mutex, board-RGB
  quiet helper, UL type detection, general-status improvements
- pn532_transport: minor cleanup
- main.c: call board_rgb_led_quiet() at boot to kill onboard LED
- sdkconfig.defaults: add board RGB Kconfig defaults
- README, docs/LIMITATIONS, docs/PINOUT: expand and correct

Made-with: Cursor
2026-04-07 21:23:04 -07:00
104 changed files with 1306 additions and 363 deletions

View File

@@ -1,17 +0,0 @@
# EditorConfig is awesome: https://editorconfig.org
<!-- stewardship-standard: editorconfig-v1 -->
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
[*.{md,markdown}]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab

27
.gitattributes vendored
View File

@@ -1,27 +0,0 @@
# stewardship-standard: gitattributes-v1
* text=auto eol=lf
*.md text eol=lf
*.txt text eol=lf
*.json text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
*.sh text eol=lf
*.py text eol=lf
*.js text eol=lf
*.ts text eol=lf
*.c text eol=lf
*.cpp text eol=lf
*.h text eol=lf
*.hpp text eol=lf
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.webp binary
*.pdf binary
*.zip binary
*.bin binary
*.elf binary
*.uf2 binary

View File

@@ -1,21 +0,0 @@
# Bug Report
## Summary
Describe the problem and expected behavior.
## Environment
- Repo version/commit:
- OS/toolchain/board/service:
- Relevant configuration with secrets removed:
## Reproduction
1.
2.
3.
## Logs
Paste only sanitized logs. Remove credentials, tokens, personal data, captures, dumps, and target identifiers.

View File

@@ -1,13 +0,0 @@
# Documentation Task
## Page Or Section
Name the README/wiki section that needs work.
## Change Needed
Describe what should be clearer, corrected, or added.
## Source Of Truth
Link to code, hardware notes, upstream docs, release notes, or maintainer decision.

View File

@@ -1,14 +0,0 @@
# Release Checklist
## Scope
Describe what is being released and why.
## Checks
- [ ] README and wiki are current.
- [ ] Changelog entry exists.
- [ ] License/provenance is clear.
- [ ] No secrets or private data are included.
- [ ] Firmware/binary artifacts include SHA256 hashes and target details.
- [ ] Build or smoke-check result is recorded.

View File

@@ -1,7 +0,0 @@
# Pull Request Checklist
- [ ] Scope is clear and limited.
- [ ] README/wiki updates are included when behavior, setup, hardware, or release process changes.
- [ ] No secrets, tokens, private data, dumps, captures, or generated dependency folders are committed.
- [ ] Build/test/smoke-check result is documented.
- [ ] License or upstream provenance is preserved.

4
.gitignore vendored
View File

@@ -5,7 +5,3 @@ firmware/managed_components/
web/node_modules/ web/node_modules/
web/dist/ web/dist/
.DS_Store .DS_Store
._*
**/._*
.cache/
**/.cache/clangd/

View File

@@ -1,13 +0,0 @@
# Changelog
All meaningful changes to this repository should be recorded here.
## Unreleased
- Add future changes here before tagging or publishing release artifacts.
## 2026-05-20 - Gitea Stewardship Import
- Verified README and wiki coverage.
- Added standard stewardship documentation where missing.
- Established security, contribution, release, and provenance expectations.

View File

@@ -1 +0,0 @@
* @drjones

View File

@@ -1,20 +0,0 @@
# Contributing
## Maintainer Expectations
Keep changes small, reviewable, and tied to a clear project purpose. Do not mix source changes with generated build output or dependency caches.
## Before Committing
- Run the relevant build, lint, or smoke test when the project provides one.
- Check that no credentials, `.env` files, tokens, private keys, captures, dumps, or personal data are staged.
- Keep firmware binaries, large archives, and generated artifacts out of Git unless the repo explicitly documents otherwise.
- Preserve upstream licenses and attribution for third-party code.
## Documentation
Update README and wiki pages when setup, hardware, architecture, environment variables, or release behavior changes.
## Safety
Only submit work intended for authorized environments. Project documentation should make scope and safe operation clearer, never weaker.

21
LICENSE
View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2026 sudo-jones-cmd
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,14 +0,0 @@
# License Status
This repository has not been assigned a blanket license by the stewardship pass.
## Current Rule
- Existing upstream licenses must be preserved.
- Third-party code must retain attribution and license files.
- Original private work remains all rights reserved until an explicit license is selected.
- Do not assume MIT, Apache, GPL, or public-domain status unless a license file in this repository says so.
## Next Step
Classify ownership and dependencies before publishing releases or accepting external contributions.

View File

@@ -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`**, password **`nfc-toolkit`** (WPA2). **mDNS** hostname **`pn532tool.local`** (HTTP port **80**). 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`), **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,9 +38,12 @@ 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`** returns **501** with a stub message — there is **no** in-field OTA in this repo yet; use **`idf.py flash`** (or the included scripts). | | **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 isnt 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 isnt left on a random color — **not** on all boards; power LEDs are hardware. |
| **Status** | **`/api/status`**: uptime, free heap, WiFi mode, **PN532 firmware version** (when reachable), scan flag, session stats. | | **Status** | **`/api/status`**: uptime, free heap, WiFi 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) ### Web UI (each page)
@@ -89,16 +92,24 @@ 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`hex command bytes | | `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/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` | **Not implemented** — returns **501** | | `POST` | `/api/ota` | `url` (HTTPS), optional `reboot` — performs a real OTA update via `esp_https_ota` |
**WebSocket:** `GET /ws` — JSON text frames as above; send **`ping`**, receive **`pong`**. **WebSocket:** `GET /ws` — JSON text frames as above; send **`ping`**, receive **`pong`**.
@@ -112,7 +123,7 @@ The SPA keeps the **last 5000** WebSocket events in **sessionStorage**. **Dow
### Card emulation (PN532 target mode) ### Card emulation (PN532 target mode)
**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**. **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.
--- ---
@@ -145,6 +156,10 @@ 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).

View File

@@ -1,19 +0,0 @@
# Security Policy
## Scope
This repository is maintained for authorized, lawful work only. Do not use code, firmware, payloads, scripts, or documentation from this project against systems, accounts, devices, networks, cards, readers, or services you do not own or do not have explicit permission to test.
## Reporting
Report security concerns privately to the maintainer. Do not open public issues containing live credentials, tokens, private captures, card data, target identifiers, exploit chains, or sensitive logs.
## Secrets And Data
- Do not commit `.env` files, API keys, Wi-Fi credentials, session cookies, private keys, dumps, captures, or personal data.
- Firmware binaries and captured artifacts must include provenance notes and SHA256 hashes before release.
- Generated dependency folders and build output belong outside Git unless there is a documented reason.
## Maintainer Rule
If a change increases misuse risk, narrows safety boundaries, or weakens provenance, it must be rejected or quarantined until documented.

View File

@@ -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; 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. - **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` 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.

View File

@@ -1,23 +0,0 @@
# Maintenance
<!-- stewardship-standard: maintenance-v1 -->
## Stewardship Rules
- Keep generated files, build outputs, copied SDKs, and raw firmware binaries out of Git unless they are the source of truth.
- Keep credentials, tokens, dumps, private messages, session stores, and local machine paths out of commits.
- Prefer small commits with clear intent and a matching issue or release note.
- Preserve upstream attribution when code is copied, forked, or adapted.
## Routine Checks
- README still describes what the project does.
- Setup instructions still work.
- Security policy is accurate for the current risk level.
- Changelog records user-visible changes.
- License status is explicit.
## Hardware Gate
- Confirm exact board, module, and peripheral versions.
- Verify flashing and recovery steps on physical hardware.
- Attach firmware binaries through releases only, with SHA256 checksums.

View File

@@ -20,3 +20,13 @@ 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.

View File

@@ -1,14 +0,0 @@
# Project Handoff
<!-- stewardship-standard: project-handoff-v1 -->
## What This Repo Needs From A Maintainer
- A one-paragraph project summary in README.md.
- Confirmed setup instructions.
- Confirmed license status.
- Confirmed provenance for imported code and binaries.
- A known-good verification command, test, build, flash, or demo path.
## Current Stewardship State
This repo has baseline governance files, wiki pages, issue templates, labels, milestones, and a readiness issue. The next maintainer should replace generic stewardship notes with project-specific facts.

View File

@@ -1,12 +0,0 @@
# Provenance Checklist
<!-- stewardship-standard: provenance-checklist-v1 -->
Use this before claiming ownership or publishing artifacts.
- [ ] Identify original upstream source, if any.
- [ ] Record fork URL, commit, tag, or archive source.
- [ ] Preserve third-party notices and license files.
- [ ] Separate local patches from imported code where practical.
- [ ] Record binary build inputs, toolchain versions, and source commit.
- [ ] Publish checksums for release assets.
- [ ] Mark unknown-origin content as blocked until resolved.

View File

@@ -1,20 +0,0 @@
# Release Process
<!-- stewardship-standard: release-process-v1 -->
## Before Tagging
- Confirm the default branch builds, runs, or flashes as documented.
- Confirm no secrets, private data, generated dependency trees, or raw binaries are accidentally committed.
- Confirm license and upstream provenance are documented.
- Update CHANGELOG.md.
- Attach binaries only as release assets with SHA256 checksums and source commit references.
## Release Notes
Include:
- Purpose of the release.
- Commit hash or tag.
- Build environment.
- Known limitations.
- Verification performed.

View File

@@ -1,20 +0,0 @@
# Roadmap
<!-- stewardship-standard: roadmap-v1 -->
## Now
- Confirm the project purpose in the README.
- Confirm build, run, or flash instructions on a clean machine.
- Classify license status and upstream provenance.
- Close the stewardship readiness checklist issue.
## Next
- Add project-specific tests or verification steps.
- Publish the first verified release only after provenance and security review.
- Replace placeholder wiki notes with project-specific architecture or hardware details.
## Later
- Add examples, screenshots, wiring diagrams, or demo media where useful.
- Decide whether duplicate or experimental branches should be archived.

View File

@@ -1,14 +0,0 @@
# Security Review
<!-- stewardship-standard: security-review-v1 -->
## Required Checks
- [ ] No credentials, tokens, cookies, API keys, private keys, or session files.
- [ ] No private user data, dumps, card data, logs, or captures that should not be stored.
- [ ] No copied dependency trees where package managers or SDK installers should be used instead.
- [ ] No unexplained binaries in source history.
- [ ] Risky behavior is documented and scoped to authorized lab use.
## Release Gate
A release is blocked until the checklist is complete or a maintainer explicitly records why the item does not apply.

View File

@@ -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 json spiffs vfs freertos nfc_engine pn532_host esp_https_ota app_update mbedtls
) )

File diff suppressed because it is too large Load Diff

View File

@@ -26,12 +26,26 @@ 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);
@@ -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_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);

View File

@@ -170,6 +170,7 @@ 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;
} }
@@ -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 (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;
} }
@@ -202,6 +204,7 @@ 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;
} }
@@ -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 (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;
} }
@@ -226,6 +230,7 @@ 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;
} }

View File

@@ -8,6 +8,9 @@
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)
{ {
@@ -18,7 +21,12 @@ static void hint_type(nfc_tag_info_t *t)
t->type_hint = 1; t->type_hint = 1;
break; break;
case 0x00: 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; break;
default: default:
t->type_hint = 0; 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; 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();
@@ -58,9 +105,51 @@ 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) {
@@ -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; 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) {

View File

@@ -1,6 +1,7 @@
#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)
@@ -15,6 +16,10 @@ 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;
} }

View File

@@ -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, 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);

View File

@@ -190,3 +190,81 @@ 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;
}

View File

@@ -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)
i2c_wakeup(); ESP_RETURN_ON_ERROR(i2c_wakeup(), TAG, "i2c wake");
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

File diff suppressed because one or more lines are too long

View File

@@ -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-9oJp152C.js"></script> <script type="module" crossorigin src="/assets/index-BmIGATlK.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-hoMg1Qkq.css"> <link rel="stylesheet" crossorigin href="/assets/index-Bb0xTRCn.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>

View File

@@ -1,4 +1,5 @@
#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"
@@ -8,10 +9,15 @@ 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");
ESP_ERROR_CHECK(nfc_engine_init()); nfc_engine_try_init(); /* non-fatal: logs warning if PN532 absent, AP starts regardless */
session_capture_init(); session_capture_init();
ESP_ERROR_CHECK(app_net_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");
}
} }

View File

@@ -31,3 +31,6 @@ 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

View File

@@ -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 } from "./NfcWsContext"; import { NfcWsProvider, useNfcWs } 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,6 +17,29 @@ 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"],
@@ -70,12 +93,7 @@ export default function App() {
MAXIMAL MAXIMAL
</span> </span>
</motion.div> </motion.div>
<span className="hidden font-mono text-[9px] text-bubble-mint/40 md:inline lg:text-[10px]"> <HeaderBadge />
<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]) => (

View File

@@ -1,5 +1,5 @@
import React, { createContext, useCallback, useContext, useMemo, useRef, useState } from "react"; import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
import type { Tag } from "./api"; import { apiGet, type Status, type Tag } from "./api";
import { useToolkitWs } from "./useWebSocket"; import { useToolkitWs } from "./useWebSocket";
export type BrowserLogEntry = { export type BrowserLogEntry = {
@@ -12,6 +12,7 @@ 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[];
@@ -89,6 +90,7 @@ 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");
@@ -121,6 +123,11 @@ 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 */
@@ -131,6 +138,19 @@ 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) {
@@ -165,6 +185,7 @@ export function NfcWsProvider({ children }: { children: React.ReactNode }) {
const v = useMemo( const v = useMemo(
() => ({ () => ({
wsOk, wsOk,
pn532Connected,
lastTag, lastTag,
tagPresent, tagPresent,
log, log,
@@ -174,7 +195,7 @@ export function NfcWsProvider({ children }: { children: React.ReactNode }) {
exportBrowserLog, exportBrowserLog,
applyScanPoll, 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>; return <Ctx.Provider value={v}>{children}</Ctx.Provider>;

View File

@@ -42,6 +42,7 @@ 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;

View File

@@ -19,7 +19,14 @@ 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); .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 = { const body = {
readerType: reader, readerType: reader,
variations, variations,

View File

@@ -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(() => {}); .catch(() => toast("Status unreachable", "err"));
}, []); }, [toast]);
useEffect(() => { useEffect(() => {
refresh(); refresh();

View File

@@ -152,9 +152,22 @@ 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>
<span className="text-bubble-accent"> {st.pn532Connected ? (
{st.pn532 ? `IC${st.pn532.ic} v${st.pn532.fwHi}.${st.pn532.fwLo}` : "n/a"} <span className="flex items-center gap-2">
</span> {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> </li>
</ul> </ul>
) : ( ) : (

View File

@@ -11,13 +11,26 @@ 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: hex.replace(/\s/g, "") }), body: JSON.stringify({ hex: cleanHex }),
}); });
const t = await r.text(); const t = await r.text();
if (!r.ok) { if (!r.ok) {

Some files were not shown because too many files have changed in this diff Show More