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
2086 lines
70 KiB
C
2086 lines
70 KiB
C
#include "net_service/app_net.h"
|
|
#include "esp_event.h"
|
|
#include "esp_log.h"
|
|
#include "esp_crt_bundle.h"
|
|
#include "esp_https_ota.h"
|
|
#include "esp_system.h"
|
|
#include "esp_http_server.h"
|
|
#include "esp_netif.h"
|
|
#include "esp_spiffs.h"
|
|
#include "esp_timer.h"
|
|
#include "esp_wifi.h"
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/semphr.h"
|
|
#include "freertos/task.h"
|
|
#include "mdns.h"
|
|
#include "nfc_engine/nfc_brute.h"
|
|
#include "nfc_engine/nfc_deep.h"
|
|
#include "nfc_engine/nfc_engine.h"
|
|
#include "nfc_engine/session_capture.h"
|
|
#include "nvs_flash.h"
|
|
#include "pn532_host/pn532_core.h"
|
|
#include "cJSON.h"
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/param.h>
|
|
#include <sys/stat.h>
|
|
|
|
static const char *TAG = "app_net";
|
|
|
|
/* SoftAP: open network (no password) for lab / fastest join. Change SSID here if you want. */
|
|
#define SOFTAP_SSID "PN532-Toolkit"
|
|
|
|
#define WS_BROADCAST_BUF 4096
|
|
#define STATIC_PATH_MAX 192
|
|
|
|
static httpd_handle_t s_server;
|
|
static SemaphoreHandle_t s_nfc_op_mu;
|
|
static volatile bool s_scan = true;
|
|
static TaskHandle_t s_scan_task;
|
|
static volatile bool s_target_active;
|
|
|
|
static void nfc_access_lock(void)
|
|
{
|
|
if (s_nfc_op_mu) {
|
|
xSemaphoreTake(s_nfc_op_mu, portMAX_DELAY);
|
|
}
|
|
}
|
|
|
|
static void nfc_access_unlock(void)
|
|
{
|
|
if (s_nfc_op_mu) {
|
|
xSemaphoreGive(s_nfc_op_mu);
|
|
}
|
|
}
|
|
|
|
static int hexval(char c)
|
|
{
|
|
if (c >= '0' && c <= '9') {
|
|
return c - '0';
|
|
}
|
|
if (c >= 'a' && c <= 'f') {
|
|
return 10 + c - 'a';
|
|
}
|
|
if (c >= 'A' && c <= 'F') {
|
|
return 10 + c - 'A';
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
static bool hex_to_bin(const char *hex, uint8_t *out, size_t out_len)
|
|
{
|
|
size_t n = strlen(hex);
|
|
if (n != out_len * 2) {
|
|
return false;
|
|
}
|
|
for (size_t i = 0; i < out_len; i++) {
|
|
int h = hexval(hex[i * 2]);
|
|
int l = hexval(hex[i * 2 + 1]);
|
|
if (h < 0 || l < 0) {
|
|
return false;
|
|
}
|
|
out[i] = (uint8_t)((h << 4) | l);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool hex_decode_flex(const char *hex, uint8_t *out, size_t out_cap, size_t *out_len)
|
|
{
|
|
size_t n = strlen(hex);
|
|
if (n % 2 || n / 2 > out_cap) {
|
|
return false;
|
|
}
|
|
*out_len = n / 2;
|
|
return hex_to_bin(hex, out, *out_len);
|
|
}
|
|
|
|
static bool copy_json_string(const cJSON *item, char *dst, size_t cap)
|
|
{
|
|
if (!dst || cap == 0 || !cJSON_IsString(item) || !item->valuestring) {
|
|
return false;
|
|
}
|
|
size_t len = strlen(item->valuestring);
|
|
if (len >= cap) {
|
|
return false;
|
|
}
|
|
memcpy(dst, item->valuestring, len + 1);
|
|
return true;
|
|
}
|
|
|
|
static void bin_to_hex(const uint8_t *src, size_t len, char *dst, size_t dst_cap)
|
|
{
|
|
if (!dst || dst_cap == 0) {
|
|
return;
|
|
}
|
|
if (!src || dst_cap < (len * 2 + 1)) {
|
|
dst[0] = '\0';
|
|
return;
|
|
}
|
|
for (size_t i = 0; i < len; i++) {
|
|
snprintf(dst + i * 2, 3, "%02X", src[i]);
|
|
}
|
|
dst[len * 2] = '\0';
|
|
}
|
|
|
|
static bool add_hex_to_object(cJSON *obj, const char *name, const uint8_t *src, size_t len)
|
|
{
|
|
if (!obj || !name || !src) {
|
|
return false;
|
|
}
|
|
char *hex = calloc(1, len * 2 + 1);
|
|
if (!hex) {
|
|
return false;
|
|
}
|
|
bin_to_hex(src, len, hex, len * 2 + 1);
|
|
cJSON_AddStringToObject(obj, name, hex);
|
|
free(hex);
|
|
return true;
|
|
}
|
|
|
|
static int json_get_int_default(const cJSON *obj, const char *name, int fallback)
|
|
{
|
|
cJSON *item = cJSON_GetObjectItemCaseSensitive((cJSON *)obj, name);
|
|
return cJSON_IsNumber(item) ? (int)cJSON_GetNumberValue(item) : fallback;
|
|
}
|
|
|
|
static bool json_get_bool_default(const cJSON *obj, const char *name, bool fallback)
|
|
{
|
|
cJSON *item = cJSON_GetObjectItemCaseSensitive((cJSON *)obj, name);
|
|
return cJSON_IsBool(item) ? cJSON_IsTrue(item) : fallback;
|
|
}
|
|
|
|
/** Read full POST body (httpd may return partial data in multiple recv calls). */
|
|
static int recv_body_capped(httpd_req_t *req, char *buf, size_t cap)
|
|
{
|
|
if (!buf || cap < 2) {
|
|
return -1;
|
|
}
|
|
size_t cl = req->content_len;
|
|
if (cl >= cap) {
|
|
return -1;
|
|
}
|
|
if (cl == 0) {
|
|
buf[0] = '\0';
|
|
return 0;
|
|
}
|
|
size_t got = 0;
|
|
while (got < cl) {
|
|
int r = httpd_req_recv(req, buf + got, cl - got);
|
|
if (r <= 0) {
|
|
return -1;
|
|
}
|
|
got += (size_t)r;
|
|
}
|
|
buf[got] = '\0';
|
|
return (int)got;
|
|
}
|
|
|
|
static char *recv_body_alloc(httpd_req_t *req, size_t max_len, int *out_len)
|
|
{
|
|
size_t cl = req->content_len;
|
|
if (cl == 0 || cl > max_len) {
|
|
return NULL;
|
|
}
|
|
char *buf = malloc(cl + 1);
|
|
if (!buf) {
|
|
return NULL;
|
|
}
|
|
size_t got = 0;
|
|
while (got < cl) {
|
|
int r = httpd_req_recv(req, buf + got, cl - got);
|
|
if (r <= 0) {
|
|
free(buf);
|
|
return NULL;
|
|
}
|
|
got += (size_t)r;
|
|
}
|
|
buf[cl] = '\0';
|
|
if (out_len) {
|
|
*out_len = (int)cl;
|
|
}
|
|
return buf;
|
|
}
|
|
|
|
esp_err_t app_net_broadcast_json(const char *channel, const char *json_text)
|
|
{
|
|
if (!s_server || !json_text) {
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
char line[WS_BROADCAST_BUF];
|
|
int n = snprintf(line, sizeof(line), "{\"channel\":\"%s\",\"payload\":%s}", channel ? channel : "event",
|
|
json_text);
|
|
if (n < 0 || n >= (int)sizeof(line)) {
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
int fds[16];
|
|
size_t fdcount = sizeof(fds) / sizeof(fds[0]);
|
|
esp_err_t er = httpd_get_client_list(s_server, &fdcount, fds);
|
|
if (er != ESP_OK) {
|
|
return er;
|
|
}
|
|
httpd_ws_frame_t pkt = {.type = HTTPD_WS_TYPE_TEXT, .payload = (uint8_t *)line, .len = (size_t)n};
|
|
for (size_t i = 0; i < fdcount; i++) {
|
|
if (httpd_ws_get_fd_info(s_server, fds[i]) == HTTPD_WS_CLIENT_WEBSOCKET) {
|
|
(void)httpd_ws_send_data(s_server, fds[i], &pkt);
|
|
}
|
|
}
|
|
return ESP_OK;
|
|
}
|
|
|
|
static esp_err_t send_json(httpd_req_t *req, cJSON *j, int status)
|
|
{
|
|
if (!req || !j) {
|
|
cJSON_Delete(j);
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
char *p = cJSON_PrintUnformatted(j);
|
|
cJSON_Delete(j);
|
|
if (!p) {
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
httpd_resp_set_type(req, "application/json");
|
|
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
|
|
httpd_resp_set_hdr(req, "Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
httpd_resp_set_hdr(req, "Access-Control-Allow-Headers", "Content-Type");
|
|
switch (status) {
|
|
case 200:
|
|
httpd_resp_set_status(req, "200 OK");
|
|
break;
|
|
case 400:
|
|
httpd_resp_set_status(req, "400 Bad Request");
|
|
break;
|
|
case 404:
|
|
httpd_resp_set_status(req, "404 Not Found");
|
|
break;
|
|
case 409:
|
|
httpd_resp_set_status(req, "409 Conflict");
|
|
break;
|
|
case 500:
|
|
httpd_resp_set_status(req, "500 Internal Server Error");
|
|
break;
|
|
case 501:
|
|
httpd_resp_set_status(req, "501 Not Implemented");
|
|
break;
|
|
default:
|
|
httpd_resp_set_status(req, "200 OK");
|
|
break;
|
|
}
|
|
esp_err_t e = httpd_resp_send(req, p, HTTPD_RESP_USE_STRLEN);
|
|
free(p);
|
|
return e;
|
|
}
|
|
|
|
static esp_err_t ensure_ultralight_tag_locked(nfc_tag_info_t *tag)
|
|
{
|
|
if (!tag) {
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
esp_err_t err = nfc_poll_passive_target(tag);
|
|
if (err != ESP_OK) {
|
|
return err;
|
|
}
|
|
return nfc_tag_is_type2(tag) ? ESP_OK : ESP_ERR_NOT_SUPPORTED;
|
|
}
|
|
|
|
static esp_err_t ensure_mifare_classic_tag_locked(nfc_tag_info_t *tag)
|
|
{
|
|
if (!tag) {
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
esp_err_t err = nfc_poll_passive_target(tag);
|
|
if (err != ESP_OK) {
|
|
return err;
|
|
}
|
|
return nfc_tag_is_mifare_classic(tag) ? ESP_OK : ESP_ERR_NOT_SUPPORTED;
|
|
}
|
|
|
|
static esp_err_t reject_if_target_active(httpd_req_t *req)
|
|
{
|
|
if (!s_target_active) {
|
|
return ESP_OK;
|
|
}
|
|
return send_json(req, cJSON_CreateString("target mode active; stop target mode first"), 409);
|
|
}
|
|
|
|
static cJSON *build_probe_locked(const nfc_tag_info_t *tag)
|
|
{
|
|
if (!tag) {
|
|
return NULL;
|
|
}
|
|
cJSON *root = cJSON_CreateObject();
|
|
cJSON *tag_json = nfc_tag_to_json(tag);
|
|
cJSON *caps = cJSON_CreateObject();
|
|
if (!root || !tag_json || !caps) {
|
|
cJSON_Delete(root);
|
|
cJSON_Delete(tag_json);
|
|
cJSON_Delete(caps);
|
|
return NULL;
|
|
}
|
|
cJSON_AddItemToObject(root, "tag", tag_json);
|
|
if (nfc_tag_is_mifare_classic(tag)) {
|
|
cJSON_AddStringToObject(caps, "family", nfc_tag_is_mifare_classic_4k(tag) ? "mifareClassic4k" : "mifareClassic");
|
|
cJSON_AddNumberToObject(caps, "sectorCount", nfc_mifare_sector_count(tag));
|
|
cJSON_AddBoolToObject(caps, "canRead", true);
|
|
cJSON_AddBoolToObject(caps, "canWrite", true);
|
|
cJSON_AddBoolToObject(caps, "canDictionaryAttack", true);
|
|
cJSON_AddBoolToObject(caps, "structuredCloneCapture", true);
|
|
cJSON_AddBoolToObject(caps, "structuredCloneProgram", true);
|
|
cJSON_AddBoolToObject(caps, "manufacturerBlockWritable", false);
|
|
cJSON_AddBoolToObject(caps, "targetModeRelevant", false);
|
|
cJSON_AddStringToObject(caps, "note",
|
|
"Classic clone/program flow can write user blocks and optional trailers, but not manufacturer block 0 on normal cards.");
|
|
} else if (nfc_tag_is_type2(tag)) {
|
|
cJSON_AddStringToObject(caps, "family", "type2");
|
|
cJSON_AddBoolToObject(caps, "canRead", true);
|
|
cJSON_AddBoolToObject(caps, "canWrite", true);
|
|
cJSON_AddBoolToObject(caps, "structuredCloneCapture", true);
|
|
cJSON_AddBoolToObject(caps, "structuredCloneProgram", true);
|
|
cJSON_AddNumberToObject(caps, "safeUserPageStart", 4);
|
|
uint8_t version[8];
|
|
if (nfc_type2_get_version(version) == ESP_OK) {
|
|
(void)add_hex_to_object(caps, "getVersionHex", version, sizeof(version));
|
|
}
|
|
cJSON_AddStringToObject(caps, "note",
|
|
"Type 2 clone/program flow writes user pages by default and leaves manufacturer/lock pages untouched unless forced.");
|
|
} else {
|
|
cJSON_AddStringToObject(caps, "family", "unknown");
|
|
cJSON_AddBoolToObject(caps, "canRead", false);
|
|
cJSON_AddBoolToObject(caps, "canWrite", false);
|
|
cJSON_AddBoolToObject(caps, "structuredCloneCapture", false);
|
|
cJSON_AddBoolToObject(caps, "structuredCloneProgram", false);
|
|
cJSON_AddStringToObject(caps, "note",
|
|
"Use raw commands and manual analysis; this tag does not match the current structured Classic/Type 2 workflows.");
|
|
}
|
|
cJSON_AddItemToObject(root, "capabilities", caps);
|
|
return root;
|
|
}
|
|
|
|
static cJSON *find_sector_hit(const cJSON *attack, int sector)
|
|
{
|
|
if (!attack) {
|
|
return NULL;
|
|
}
|
|
cJSON *hits = cJSON_GetObjectItemCaseSensitive((cJSON *)attack, "sectorHits");
|
|
if (!cJSON_IsArray(hits)) {
|
|
return NULL;
|
|
}
|
|
int n = cJSON_GetArraySize(hits);
|
|
for (int i = 0; i < n; i++) {
|
|
cJSON *it = cJSON_GetArrayItem(hits, i);
|
|
if (!it) continue;
|
|
cJSON *s = cJSON_GetObjectItemCaseSensitive(it, "sector");
|
|
if (cJSON_IsNumber(s) && (int)cJSON_GetNumberValue(s) == sector) {
|
|
return it;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static bool parse_sector_key_hit(const cJSON *hit, nfc_mifare_key_t *key, char *key_hex_out, size_t key_hex_cap)
|
|
{
|
|
if (!hit || !key) {
|
|
return false;
|
|
}
|
|
cJSON *kh = cJSON_GetObjectItemCaseSensitive((cJSON *)hit, "keyHex");
|
|
cJSON *kt = cJSON_GetObjectItemCaseSensitive((cJSON *)hit, "keyType");
|
|
if (!cJSON_IsString(kh) || !kh->valuestring || !cJSON_IsString(kt) || !kt->valuestring) {
|
|
return false;
|
|
}
|
|
if (!hex_to_bin(kh->valuestring, key->key, 6)) {
|
|
return false;
|
|
}
|
|
key->key_b = (strcmp(kt->valuestring, "B") == 0);
|
|
if (key_hex_out && key_hex_cap > 0) {
|
|
size_t len = strlen(kh->valuestring);
|
|
if (len >= key_hex_cap) {
|
|
return false;
|
|
}
|
|
memcpy(key_hex_out, kh->valuestring, len + 1);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static cJSON *build_type2_capture_locked(const nfc_tag_info_t *tag, int max_pages)
|
|
{
|
|
if (!tag) {
|
|
return NULL;
|
|
}
|
|
if (max_pages < 4) {
|
|
max_pages = 4;
|
|
}
|
|
if (max_pages > 240) {
|
|
max_pages = 240;
|
|
}
|
|
cJSON *root = cJSON_CreateObject();
|
|
cJSON *tag_json = nfc_tag_to_json(tag);
|
|
cJSON *pages = cJSON_CreateArray();
|
|
if (!root || !tag_json || !pages) {
|
|
cJSON_Delete(root);
|
|
cJSON_Delete(tag_json);
|
|
cJSON_Delete(pages);
|
|
return NULL;
|
|
}
|
|
cJSON_AddStringToObject(root, "format", "type2");
|
|
cJSON_AddItemToObject(root, "tag", tag_json);
|
|
uint8_t version[8];
|
|
if (nfc_type2_get_version(version) == ESP_OK) {
|
|
(void)add_hex_to_object(root, "getVersionHex", version, sizeof(version));
|
|
}
|
|
for (int page = 0; page < max_pages; page++) {
|
|
uint8_t data[4];
|
|
if (nfc_ultralight_read_page((uint8_t)page, data) != ESP_OK) {
|
|
break;
|
|
}
|
|
char hx[9];
|
|
bin_to_hex(data, sizeof(data), hx, sizeof(hx));
|
|
cJSON_AddItemToArray(pages, cJSON_CreateString(hx));
|
|
}
|
|
cJSON_AddNumberToObject(root, "pageCount", cJSON_GetArraySize(pages));
|
|
cJSON_AddItemToObject(root, "pagesHex", pages);
|
|
cJSON_AddStringToObject(root, "programNote",
|
|
"Programming skips pages 0-3 by default because they contain manufacturer and lock/config data.");
|
|
return root;
|
|
}
|
|
|
|
static cJSON *build_classic_capture_locked(const nfc_tag_info_t *tag, const uint8_t *extra_keys, size_t extra_n,
|
|
bool variations)
|
|
{
|
|
if (!tag) {
|
|
return NULL;
|
|
}
|
|
int sectors = nfc_mifare_sector_count(tag);
|
|
if (sectors <= 0) {
|
|
return NULL;
|
|
}
|
|
cJSON *attack = nfc_mifare_dictionary_attack((nfc_tag_info_t *)tag, 0, (uint8_t)(sectors - 1),
|
|
extra_keys, extra_n, variations, NULL);
|
|
cJSON *root = cJSON_CreateObject();
|
|
cJSON *tag_json = nfc_tag_to_json(tag);
|
|
cJSON *arr = cJSON_CreateArray();
|
|
if (!attack || !root || !tag_json || !arr) {
|
|
cJSON_Delete(attack);
|
|
cJSON_Delete(root);
|
|
cJSON_Delete(tag_json);
|
|
cJSON_Delete(arr);
|
|
return NULL;
|
|
}
|
|
cJSON_AddStringToObject(root, "format", nfc_tag_is_mifare_classic_4k(tag) ? "mifareClassic4k" : "mifareClassic");
|
|
cJSON_AddItemToObject(root, "tag", tag_json);
|
|
cJSON_AddNumberToObject(root, "sectorCount", sectors);
|
|
cJSON_AddItemToObject(root, "keySearch", attack);
|
|
for (int sector = 0; sector < sectors; sector++) {
|
|
int first_block = 0;
|
|
int block_count = 0;
|
|
uint8_t trailer_block = 0;
|
|
cJSON *sec = cJSON_CreateObject();
|
|
if (!sec) {
|
|
cJSON_Delete(root);
|
|
cJSON_Delete(arr);
|
|
return NULL;
|
|
}
|
|
cJSON_AddNumberToObject(sec, "sector", sector);
|
|
if (!nfc_mifare_sector_layout(tag, sector, &first_block, &block_count, &trailer_block)) {
|
|
cJSON_AddBoolToObject(sec, "layoutError", true);
|
|
cJSON_AddItemToArray(arr, sec);
|
|
continue;
|
|
}
|
|
cJSON_AddNumberToObject(sec, "firstBlock", first_block);
|
|
cJSON_AddNumberToObject(sec, "blockCount", block_count);
|
|
cJSON_AddNumberToObject(sec, "trailerBlock", trailer_block);
|
|
cJSON *hit = find_sector_hit(attack, sector);
|
|
nfc_mifare_key_t key;
|
|
char key_hex[13];
|
|
if (!parse_sector_key_hit(hit, &key, key_hex, sizeof(key_hex))) {
|
|
cJSON_AddBoolToObject(sec, "authFailed", true);
|
|
cJSON_AddItemToArray(arr, sec);
|
|
continue;
|
|
}
|
|
cJSON_AddStringToObject(sec, "keyHex", key_hex);
|
|
cJSON_AddStringToObject(sec, "keyType", key.key_b ? "B" : "A");
|
|
if (nfc_mifare_authenticate_block(tag, trailer_block, &key) != ESP_OK) {
|
|
cJSON_AddBoolToObject(sec, "authFailed", true);
|
|
cJSON_AddItemToArray(arr, sec);
|
|
continue;
|
|
}
|
|
cJSON *blocks = cJSON_CreateArray();
|
|
if (!blocks) {
|
|
cJSON_Delete(sec);
|
|
cJSON_Delete(root);
|
|
cJSON_Delete(arr);
|
|
return NULL;
|
|
}
|
|
for (int i = 0; i < block_count; i++) {
|
|
uint8_t block_data[NFC_BLOCK_LEN];
|
|
if (nfc_mifare_read_block((uint8_t)(first_block + i), block_data) == ESP_OK) {
|
|
char hx[NFC_BLOCK_LEN * 2 + 1];
|
|
bin_to_hex(block_data, sizeof(block_data), hx, sizeof(hx));
|
|
cJSON_AddItemToArray(blocks, cJSON_CreateString(hx));
|
|
} else {
|
|
cJSON_AddItemToArray(blocks, cJSON_CreateNull());
|
|
}
|
|
}
|
|
cJSON_AddItemToObject(sec, "blocksHex", blocks);
|
|
cJSON_AddItemToArray(arr, sec);
|
|
}
|
|
cJSON_AddItemToObject(root, "sectors", arr);
|
|
cJSON_AddStringToObject(root, "programNote",
|
|
"Programming writes data blocks and optionally trailers; block 0 is always skipped on normal Classic cards.");
|
|
return root;
|
|
}
|
|
|
|
static cJSON *program_type2_snapshot_locked(const cJSON *snapshot, bool include_lock_pages)
|
|
{
|
|
cJSON *pages = cJSON_GetObjectItemCaseSensitive((cJSON *)snapshot, "pagesHex");
|
|
if (!cJSON_IsArray(pages)) {
|
|
return NULL;
|
|
}
|
|
int count = cJSON_GetArraySize(pages);
|
|
int first_page = include_lock_pages ? 0 : 4;
|
|
cJSON *out = cJSON_CreateObject();
|
|
if (!out) {
|
|
return NULL;
|
|
}
|
|
int written = 0;
|
|
int skipped = 0;
|
|
int failed = 0;
|
|
for (int page = 0; page < count; page++) {
|
|
if (page < first_page) {
|
|
skipped++;
|
|
continue;
|
|
}
|
|
cJSON *it = cJSON_GetArrayItem(pages, page);
|
|
if (!cJSON_IsString(it) || !it->valuestring) {
|
|
skipped++;
|
|
continue;
|
|
}
|
|
uint8_t data[4];
|
|
if (!hex_to_bin(it->valuestring, data, sizeof(data))) {
|
|
failed++;
|
|
continue;
|
|
}
|
|
if (nfc_ultralight_write_page((uint8_t)page, data) == ESP_OK) {
|
|
written++;
|
|
} else {
|
|
failed++;
|
|
}
|
|
}
|
|
cJSON_AddStringToObject(out, "format", "type2");
|
|
cJSON_AddNumberToObject(out, "writtenPages", written);
|
|
cJSON_AddNumberToObject(out, "skippedPages", skipped);
|
|
cJSON_AddNumberToObject(out, "failedPages", failed);
|
|
cJSON_AddBoolToObject(out, "includeLockPages", include_lock_pages);
|
|
return out;
|
|
}
|
|
|
|
static cJSON *program_classic_snapshot_locked(const nfc_tag_info_t *tag, const cJSON *snapshot, bool include_trailers)
|
|
{
|
|
if (!tag) {
|
|
return NULL;
|
|
}
|
|
cJSON *sectors = cJSON_GetObjectItemCaseSensitive((cJSON *)snapshot, "sectors");
|
|
if (!cJSON_IsArray(sectors)) {
|
|
return NULL;
|
|
}
|
|
int written = 0;
|
|
int skipped = 0;
|
|
int failed = 0;
|
|
int sectors_n = cJSON_GetArraySize(sectors);
|
|
for (int si = 0; si < sectors_n; si++) {
|
|
cJSON *sec = cJSON_GetArrayItem(sectors, si);
|
|
if (!sec) { skipped++; continue; }
|
|
cJSON *blocks = cJSON_GetObjectItemCaseSensitive(sec, "blocksHex");
|
|
cJSON *first = cJSON_GetObjectItemCaseSensitive(sec, "firstBlock");
|
|
cJSON *count = cJSON_GetObjectItemCaseSensitive(sec, "blockCount");
|
|
cJSON *trailer = cJSON_GetObjectItemCaseSensitive(sec, "trailerBlock");
|
|
if (!cJSON_IsArray(blocks) || !cJSON_IsNumber(first) || !cJSON_IsNumber(count) || !cJSON_IsNumber(trailer)) {
|
|
skipped++;
|
|
continue;
|
|
}
|
|
nfc_mifare_key_t key;
|
|
if (!parse_sector_key_hit(sec, &key, NULL, 0)) {
|
|
failed++;
|
|
continue;
|
|
}
|
|
uint8_t trailer_block = (uint8_t)cJSON_GetNumberValue(trailer);
|
|
if (nfc_mifare_authenticate_block(tag, trailer_block, &key) != ESP_OK) {
|
|
failed++;
|
|
continue;
|
|
}
|
|
int first_block = (int)cJSON_GetNumberValue(first);
|
|
int block_count = (int)cJSON_GetNumberValue(count);
|
|
int blocks_n = cJSON_GetArraySize(blocks);
|
|
int limit = block_count < blocks_n ? block_count : blocks_n;
|
|
for (int bi = 0; bi < limit; bi++) {
|
|
int block_no = first_block + bi;
|
|
if (block_no == 0) {
|
|
skipped++;
|
|
continue;
|
|
}
|
|
if (block_no == trailer_block && !include_trailers) {
|
|
skipped++;
|
|
continue;
|
|
}
|
|
cJSON *it = cJSON_GetArrayItem(blocks, bi);
|
|
if (!cJSON_IsString(it) || !it->valuestring) {
|
|
skipped++;
|
|
continue;
|
|
}
|
|
uint8_t data[NFC_BLOCK_LEN];
|
|
if (!hex_to_bin(it->valuestring, data, sizeof(data))) {
|
|
failed++;
|
|
continue;
|
|
}
|
|
if (nfc_mifare_write_block((uint8_t)block_no, data) == ESP_OK) {
|
|
written++;
|
|
} else {
|
|
failed++;
|
|
}
|
|
}
|
|
}
|
|
cJSON *out = cJSON_CreateObject();
|
|
if (!out) {
|
|
return NULL;
|
|
}
|
|
cJSON_AddStringToObject(out, "format", "mifareClassic");
|
|
cJSON_AddNumberToObject(out, "writtenBlocks", written);
|
|
cJSON_AddNumberToObject(out, "skippedBlocks", skipped);
|
|
cJSON_AddNumberToObject(out, "failedBlocks", failed);
|
|
cJSON_AddBoolToObject(out, "includeTrailers", include_trailers);
|
|
return out;
|
|
}
|
|
|
|
static esp_err_t api_status(httpd_req_t *req)
|
|
{
|
|
cJSON *o = cJSON_CreateObject();
|
|
if (!o) {
|
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "out of memory");
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
cJSON_AddStringToObject(o, "app", "pn532_nfc_toolkit");
|
|
cJSON_AddNumberToObject(o, "uptimeMs", (double)(esp_timer_get_time() / 1000));
|
|
cJSON_AddNumberToObject(o, "freeHeap", (double)esp_get_free_heap_size());
|
|
wifi_mode_t mode;
|
|
esp_wifi_get_mode(&mode);
|
|
cJSON_AddNumberToObject(o, "wifiMode", mode);
|
|
bool pn532_ok = nfc_engine_is_ready();
|
|
cJSON_AddBoolToObject(o, "pn532Connected", pn532_ok);
|
|
if (pn532_ok) {
|
|
uint8_t ic = 0, hi = 0, lo = 0;
|
|
nfc_access_lock();
|
|
if (pn532_get_firmware_version(&ic, &hi, &lo) == ESP_OK) {
|
|
cJSON *pn = cJSON_CreateObject();
|
|
if (pn) {
|
|
cJSON_AddNumberToObject(pn, "ic", ic);
|
|
cJSON_AddNumberToObject(pn, "fwHi", hi);
|
|
cJSON_AddNumberToObject(pn, "fwLo", lo);
|
|
cJSON_AddItemToObject(o, "pn532", pn);
|
|
}
|
|
}
|
|
nfc_access_unlock();
|
|
}
|
|
cJSON_AddBoolToObject(o, "scanning", s_scan);
|
|
cJSON_AddBoolToObject(o, "targetActive", s_target_active);
|
|
size_t cap_u = 0;
|
|
uint32_t cap_l = 0;
|
|
bool cap_f = false;
|
|
session_capture_get_status(&cap_u, &cap_l, &cap_f);
|
|
cJSON *cap = cJSON_CreateObject();
|
|
if (!cap) {
|
|
cJSON_Delete(o);
|
|
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "out of memory");
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
cJSON_AddNumberToObject(cap, "usedBytes", (double)cap_u);
|
|
cJSON_AddNumberToObject(cap, "maxBytes", (double)session_capture_max());
|
|
cJSON_AddNumberToObject(cap, "lines", (double)cap_l);
|
|
cJSON_AddBoolToObject(cap, "full", cap_f);
|
|
cJSON_AddBoolToObject(cap, "deepCapture", session_capture_deep_enabled());
|
|
cJSON_AddItemToObject(o, "session", cap);
|
|
return send_json(req, o, 200);
|
|
}
|
|
|
|
static void tag_uid_hex(const nfc_tag_info_t *tag, char *out, size_t out_sz)
|
|
{
|
|
if (!out || out_sz == 0) {
|
|
return;
|
|
}
|
|
out[0] = 0;
|
|
size_t p = 0;
|
|
for (int i = 0; i < tag->uid_len && p + 2 < out_sz; i++) {
|
|
p += (size_t)snprintf(out + p, out_sz - p, "%02X", tag->uid[i]);
|
|
}
|
|
}
|
|
|
|
static esp_err_t api_session_export(httpd_req_t *req)
|
|
{
|
|
size_t n = session_capture_export_size();
|
|
if (n == 0) {
|
|
httpd_resp_set_type(req, "text/plain");
|
|
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
|
|
return httpd_resp_send(req, "empty", HTTPD_RESP_USE_STRLEN);
|
|
}
|
|
char *buf = malloc(n);
|
|
if (!buf) {
|
|
return send_json(req, cJSON_CreateString("out of memory"), 500);
|
|
}
|
|
size_t got = 0;
|
|
session_capture_copy_to(buf, n, &got);
|
|
httpd_resp_set_type(req, "application/x-ndjson");
|
|
httpd_resp_set_hdr(req, "Content-Disposition", "attachment; filename=\"pn532-deep-capture.ndjson\"");
|
|
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
|
|
esp_err_t er = httpd_resp_send(req, buf, got);
|
|
free(buf);
|
|
return er;
|
|
}
|
|
|
|
static esp_err_t api_session_clear(httpd_req_t *req)
|
|
{
|
|
char drain[128];
|
|
(void)recv_body_capped(req, drain, sizeof(drain));
|
|
session_capture_clear();
|
|
cJSON *o = cJSON_CreateObject();
|
|
cJSON_AddBoolToObject(o, "ok", true);
|
|
return send_json(req, o, 200);
|
|
}
|
|
|
|
static esp_err_t api_session_deep(httpd_req_t *req)
|
|
{
|
|
char buf[128];
|
|
int r = recv_body_capped(req, buf, sizeof(buf));
|
|
if (r < 0) {
|
|
return send_json(req, cJSON_CreateString("body required or too large"), 400);
|
|
}
|
|
if (r == 0) {
|
|
cJSON *o = cJSON_CreateObject();
|
|
cJSON_AddBoolToObject(o, "deepCapture", session_capture_deep_enabled());
|
|
return send_json(req, o, 200);
|
|
}
|
|
cJSON *j = cJSON_Parse(buf);
|
|
if (!j) {
|
|
return send_json(req, cJSON_CreateString("bad json"), 400);
|
|
}
|
|
cJSON *en = cJSON_GetObjectItem(j, "enable");
|
|
if (cJSON_IsBool(en)) {
|
|
session_capture_set_deep(cJSON_IsTrue(en));
|
|
}
|
|
cJSON_Delete(j);
|
|
cJSON *o = cJSON_CreateObject();
|
|
cJSON_AddBoolToObject(o, "deepCapture", session_capture_deep_enabled());
|
|
return send_json(req, o, 200);
|
|
}
|
|
|
|
static esp_err_t api_nfc_poll(httpd_req_t *req)
|
|
{
|
|
esp_err_t gate = reject_if_target_active(req);
|
|
if (gate != ESP_OK) {
|
|
return gate;
|
|
}
|
|
char drain[128];
|
|
(void)recv_body_capped(req, drain, sizeof(drain));
|
|
|
|
nfc_tag_info_t tag;
|
|
nfc_access_lock();
|
|
esp_err_t e = nfc_poll_passive_target(&tag);
|
|
if (e == ESP_ERR_NOT_FOUND) {
|
|
nfc_access_unlock();
|
|
cJSON *o = cJSON_CreateObject();
|
|
cJSON_AddBoolToObject(o, "present", false);
|
|
return send_json(req, o, 200);
|
|
}
|
|
if (e != ESP_OK) {
|
|
nfc_access_unlock();
|
|
cJSON *o = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(o, "error", esp_err_to_name(e));
|
|
return send_json(req, o, 400);
|
|
}
|
|
cJSON *tj = nfc_tag_to_json(&tag);
|
|
nfc_access_unlock();
|
|
if (!tj) {
|
|
return send_json(req, cJSON_CreateString("out of memory building tag"), 500);
|
|
}
|
|
cJSON *o = cJSON_CreateObject();
|
|
cJSON_AddBoolToObject(o, "present", true);
|
|
cJSON_AddItemToObject(o, "tag", tj);
|
|
return send_json(req, o, 200);
|
|
}
|
|
|
|
static esp_err_t api_scan(httpd_req_t *req)
|
|
{
|
|
char buf[128];
|
|
int r = recv_body_capped(req, buf, sizeof(buf));
|
|
if (r < 0) {
|
|
return send_json(req, cJSON_CreateString("no body or too large"), 400);
|
|
}
|
|
if (r == 0) {
|
|
cJSON *o = cJSON_CreateObject();
|
|
cJSON_AddBoolToObject(o, "enable", s_scan);
|
|
return send_json(req, o, 200);
|
|
}
|
|
cJSON *j = cJSON_Parse(buf);
|
|
if (!j) {
|
|
return send_json(req, cJSON_CreateString("bad json"), 400);
|
|
}
|
|
cJSON *en = cJSON_GetObjectItem(j, "enable");
|
|
if (cJSON_IsBool(en) && cJSON_IsTrue(en) && s_target_active) {
|
|
cJSON_Delete(j);
|
|
return send_json(req, cJSON_CreateString("target mode active; stop target mode first"), 409);
|
|
}
|
|
if (cJSON_IsBool(en)) {
|
|
app_net_set_continuous_scan(cJSON_IsTrue(en));
|
|
}
|
|
cJSON_Delete(j);
|
|
cJSON *o = cJSON_CreateObject();
|
|
cJSON_AddBoolToObject(o, "enable", s_scan);
|
|
return send_json(req, o, 200);
|
|
}
|
|
|
|
static esp_err_t api_general_status(httpd_req_t *req)
|
|
{
|
|
esp_err_t gate = reject_if_target_active(req);
|
|
if (gate != ESP_OK) {
|
|
return gate;
|
|
}
|
|
uint8_t gs[32];
|
|
size_t gl = 0;
|
|
nfc_access_lock();
|
|
esp_err_t e = pn532_get_general_status(gs, sizeof(gs), &gl);
|
|
nfc_access_unlock();
|
|
if (e != ESP_OK) {
|
|
cJSON *o = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(o, "error", esp_err_to_name(e));
|
|
return send_json(req, o, 400);
|
|
}
|
|
cJSON *o = cJSON_CreateObject();
|
|
cJSON *arr = cJSON_CreateArray();
|
|
if (!arr) {
|
|
cJSON_Delete(o);
|
|
return send_json(req, cJSON_CreateString("out of memory"), 500);
|
|
}
|
|
for (size_t i = 0; i < gl; i++) {
|
|
cJSON_AddItemToArray(arr, cJSON_CreateNumber(gs[i]));
|
|
}
|
|
cJSON_AddItemToObject(o, "raw", arr);
|
|
return send_json(req, o, 200);
|
|
}
|
|
|
|
static esp_err_t api_nfc_probe(httpd_req_t *req)
|
|
{
|
|
esp_err_t gate = reject_if_target_active(req);
|
|
if (gate != ESP_OK) {
|
|
return gate;
|
|
}
|
|
char drain[64];
|
|
(void)recv_body_capped(req, drain, sizeof(drain));
|
|
nfc_tag_info_t tag;
|
|
nfc_access_lock();
|
|
esp_err_t err = nfc_poll_passive_target(&tag);
|
|
if (err != ESP_OK) {
|
|
nfc_access_unlock();
|
|
cJSON *o = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(o, "error", err == ESP_ERR_NOT_FOUND ? "no tag" : esp_err_to_name(err));
|
|
return send_json(req, o, err == ESP_ERR_NOT_FOUND ? 400 : 500);
|
|
}
|
|
cJSON *o = build_probe_locked(&tag);
|
|
nfc_access_unlock();
|
|
if (!o) {
|
|
return send_json(req, cJSON_CreateString("probe failed"), 500);
|
|
}
|
|
return send_json(req, o, 200);
|
|
}
|
|
|
|
static esp_err_t api_clone_capture(httpd_req_t *req)
|
|
{
|
|
esp_err_t gate = reject_if_target_active(req);
|
|
if (gate != ESP_OK) {
|
|
return gate;
|
|
}
|
|
char *body = NULL;
|
|
cJSON *j = NULL;
|
|
if (req->content_len > 0) {
|
|
body = recv_body_alloc(req, 8192, NULL);
|
|
if (!body) {
|
|
return send_json(req, cJSON_CreateString("body too large"), 400);
|
|
}
|
|
j = cJSON_Parse(body);
|
|
free(body);
|
|
if (!j) {
|
|
return send_json(req, cJSON_CreateString("bad json"), 400);
|
|
}
|
|
} else {
|
|
j = cJSON_CreateObject();
|
|
}
|
|
char mode[16] = "auto";
|
|
(void)copy_json_string(cJSON_GetObjectItemCaseSensitive(j, "mode"), mode, sizeof(mode));
|
|
int max_pages = json_get_int_default(j, "maxPages", 64);
|
|
bool variations = json_get_bool_default(j, "variations", false);
|
|
uint8_t extra[96 * 6];
|
|
size_t extra_n = 0;
|
|
cJSON *keys = cJSON_GetObjectItemCaseSensitive(j, "keysHex");
|
|
if (cJSON_IsArray(keys)) {
|
|
int n = cJSON_GetArraySize(keys);
|
|
for (int i = 0; i < n && extra_n < 96; i++) {
|
|
cJSON *it = cJSON_GetArrayItem(keys, i);
|
|
if (cJSON_IsString(it) && it->valuestring && hex_to_bin(it->valuestring, extra + extra_n * 6, 6)) {
|
|
extra_n++;
|
|
}
|
|
}
|
|
}
|
|
cJSON_Delete(j);
|
|
|
|
nfc_tag_info_t tag;
|
|
nfc_access_lock();
|
|
esp_err_t err = nfc_poll_passive_target(&tag);
|
|
if (err != ESP_OK) {
|
|
nfc_access_unlock();
|
|
return send_json(req, cJSON_CreateString("no tag present"), 400);
|
|
}
|
|
cJSON *out = NULL;
|
|
if (strcmp(mode, "classic") == 0) {
|
|
if (!nfc_tag_is_mifare_classic(&tag)) {
|
|
nfc_access_unlock();
|
|
return send_json(req, cJSON_CreateString("tag is not MIFARE Classic"), 400);
|
|
}
|
|
out = build_classic_capture_locked(&tag, extra_n ? extra : NULL, extra_n, variations);
|
|
} else if (strcmp(mode, "type2") == 0) {
|
|
if (!nfc_tag_is_type2(&tag)) {
|
|
nfc_access_unlock();
|
|
return send_json(req, cJSON_CreateString("tag is not Type 2 / Ultralight"), 400);
|
|
}
|
|
out = build_type2_capture_locked(&tag, max_pages);
|
|
} else if (nfc_tag_is_mifare_classic(&tag)) {
|
|
out = build_classic_capture_locked(&tag, extra_n ? extra : NULL, extra_n, variations);
|
|
} else if (nfc_tag_is_type2(&tag)) {
|
|
out = build_type2_capture_locked(&tag, max_pages);
|
|
}
|
|
nfc_access_unlock();
|
|
if (!out) {
|
|
return send_json(req, cJSON_CreateString("capture unsupported or failed"), 400);
|
|
}
|
|
return send_json(req, out, 200);
|
|
}
|
|
|
|
static esp_err_t api_clone_program(httpd_req_t *req)
|
|
{
|
|
esp_err_t gate = reject_if_target_active(req);
|
|
if (gate != ESP_OK) {
|
|
return gate;
|
|
}
|
|
char *body = recv_body_alloc(req, 65536, NULL);
|
|
if (!body) {
|
|
return send_json(req, cJSON_CreateString("snapshot body too large"), 400);
|
|
}
|
|
cJSON *j = cJSON_Parse(body);
|
|
free(body);
|
|
if (!j) {
|
|
return send_json(req, cJSON_CreateString("bad json"), 400);
|
|
}
|
|
cJSON *snapshot = cJSON_GetObjectItemCaseSensitive(j, "snapshot");
|
|
cJSON *format = snapshot ? cJSON_GetObjectItemCaseSensitive(snapshot, "format") : NULL;
|
|
bool include_trailers = json_get_bool_default(j, "includeTrailers", false);
|
|
bool include_lock_pages = json_get_bool_default(j, "includeLockPages", false);
|
|
if (!cJSON_IsObject(snapshot) || !cJSON_IsString(format) || !format->valuestring) {
|
|
cJSON_Delete(j);
|
|
return send_json(req, cJSON_CreateString("snapshot.format required"), 400);
|
|
}
|
|
|
|
nfc_tag_info_t tag;
|
|
cJSON *out = NULL;
|
|
nfc_access_lock();
|
|
if (strcmp(format->valuestring, "type2") == 0) {
|
|
esp_err_t err = ensure_ultralight_tag_locked(&tag);
|
|
if (err == ESP_OK) {
|
|
out = program_type2_snapshot_locked(snapshot, include_lock_pages);
|
|
}
|
|
} else if (strncmp(format->valuestring, "mifareClassic", 13) == 0) {
|
|
esp_err_t err = ensure_mifare_classic_tag_locked(&tag);
|
|
if (err == ESP_OK) {
|
|
out = program_classic_snapshot_locked(&tag, snapshot, include_trailers);
|
|
}
|
|
}
|
|
nfc_access_unlock();
|
|
cJSON_Delete(j);
|
|
if (!out) {
|
|
return send_json(req, cJSON_CreateString("program failed or unsupported target"), 400);
|
|
}
|
|
return send_json(req, out, 200);
|
|
}
|
|
|
|
static esp_err_t api_mifare_read(httpd_req_t *req)
|
|
{
|
|
esp_err_t gate = reject_if_target_active(req);
|
|
if (gate != ESP_OK) {
|
|
return gate;
|
|
}
|
|
char buf[512];
|
|
char key_hex[13];
|
|
int r = recv_body_capped(req, buf, sizeof(buf));
|
|
if (r < 0) {
|
|
return send_json(req, cJSON_CreateString("no body or too large"), 400);
|
|
}
|
|
cJSON *j = cJSON_Parse(buf);
|
|
if (!j) {
|
|
return send_json(req, cJSON_CreateString("bad json"), 400);
|
|
}
|
|
cJSON *jbk = cJSON_GetObjectItem(j, "block");
|
|
cJSON *jk = cJSON_GetObjectItem(j, "key");
|
|
int block = cJSON_IsNumber(jbk) ? (int)cJSON_GetNumberValue(jbk) : -1;
|
|
cJSON *jb = cJSON_GetObjectItem(j, "keyB");
|
|
bool key_b = cJSON_IsTrue(jb);
|
|
bool have_key = copy_json_string(jk, key_hex, sizeof(key_hex));
|
|
cJSON_Delete(j);
|
|
if (block < 0 || block > 255 || !have_key) {
|
|
return send_json(req, cJSON_CreateString("block/key required (block 0-255)"), 400);
|
|
}
|
|
uint8_t keyb[6];
|
|
if (!hex_to_bin(key_hex, keyb, 6)) {
|
|
return send_json(req, cJSON_CreateString("key must be 12 hex chars"), 400);
|
|
}
|
|
nfc_tag_info_t tag;
|
|
nfc_access_lock();
|
|
esp_err_t err = ensure_mifare_classic_tag_locked(&tag);
|
|
if (err == ESP_ERR_NOT_SUPPORTED) {
|
|
nfc_access_unlock();
|
|
return send_json(req, cJSON_CreateString("tag is not MIFARE Classic"), 400);
|
|
}
|
|
if (err != ESP_OK) {
|
|
nfc_access_unlock();
|
|
return send_json(req, cJSON_CreateString("no tag"), 400);
|
|
}
|
|
nfc_mifare_key_t k = {.key_b = key_b};
|
|
memcpy(k.key, keyb, 6);
|
|
if (nfc_mifare_authenticate_block(&tag, (uint8_t)block, &k) != ESP_OK) {
|
|
nfc_access_unlock();
|
|
return send_json(req, cJSON_CreateString("auth failed"), 400);
|
|
}
|
|
uint8_t blk[NFC_BLOCK_LEN];
|
|
if (nfc_mifare_read_block((uint8_t)block, blk) != ESP_OK) {
|
|
nfc_access_unlock();
|
|
return send_json(req, cJSON_CreateString("read failed"), 400);
|
|
}
|
|
nfc_access_unlock();
|
|
char hexout[NFC_BLOCK_LEN * 2 + 1];
|
|
for (int i = 0; i < NFC_BLOCK_LEN; i++) {
|
|
snprintf(hexout + i * 2, 3, "%02X", blk[i]);
|
|
}
|
|
cJSON *o = cJSON_CreateObject();
|
|
cJSON_AddNumberToObject(o, "block", block);
|
|
cJSON_AddStringToObject(o, "data", hexout);
|
|
return send_json(req, o, 200);
|
|
}
|
|
|
|
static esp_err_t api_mifare_write(httpd_req_t *req)
|
|
{
|
|
esp_err_t gate = reject_if_target_active(req);
|
|
if (gate != ESP_OK) {
|
|
return gate;
|
|
}
|
|
char buf[512];
|
|
char key_hex[13];
|
|
char data_hex[33];
|
|
int r = recv_body_capped(req, buf, sizeof(buf));
|
|
if (r < 0) {
|
|
return send_json(req, cJSON_CreateString("no body or too large"), 400);
|
|
}
|
|
cJSON *j = cJSON_Parse(buf);
|
|
if (!j) {
|
|
return send_json(req, cJSON_CreateString("bad json"), 400);
|
|
}
|
|
cJSON *jbk = cJSON_GetObjectItem(j, "block");
|
|
cJSON *jk = cJSON_GetObjectItem(j, "key");
|
|
cJSON *jd = cJSON_GetObjectItem(j, "data");
|
|
int block = cJSON_IsNumber(jbk) ? (int)cJSON_GetNumberValue(jbk) : -1;
|
|
cJSON *jb = cJSON_GetObjectItem(j, "keyB");
|
|
bool key_b = cJSON_IsTrue(jb);
|
|
bool have_key = copy_json_string(jk, key_hex, sizeof(key_hex));
|
|
bool have_data = copy_json_string(jd, data_hex, sizeof(data_hex));
|
|
cJSON_Delete(j);
|
|
if (block < 0 || block > 255 || !have_key || !have_data) {
|
|
return send_json(req, cJSON_CreateString("block/key/data required (block 0-255)"), 400);
|
|
}
|
|
uint8_t keyb[6], blk[NFC_BLOCK_LEN];
|
|
if (!hex_to_bin(key_hex, keyb, 6) || !hex_to_bin(data_hex, blk, NFC_BLOCK_LEN)) {
|
|
return send_json(req, cJSON_CreateString("bad hex"), 400);
|
|
}
|
|
nfc_tag_info_t tag;
|
|
nfc_access_lock();
|
|
esp_err_t err = ensure_mifare_classic_tag_locked(&tag);
|
|
if (err == ESP_ERR_NOT_SUPPORTED) {
|
|
nfc_access_unlock();
|
|
return send_json(req, cJSON_CreateString("tag is not MIFARE Classic"), 400);
|
|
}
|
|
if (err != ESP_OK) {
|
|
nfc_access_unlock();
|
|
return send_json(req, cJSON_CreateString("no tag"), 400);
|
|
}
|
|
nfc_mifare_key_t k = {.key_b = key_b};
|
|
memcpy(k.key, keyb, 6);
|
|
if (nfc_mifare_authenticate_block(&tag, (uint8_t)block, &k) != ESP_OK) {
|
|
nfc_access_unlock();
|
|
return send_json(req, cJSON_CreateString("auth failed"), 400);
|
|
}
|
|
if (nfc_mifare_write_block((uint8_t)block, blk) != ESP_OK) {
|
|
nfc_access_unlock();
|
|
return send_json(req, cJSON_CreateString("write failed"), 400);
|
|
}
|
|
nfc_access_unlock();
|
|
return send_json(req, cJSON_CreateObject(), 200);
|
|
}
|
|
|
|
static esp_err_t api_ul_read(httpd_req_t *req)
|
|
{
|
|
esp_err_t gate = reject_if_target_active(req);
|
|
if (gate != ESP_OK) {
|
|
return gate;
|
|
}
|
|
char buf[128];
|
|
int r = recv_body_capped(req, buf, sizeof(buf));
|
|
if (r < 0) {
|
|
return send_json(req, cJSON_CreateString("no body or too large"), 400);
|
|
}
|
|
cJSON *j = cJSON_Parse(buf);
|
|
if (!j) {
|
|
return send_json(req, cJSON_CreateString("bad json"), 400);
|
|
}
|
|
cJSON *jp = cJSON_GetObjectItem(j, "page");
|
|
int page = cJSON_IsNumber(jp) ? (int)cJSON_GetNumberValue(jp) : -1;
|
|
cJSON_Delete(j);
|
|
if (page < 0 || page > 255) {
|
|
return send_json(req, cJSON_CreateString("page required (0-255)"), 400);
|
|
}
|
|
uint8_t d[4];
|
|
nfc_tag_info_t tag;
|
|
nfc_access_lock();
|
|
esp_err_t e = ensure_ultralight_tag_locked(&tag);
|
|
if (e == ESP_ERR_NOT_SUPPORTED) {
|
|
nfc_access_unlock();
|
|
return send_json(req, cJSON_CreateString("tag is not Ultralight/NTAG"), 400);
|
|
}
|
|
if (e != ESP_OK) {
|
|
nfc_access_unlock();
|
|
return send_json(req, cJSON_CreateString("no tag"), 400);
|
|
}
|
|
if (nfc_ultralight_read_page((uint8_t)page, d) != ESP_OK) {
|
|
nfc_access_unlock();
|
|
return send_json(req, cJSON_CreateString("read failed"), 400);
|
|
}
|
|
nfc_access_unlock();
|
|
char hx[9];
|
|
snprintf(hx, sizeof hx, "%02X%02X%02X%02X", d[0], d[1], d[2], d[3]);
|
|
cJSON *o = cJSON_CreateObject();
|
|
cJSON_AddNumberToObject(o, "page", page);
|
|
cJSON_AddStringToObject(o, "data", hx);
|
|
return send_json(req, o, 200);
|
|
}
|
|
|
|
static esp_err_t api_ul_write(httpd_req_t *req)
|
|
{
|
|
esp_err_t gate = reject_if_target_active(req);
|
|
if (gate != ESP_OK) {
|
|
return gate;
|
|
}
|
|
char buf[128];
|
|
char data_hex[9];
|
|
int r = recv_body_capped(req, buf, sizeof(buf));
|
|
if (r < 0) {
|
|
return send_json(req, cJSON_CreateString("no body or too large"), 400);
|
|
}
|
|
cJSON *j = cJSON_Parse(buf);
|
|
if (!j) {
|
|
return send_json(req, cJSON_CreateString("bad json"), 400);
|
|
}
|
|
cJSON *jp = cJSON_GetObjectItem(j, "page");
|
|
cJSON *jd = cJSON_GetObjectItem(j, "data");
|
|
int page = cJSON_IsNumber(jp) ? (int)cJSON_GetNumberValue(jp) : -1;
|
|
bool have_data = copy_json_string(jd, data_hex, sizeof(data_hex));
|
|
cJSON_Delete(j);
|
|
if (page < 0 || page > 255 || !have_data) {
|
|
return send_json(req, cJSON_CreateString("page and data (8 hex) required (page 0-255)"), 400);
|
|
}
|
|
uint8_t d[4];
|
|
if (!hex_to_bin(data_hex, d, 4)) {
|
|
return send_json(req, cJSON_CreateString("data must be 8 hex chars (4 bytes)"), 400);
|
|
}
|
|
nfc_tag_info_t tag;
|
|
nfc_access_lock();
|
|
esp_err_t e = ensure_ultralight_tag_locked(&tag);
|
|
if (e == ESP_ERR_NOT_SUPPORTED) {
|
|
nfc_access_unlock();
|
|
return send_json(req, cJSON_CreateString("tag is not Ultralight/NTAG"), 400);
|
|
}
|
|
if (e != ESP_OK) {
|
|
nfc_access_unlock();
|
|
return send_json(req, cJSON_CreateString("no tag"), 400);
|
|
}
|
|
if (nfc_ultralight_write_page((uint8_t)page, d) != ESP_OK) {
|
|
nfc_access_unlock();
|
|
return send_json(req, cJSON_CreateString("write failed"), 400);
|
|
}
|
|
nfc_access_unlock();
|
|
cJSON *o = cJSON_CreateObject();
|
|
cJSON_AddNumberToObject(o, "page", page);
|
|
cJSON_AddBoolToObject(o, "ok", true);
|
|
return send_json(req, o, 200);
|
|
}
|
|
|
|
static esp_err_t api_target_status(httpd_req_t *req)
|
|
{
|
|
char drain[64];
|
|
(void)recv_body_capped(req, drain, sizeof(drain));
|
|
cJSON *o = cJSON_CreateObject();
|
|
cJSON_AddBoolToObject(o, "active", s_target_active);
|
|
return send_json(req, o, 200);
|
|
}
|
|
|
|
static esp_err_t api_target_start(httpd_req_t *req)
|
|
{
|
|
char *body = req->content_len ? recv_body_alloc(req, 4096, NULL) : NULL;
|
|
cJSON *j = body ? cJSON_Parse(body) : cJSON_CreateObject();
|
|
free(body);
|
|
if (!j) {
|
|
return send_json(req, cJSON_CreateString("bad json"), 400);
|
|
}
|
|
char raw_params_hex[600] = {0};
|
|
char sens_res_hex[5] = "0400";
|
|
char nfcid1t_hex[7] = "112233";
|
|
char sel_res_hex[3] = "20";
|
|
char felica_hex[37] = "01FEA2A3A4A5A6A7C0C1C2C3C4C5C6C7FFFF";
|
|
char nfcid3t_hex[21] = "F0140102030405060708";
|
|
char gt_hex[256] = {0};
|
|
char tk_hex[256] = {0};
|
|
(void)copy_json_string(cJSON_GetObjectItemCaseSensitive(j, "rawParamsHex"), raw_params_hex, sizeof(raw_params_hex));
|
|
(void)copy_json_string(cJSON_GetObjectItemCaseSensitive(j, "sensResHex"), sens_res_hex, sizeof(sens_res_hex));
|
|
(void)copy_json_string(cJSON_GetObjectItemCaseSensitive(j, "nfcid1tHex"), nfcid1t_hex, sizeof(nfcid1t_hex));
|
|
(void)copy_json_string(cJSON_GetObjectItemCaseSensitive(j, "selResHex"), sel_res_hex, sizeof(sel_res_hex));
|
|
(void)copy_json_string(cJSON_GetObjectItemCaseSensitive(j, "felicaParamsHex"), felica_hex, sizeof(felica_hex));
|
|
(void)copy_json_string(cJSON_GetObjectItemCaseSensitive(j, "nfcid3tHex"), nfcid3t_hex, sizeof(nfcid3t_hex));
|
|
(void)copy_json_string(cJSON_GetObjectItemCaseSensitive(j, "generalTargetHex"), gt_hex, sizeof(gt_hex));
|
|
(void)copy_json_string(cJSON_GetObjectItemCaseSensitive(j, "historicalTargetHex"), tk_hex, sizeof(tk_hex));
|
|
int mode_byte = json_get_int_default(j, "modeByte", 0x04);
|
|
int timeout_ms = json_get_int_default(j, "timeoutMs", 1000);
|
|
cJSON_Delete(j);
|
|
|
|
uint8_t params[256];
|
|
size_t params_len = 0;
|
|
if (raw_params_hex[0]) {
|
|
if (!hex_decode_flex(raw_params_hex, params, sizeof(params), ¶ms_len)) {
|
|
return send_json(req, cJSON_CreateString("rawParamsHex invalid"), 400);
|
|
}
|
|
} else {
|
|
uint8_t sens_res[2], nfcid1t[3], sel_res[1], felica[18], nfcid3t[10], gt[127], tk[127];
|
|
size_t gt_len = 0, tk_len = 0;
|
|
if (!hex_to_bin(sens_res_hex, sens_res, sizeof(sens_res)) ||
|
|
!hex_to_bin(nfcid1t_hex, nfcid1t, sizeof(nfcid1t)) ||
|
|
!hex_to_bin(sel_res_hex, sel_res, sizeof(sel_res)) ||
|
|
!hex_to_bin(felica_hex, felica, sizeof(felica)) ||
|
|
!hex_to_bin(nfcid3t_hex, nfcid3t, sizeof(nfcid3t)) ||
|
|
(gt_hex[0] && !hex_decode_flex(gt_hex, gt, sizeof(gt), >_len)) ||
|
|
(tk_hex[0] && !hex_decode_flex(tk_hex, tk, sizeof(tk), &tk_len))) {
|
|
return send_json(req, cJSON_CreateString("target parameter hex invalid"), 400);
|
|
}
|
|
params[params_len++] = (uint8_t)mode_byte;
|
|
memcpy(params + params_len, sens_res, sizeof(sens_res));
|
|
params_len += sizeof(sens_res);
|
|
memcpy(params + params_len, nfcid1t, sizeof(nfcid1t));
|
|
params_len += sizeof(nfcid1t);
|
|
memcpy(params + params_len, sel_res, sizeof(sel_res));
|
|
params_len += sizeof(sel_res);
|
|
memcpy(params + params_len, felica, sizeof(felica));
|
|
params_len += sizeof(felica);
|
|
memcpy(params + params_len, nfcid3t, sizeof(nfcid3t));
|
|
params_len += sizeof(nfcid3t);
|
|
params[params_len++] = (uint8_t)gt_len;
|
|
if (gt_len) {
|
|
memcpy(params + params_len, gt, gt_len);
|
|
params_len += gt_len;
|
|
}
|
|
params[params_len++] = (uint8_t)tk_len;
|
|
if (tk_len) {
|
|
memcpy(params + params_len, tk, tk_len);
|
|
params_len += tk_len;
|
|
}
|
|
}
|
|
|
|
uint8_t resp[128];
|
|
size_t rlen = 0;
|
|
bool prev_scan = s_scan;
|
|
s_scan = false;
|
|
nfc_access_lock();
|
|
esp_err_t err = pn532_tg_init_as_target(params, params_len, resp, sizeof(resp), &rlen, timeout_ms);
|
|
nfc_access_unlock();
|
|
if (err != ESP_OK) {
|
|
s_scan = prev_scan;
|
|
cJSON *o = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(o, "error", esp_err_to_name(err));
|
|
return send_json(req, o, 400);
|
|
}
|
|
s_target_active = true;
|
|
cJSON *o = cJSON_CreateObject();
|
|
cJSON_AddBoolToObject(o, "active", true);
|
|
cJSON_AddNumberToObject(o, "modeByte", mode_byte);
|
|
(void)add_hex_to_object(o, "responseHex", resp, rlen);
|
|
return send_json(req, o, 200);
|
|
}
|
|
|
|
static esp_err_t api_target_recv(httpd_req_t *req)
|
|
{
|
|
char buf[128];
|
|
int timeout_ms = 15000;
|
|
int r = recv_body_capped(req, buf, sizeof(buf));
|
|
if (r > 0) {
|
|
cJSON *j = cJSON_Parse(buf);
|
|
if (j) {
|
|
timeout_ms = json_get_int_default(j, "timeoutMs", timeout_ms);
|
|
cJSON_Delete(j);
|
|
}
|
|
}
|
|
if (!s_target_active) {
|
|
return send_json(req, cJSON_CreateString("target mode not active"), 409);
|
|
}
|
|
uint8_t resp[300];
|
|
size_t rlen = 0;
|
|
nfc_access_lock();
|
|
esp_err_t err = pn532_tg_get_data(resp, sizeof(resp), &rlen, timeout_ms);
|
|
nfc_access_unlock();
|
|
if (err != ESP_OK) {
|
|
cJSON *o = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(o, "error", esp_err_to_name(err));
|
|
return send_json(req, o, 400);
|
|
}
|
|
cJSON *o = cJSON_CreateObject();
|
|
uint8_t status = rlen ? resp[0] : 0xFF;
|
|
cJSON_AddBoolToObject(o, "ok", status == 0x00);
|
|
cJSON_AddNumberToObject(o, "status", status);
|
|
if (rlen > 1) {
|
|
(void)add_hex_to_object(o, "dataHex", resp + 1, rlen - 1);
|
|
} else {
|
|
cJSON_AddStringToObject(o, "dataHex", "");
|
|
}
|
|
return send_json(req, o, 200);
|
|
}
|
|
|
|
static esp_err_t api_target_send(httpd_req_t *req)
|
|
{
|
|
char buf[1024];
|
|
int r = recv_body_capped(req, buf, sizeof(buf));
|
|
if (r < 0) {
|
|
return send_json(req, cJSON_CreateString("body required"), 400);
|
|
}
|
|
cJSON *j = cJSON_Parse(buf);
|
|
if (!j) {
|
|
return send_json(req, cJSON_CreateString("bad json"), 400);
|
|
}
|
|
char data_hex[521] = {0};
|
|
(void)copy_json_string(cJSON_GetObjectItemCaseSensitive(j, "dataHex"), data_hex, sizeof(data_hex));
|
|
int timeout_ms = json_get_int_default(j, "timeoutMs", 1000);
|
|
cJSON_Delete(j);
|
|
if (!s_target_active) {
|
|
return send_json(req, cJSON_CreateString("target mode not active"), 409);
|
|
}
|
|
uint8_t data[260];
|
|
size_t data_len = 0;
|
|
if (!data_hex[0] || !hex_decode_flex(data_hex, data, sizeof(data), &data_len)) {
|
|
return send_json(req, cJSON_CreateString("dataHex required"), 400);
|
|
}
|
|
uint8_t resp[64];
|
|
size_t rlen = 0;
|
|
nfc_access_lock();
|
|
esp_err_t err = pn532_tg_set_data(data, data_len, resp, sizeof(resp), &rlen, timeout_ms);
|
|
nfc_access_unlock();
|
|
if (err != ESP_OK) {
|
|
cJSON *o = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(o, "error", esp_err_to_name(err));
|
|
return send_json(req, o, 400);
|
|
}
|
|
cJSON *o = cJSON_CreateObject();
|
|
uint8_t status = rlen ? resp[0] : 0xFF;
|
|
cJSON_AddBoolToObject(o, "ok", status == 0x00);
|
|
cJSON_AddNumberToObject(o, "status", status);
|
|
(void)add_hex_to_object(o, "responseHex", resp, rlen);
|
|
return send_json(req, o, 200);
|
|
}
|
|
|
|
static esp_err_t api_target_stop(httpd_req_t *req)
|
|
{
|
|
char drain[64];
|
|
(void)recv_body_capped(req, drain, sizeof(drain));
|
|
nfc_access_lock();
|
|
esp_err_t err = pn532_sam_config_normal();
|
|
nfc_access_unlock();
|
|
s_target_active = false;
|
|
s_scan = true;
|
|
cJSON *o = cJSON_CreateObject();
|
|
cJSON_AddBoolToObject(o, "active", false);
|
|
if (err != ESP_OK) {
|
|
cJSON_AddStringToObject(o, "error", esp_err_to_name(err));
|
|
return send_json(req, o, 400);
|
|
}
|
|
return send_json(req, o, 200);
|
|
}
|
|
|
|
static esp_err_t api_ota_real(httpd_req_t *req)
|
|
{
|
|
char buf[1024];
|
|
int r = recv_body_capped(req, buf, sizeof(buf));
|
|
if (r < 0) {
|
|
return send_json(req, cJSON_CreateString("url required"), 400);
|
|
}
|
|
cJSON *j = cJSON_Parse(buf);
|
|
if (!j) {
|
|
return send_json(req, cJSON_CreateString("bad json"), 400);
|
|
}
|
|
char url[512] = {0};
|
|
(void)copy_json_string(cJSON_GetObjectItemCaseSensitive(j, "url"), url, sizeof(url));
|
|
bool reboot = json_get_bool_default(j, "reboot", true);
|
|
cJSON_Delete(j);
|
|
if (!url[0] || strncmp(url, "https://", 8) != 0) {
|
|
return send_json(req, cJSON_CreateString("https url required"), 400);
|
|
}
|
|
|
|
bool prev_scan = s_scan;
|
|
s_scan = false;
|
|
esp_http_client_config_t http_cfg = {
|
|
.url = url,
|
|
.crt_bundle_attach = esp_crt_bundle_attach,
|
|
.keep_alive_enable = true,
|
|
.timeout_ms = 120000,
|
|
};
|
|
esp_https_ota_config_t ota_cfg = {
|
|
.http_config = &http_cfg,
|
|
};
|
|
esp_err_t err = esp_https_ota(&ota_cfg);
|
|
if (err != ESP_OK) {
|
|
s_scan = prev_scan;
|
|
cJSON *o = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(o, "error", esp_err_to_name(err));
|
|
return send_json(req, o, 500);
|
|
}
|
|
cJSON *o = cJSON_CreateObject();
|
|
cJSON_AddBoolToObject(o, "ok", true);
|
|
cJSON_AddBoolToObject(o, "rebooting", reboot);
|
|
esp_err_t send_err = send_json(req, o, 200);
|
|
if (send_err == ESP_OK && reboot) {
|
|
vTaskDelay(pdMS_TO_TICKS(500));
|
|
esp_restart();
|
|
}
|
|
return send_err;
|
|
}
|
|
|
|
static esp_err_t api_mifare_dictionary_attack(httpd_req_t *req)
|
|
{
|
|
esp_err_t gate = reject_if_target_active(req);
|
|
if (gate != ESP_OK) {
|
|
return gate;
|
|
}
|
|
char *body = recv_body_alloc(req, 8192, NULL);
|
|
if (!body) {
|
|
return send_json(req, cJSON_CreateString("no body or too large"), 400);
|
|
}
|
|
cJSON *j = cJSON_Parse(body);
|
|
free(body);
|
|
if (!j) {
|
|
return send_json(req, cJSON_CreateString("bad json"), 400);
|
|
}
|
|
cJSON *jr = cJSON_GetObjectItem(j, "readerType");
|
|
const char *rtype = cJSON_IsString(jr) ? jr->valuestring : NULL;
|
|
uint8_t s0 = 0;
|
|
uint8_t s1 = 15;
|
|
cJSON *jf = cJSON_GetObjectItem(j, "sectorFirst");
|
|
cJSON *jl = cJSON_GetObjectItem(j, "sectorLast");
|
|
if (cJSON_IsNumber(jf)) {
|
|
s0 = (uint8_t)cJSON_GetNumberValue(jf);
|
|
}
|
|
if (cJSON_IsNumber(jl)) {
|
|
s1 = (uint8_t)cJSON_GetNumberValue(jl);
|
|
} else if (rtype && strcmp(rtype, "classic4k") == 0) {
|
|
s1 = 39;
|
|
}
|
|
bool variations = cJSON_IsTrue(cJSON_GetObjectItem(j, "variations"));
|
|
uint8_t extra[96 * 6];
|
|
size_t extra_n = 0;
|
|
cJSON *keys = cJSON_GetObjectItem(j, "keysHex");
|
|
if (cJSON_IsArray(keys)) {
|
|
int n = cJSON_GetArraySize(keys);
|
|
for (int i = 0; i < n && extra_n < 96; i++) {
|
|
cJSON *it = cJSON_GetArrayItem(keys, i);
|
|
if (!cJSON_IsString(it)) {
|
|
continue;
|
|
}
|
|
if (hex_to_bin(it->valuestring, extra + extra_n * 6, 6)) {
|
|
extra_n++;
|
|
}
|
|
}
|
|
}
|
|
cJSON_Delete(j);
|
|
|
|
nfc_tag_info_t tag;
|
|
nfc_access_lock();
|
|
if (nfc_poll_passive_target(&tag) != ESP_OK) {
|
|
nfc_access_unlock();
|
|
return send_json(req, cJSON_CreateString("no tag present"), 400);
|
|
}
|
|
cJSON *out = nfc_mifare_dictionary_attack(&tag, s0, s1, extra_n ? extra : NULL, extra_n, variations, NULL);
|
|
nfc_access_unlock();
|
|
if (!out) {
|
|
return send_json(req, cJSON_CreateString("attack failed"), 400);
|
|
}
|
|
return send_json(req, out, 200);
|
|
}
|
|
|
|
static esp_err_t api_nfc_emulate_raw(httpd_req_t *req)
|
|
{
|
|
esp_err_t gate = reject_if_target_active(req);
|
|
if (gate != ESP_OK) {
|
|
return gate;
|
|
}
|
|
char buf[1024];
|
|
char hex[521];
|
|
int r = recv_body_capped(req, buf, sizeof(buf));
|
|
if (r < 0) {
|
|
return send_json(req, cJSON_CreateString("no body or too large"), 400);
|
|
}
|
|
cJSON *j = cJSON_Parse(buf);
|
|
if (!j) {
|
|
return send_json(req, cJSON_CreateString("bad json"), 400);
|
|
}
|
|
cJSON *jh = cJSON_GetObjectItem(j, "hex");
|
|
bool have_hex = copy_json_string(jh, hex, sizeof(hex));
|
|
cJSON_Delete(j);
|
|
uint8_t bin[260];
|
|
size_t blen = 0;
|
|
if (!have_hex || !hex_decode_flex(hex, bin, sizeof(bin), &blen)) {
|
|
return send_json(req, cJSON_CreateString("hex command required"), 400);
|
|
}
|
|
uint8_t resp[300];
|
|
size_t rlen = 0;
|
|
nfc_access_lock();
|
|
esp_err_t err = pn532_send_cmd(bin, blen, resp, sizeof(resp), &rlen, 800);
|
|
nfc_access_unlock();
|
|
if (err != ESP_OK) {
|
|
cJSON *o = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(o, "error", esp_err_to_name(err));
|
|
return send_json(req, o, 400);
|
|
}
|
|
char *rh = calloc(1, rlen * 2 + 1);
|
|
if (!rh) {
|
|
return send_json(req, cJSON_CreateString("out of memory"), 500);
|
|
}
|
|
for (size_t i = 0; i < rlen; i++) {
|
|
snprintf(rh + i * 2, 3, "%02X", resp[i]);
|
|
}
|
|
cJSON *o = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(o, "response", rh);
|
|
free(rh);
|
|
return send_json(req, o, 200);
|
|
}
|
|
|
|
static esp_err_t api_raw_pn532(httpd_req_t *req)
|
|
{
|
|
esp_err_t gate = reject_if_target_active(req);
|
|
if (gate != ESP_OK) {
|
|
return gate;
|
|
}
|
|
char buf[1024];
|
|
char hex[521];
|
|
int r = recv_body_capped(req, buf, sizeof(buf));
|
|
if (r < 0) {
|
|
return send_json(req, cJSON_CreateString("no body or too large"), 400);
|
|
}
|
|
cJSON *j = cJSON_Parse(buf);
|
|
if (!j) {
|
|
return send_json(req, cJSON_CreateString("bad json"), 400);
|
|
}
|
|
cJSON *jf = cJSON_GetObjectItem(j, "frame");
|
|
bool have_hex = copy_json_string(jf, hex, sizeof(hex));
|
|
cJSON_Delete(j);
|
|
size_t blen = 0;
|
|
uint8_t bin[260];
|
|
if (!have_hex || !hex_decode_flex(hex, bin, sizeof(bin), &blen)) {
|
|
return send_json(req, cJSON_CreateString("frame hex required, even length, max 260B"), 400);
|
|
}
|
|
uint8_t resp[300];
|
|
size_t rlen = 0;
|
|
nfc_access_lock();
|
|
esp_err_t e = pn532_send_cmd(bin, blen, resp, sizeof(resp), &rlen, 300);
|
|
nfc_access_unlock();
|
|
if (e != ESP_OK) {
|
|
cJSON *o = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(o, "error", esp_err_to_name(e));
|
|
return send_json(req, o, 400);
|
|
}
|
|
char *rh = calloc(1, rlen * 2 + 1);
|
|
if (!rh) {
|
|
return send_json(req, cJSON_CreateString("out of memory"), 500);
|
|
}
|
|
for (size_t i = 0; i < rlen; i++) {
|
|
snprintf(rh + i * 2, 3, "%02X", resp[i]);
|
|
}
|
|
cJSON *o = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(o, "response", rh);
|
|
free(rh);
|
|
return send_json(req, o, 200);
|
|
}
|
|
|
|
static esp_err_t api_cors_preflight(httpd_req_t *req)
|
|
{
|
|
(void)req;
|
|
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
|
|
httpd_resp_set_hdr(req, "Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
httpd_resp_set_hdr(req, "Access-Control-Allow-Headers", "Content-Type");
|
|
httpd_resp_set_hdr(req, "Access-Control-Max-Age", "86400");
|
|
httpd_resp_set_status(req, "204 No Content");
|
|
return httpd_resp_send(req, "", 0);
|
|
}
|
|
|
|
static esp_err_t static_any(httpd_req_t *req)
|
|
{
|
|
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
|
|
if (strcmp(req->uri, "/") == 0) {
|
|
httpd_resp_set_hdr(req, "Cache-Control", "no-cache");
|
|
FILE *f = fopen("/spiffs/index.html", "r");
|
|
if (!f) {
|
|
httpd_resp_send(req, "<h1>PN532 Toolkit</h1><p>Build web UI into /data</p>", HTTPD_RESP_USE_STRLEN);
|
|
return ESP_OK;
|
|
}
|
|
char buf[512];
|
|
size_t n;
|
|
httpd_resp_set_type(req, "text/html");
|
|
while ((n = fread(buf, 1, sizeof(buf), f)) > 0) {
|
|
if (httpd_resp_send_chunk(req, buf, n) != ESP_OK) {
|
|
fclose(f);
|
|
return ESP_FAIL;
|
|
}
|
|
}
|
|
fclose(f);
|
|
return httpd_resp_send_chunk(req, NULL, 0);
|
|
}
|
|
if (strstr(req->uri, "..") != NULL) {
|
|
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "bad path");
|
|
return ESP_FAIL;
|
|
}
|
|
char path[STATIC_PATH_MAX];
|
|
int path_len = snprintf(path, sizeof path, "/spiffs%s", req->uri);
|
|
if (path_len < 0 || path_len >= (int)sizeof(path)) {
|
|
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "path too long");
|
|
return ESP_FAIL;
|
|
}
|
|
struct stat st;
|
|
if (stat(path, &st) != 0) {
|
|
httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "not found");
|
|
return ESP_FAIL;
|
|
}
|
|
FILE *f = fopen(path, "r");
|
|
if (!f) {
|
|
httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "not found");
|
|
return ESP_FAIL;
|
|
}
|
|
if (strstr(req->uri, ".js")) {
|
|
httpd_resp_set_type(req, "application/javascript");
|
|
} else if (strstr(req->uri, ".css")) {
|
|
httpd_resp_set_type(req, "text/css");
|
|
} else if (strstr(req->uri, ".html")) {
|
|
httpd_resp_set_type(req, "text/html");
|
|
}
|
|
char buf[512];
|
|
size_t n;
|
|
while ((n = fread(buf, 1, sizeof(buf), f)) > 0) {
|
|
if (httpd_resp_send_chunk(req, buf, n) != ESP_OK) {
|
|
fclose(f);
|
|
return ESP_FAIL;
|
|
}
|
|
}
|
|
fclose(f);
|
|
return httpd_resp_send_chunk(req, NULL, 0);
|
|
}
|
|
|
|
static esp_err_t ws_handler(httpd_req_t *req)
|
|
{
|
|
if (req->method == HTTP_GET) {
|
|
ESP_LOGI(TAG, "WS handshake");
|
|
return ESP_OK;
|
|
}
|
|
httpd_ws_frame_t ws = {.type = HTTPD_WS_TYPE_TEXT};
|
|
uint8_t buf[128];
|
|
ws.payload = buf;
|
|
esp_err_t ret = httpd_ws_recv_frame(req, &ws, sizeof(buf));
|
|
if (ret != ESP_OK) {
|
|
return ret;
|
|
}
|
|
if (ws.type == HTTPD_WS_TYPE_TEXT && ws.len + 1 < sizeof(buf)) {
|
|
buf[ws.len] = 0;
|
|
if (strcmp((char *)buf, "ping") == 0) {
|
|
ws.type = HTTPD_WS_TYPE_TEXT;
|
|
ws.payload = (uint8_t *)"{\"channel\":\"pong\"}";
|
|
ws.len = strlen((char *)ws.payload);
|
|
return httpd_ws_send_frame(req, &ws);
|
|
}
|
|
}
|
|
return ESP_OK;
|
|
}
|
|
|
|
static void reconnect_task(void *arg)
|
|
{
|
|
(void)arg;
|
|
while (1) {
|
|
vTaskDelay(pdMS_TO_TICKS(5000));
|
|
if (!nfc_engine_is_ready()) {
|
|
nfc_access_lock();
|
|
bool ok = nfc_engine_try_reattach();
|
|
nfc_access_unlock();
|
|
if (ok) {
|
|
ESP_LOGI(TAG, "PN532 reattached — broadcasting status");
|
|
app_net_broadcast_json("pn532", "{\"connected\":true}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void scan_loop_task(void *arg)
|
|
{
|
|
(void)arg;
|
|
nfc_tag_info_t last;
|
|
memset(&last, 0, sizeof(last));
|
|
while (1) {
|
|
if (!nfc_engine_is_ready()) {
|
|
vTaskDelay(pdMS_TO_TICKS(500));
|
|
continue;
|
|
}
|
|
if (!s_scan) {
|
|
vTaskDelay(pdMS_TO_TICKS(200));
|
|
continue;
|
|
}
|
|
if (session_capture_deep_enabled() && session_capture_is_full()) {
|
|
vTaskDelay(pdMS_TO_TICKS(400));
|
|
continue;
|
|
}
|
|
nfc_tag_info_t tag;
|
|
nfc_access_lock();
|
|
esp_err_t e = nfc_poll_passive_target(&tag);
|
|
if (e == ESP_OK) {
|
|
bool is_new = (last.uid_len != tag.uid_len || memcmp(last.uid, tag.uid, tag.uid_len) != 0);
|
|
if (is_new) {
|
|
last = tag;
|
|
cJSON *j = nfc_tag_to_json(&tag);
|
|
if (j) {
|
|
char *raw = cJSON_PrintUnformatted(j);
|
|
cJSON_Delete(j);
|
|
if (raw) {
|
|
app_net_broadcast_json("scan", raw);
|
|
free(raw);
|
|
}
|
|
}
|
|
if (session_capture_deep_enabled() && !session_capture_is_full()) {
|
|
cJSON *deep = nfc_tag_deep_profile(&tag);
|
|
nfc_access_unlock();
|
|
char *line = deep ? cJSON_PrintUnformatted(deep) : NULL;
|
|
cJSON_Delete(deep);
|
|
if (line) {
|
|
if (!session_capture_append_line(line)) {
|
|
cJSON *mini = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(mini, "event", "bufferFull");
|
|
cJSON_AddBoolToObject(mini, "paused", true);
|
|
char *m = cJSON_PrintUnformatted(mini);
|
|
cJSON_Delete(mini);
|
|
if (m) {
|
|
app_net_broadcast_json("capture", m);
|
|
free(m);
|
|
}
|
|
} else {
|
|
size_t u = 0;
|
|
uint32_t lc = 0;
|
|
bool f = false;
|
|
session_capture_get_status(&u, &lc, &f);
|
|
char uh[32];
|
|
tag_uid_hex(&tag, uh, sizeof uh);
|
|
cJSON *ev = cJSON_CreateObject();
|
|
cJSON_AddStringToObject(ev, "event", "recorded");
|
|
cJSON_AddStringToObject(ev, "uid", uh);
|
|
cJSON_AddNumberToObject(ev, "usedBytes", (double)u);
|
|
cJSON_AddNumberToObject(ev, "lines", (double)lc);
|
|
cJSON_AddBoolToObject(ev, "full", f);
|
|
char *es = cJSON_PrintUnformatted(ev);
|
|
cJSON_Delete(ev);
|
|
if (es) {
|
|
app_net_broadcast_json("capture", es);
|
|
free(es);
|
|
}
|
|
}
|
|
free(line);
|
|
}
|
|
} else {
|
|
nfc_access_unlock();
|
|
}
|
|
} else {
|
|
nfc_access_unlock();
|
|
}
|
|
} else {
|
|
nfc_access_unlock();
|
|
if (last.uid_len) {
|
|
memset(&last, 0, sizeof(last));
|
|
app_net_broadcast_json("scan", "{\"present\":false}");
|
|
}
|
|
}
|
|
vTaskDelay(pdMS_TO_TICKS(session_capture_deep_enabled() ? 220 : 65));
|
|
}
|
|
}
|
|
|
|
void app_net_set_continuous_scan(bool on) { s_scan = on; }
|
|
|
|
bool app_net_continuous_scan(void) { return s_scan; }
|
|
|
|
static bool register_uri_checked(httpd_handle_t server, const httpd_uri_t *uri)
|
|
{
|
|
if (!server || !uri) {
|
|
return false;
|
|
}
|
|
esp_err_t err = httpd_register_uri_handler(server, uri);
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "register %s %s failed: %s",
|
|
uri->method == HTTP_GET ? "GET"
|
|
: uri->method == HTTP_POST ? "POST"
|
|
: uri->method == HTTP_OPTIONS ? "OPTIONS"
|
|
: "?",
|
|
uri->uri ? uri->uri : "(null)", esp_err_to_name(err));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static httpd_handle_t start_server(void)
|
|
{
|
|
httpd_config_t cfg = HTTPD_DEFAULT_CONFIG();
|
|
cfg.lru_purge_enable = true;
|
|
cfg.max_uri_handlers = 48;
|
|
cfg.stack_size = 8192;
|
|
httpd_handle_t s = NULL;
|
|
if (httpd_start(&s, &cfg) != ESP_OK) {
|
|
return NULL;
|
|
}
|
|
|
|
httpd_uri_t u = {.uri = "/*", .method = HTTP_OPTIONS, .handler = api_cors_preflight};
|
|
if (!register_uri_checked(s, &u)) {
|
|
httpd_stop(s);
|
|
return NULL;
|
|
}
|
|
|
|
u = (httpd_uri_t){.uri = "/api/status", .method = HTTP_GET, .handler = api_status};
|
|
if (!register_uri_checked(s, &u)) {
|
|
httpd_stop(s);
|
|
return NULL;
|
|
}
|
|
u = (httpd_uri_t){.uri = "/api/nfc/poll", .method = HTTP_POST, .handler = api_nfc_poll};
|
|
if (!register_uri_checked(s, &u)) {
|
|
httpd_stop(s);
|
|
return NULL;
|
|
}
|
|
u = (httpd_uri_t){.uri = "/api/nfc/probe", .method = HTTP_POST, .handler = api_nfc_probe};
|
|
if (!register_uri_checked(s, &u)) {
|
|
httpd_stop(s);
|
|
return NULL;
|
|
}
|
|
u = (httpd_uri_t){.uri = "/api/nfc/scan", .method = HTTP_POST, .handler = api_scan};
|
|
if (!register_uri_checked(s, &u)) {
|
|
httpd_stop(s);
|
|
return NULL;
|
|
}
|
|
u = (httpd_uri_t){.uri = "/api/nfc/target/status", .method = HTTP_POST, .handler = api_target_status};
|
|
if (!register_uri_checked(s, &u)) {
|
|
httpd_stop(s);
|
|
return NULL;
|
|
}
|
|
u = (httpd_uri_t){.uri = "/api/nfc/target/start", .method = HTTP_POST, .handler = api_target_start};
|
|
if (!register_uri_checked(s, &u)) {
|
|
httpd_stop(s);
|
|
return NULL;
|
|
}
|
|
u = (httpd_uri_t){.uri = "/api/nfc/target/recv", .method = HTTP_POST, .handler = api_target_recv};
|
|
if (!register_uri_checked(s, &u)) {
|
|
httpd_stop(s);
|
|
return NULL;
|
|
}
|
|
u = (httpd_uri_t){.uri = "/api/nfc/target/send", .method = HTTP_POST, .handler = api_target_send};
|
|
if (!register_uri_checked(s, &u)) {
|
|
httpd_stop(s);
|
|
return NULL;
|
|
}
|
|
u = (httpd_uri_t){.uri = "/api/nfc/target/stop", .method = HTTP_POST, .handler = api_target_stop};
|
|
if (!register_uri_checked(s, &u)) {
|
|
httpd_stop(s);
|
|
return NULL;
|
|
}
|
|
u = (httpd_uri_t){.uri = "/api/pn532/general-status", .method = HTTP_GET, .handler = api_general_status};
|
|
if (!register_uri_checked(s, &u)) {
|
|
httpd_stop(s);
|
|
return NULL;
|
|
}
|
|
u = (httpd_uri_t){.uri = "/api/mifare/read-block", .method = HTTP_POST, .handler = api_mifare_read};
|
|
if (!register_uri_checked(s, &u)) {
|
|
httpd_stop(s);
|
|
return NULL;
|
|
}
|
|
u = (httpd_uri_t){.uri = "/api/mifare/write-block", .method = HTTP_POST, .handler = api_mifare_write};
|
|
if (!register_uri_checked(s, &u)) {
|
|
httpd_stop(s);
|
|
return NULL;
|
|
}
|
|
u = (httpd_uri_t){.uri = "/api/ul/read-page", .method = HTTP_POST, .handler = api_ul_read};
|
|
if (!register_uri_checked(s, &u)) {
|
|
httpd_stop(s);
|
|
return NULL;
|
|
}
|
|
u = (httpd_uri_t){.uri = "/api/ul/write-page", .method = HTTP_POST, .handler = api_ul_write};
|
|
if (!register_uri_checked(s, &u)) {
|
|
httpd_stop(s);
|
|
return NULL;
|
|
}
|
|
u = (httpd_uri_t){.uri = "/api/raw/pn532", .method = HTTP_POST, .handler = api_raw_pn532};
|
|
if (!register_uri_checked(s, &u)) {
|
|
httpd_stop(s);
|
|
return NULL;
|
|
}
|
|
u = (httpd_uri_t){.uri = "/api/clone/capture", .method = HTTP_POST, .handler = api_clone_capture};
|
|
if (!register_uri_checked(s, &u)) {
|
|
httpd_stop(s);
|
|
return NULL;
|
|
}
|
|
u = (httpd_uri_t){.uri = "/api/clone/program", .method = HTTP_POST, .handler = api_clone_program};
|
|
if (!register_uri_checked(s, &u)) {
|
|
httpd_stop(s);
|
|
return NULL;
|
|
}
|
|
u = (httpd_uri_t){.uri = "/api/ota", .method = HTTP_POST, .handler = api_ota_real};
|
|
if (!register_uri_checked(s, &u)) {
|
|
httpd_stop(s);
|
|
return NULL;
|
|
}
|
|
u = (httpd_uri_t){.uri = "/api/session/export", .method = HTTP_GET, .handler = api_session_export};
|
|
if (!register_uri_checked(s, &u)) {
|
|
httpd_stop(s);
|
|
return NULL;
|
|
}
|
|
u = (httpd_uri_t){.uri = "/api/session/clear", .method = HTTP_POST, .handler = api_session_clear};
|
|
if (!register_uri_checked(s, &u)) {
|
|
httpd_stop(s);
|
|
return NULL;
|
|
}
|
|
u = (httpd_uri_t){.uri = "/api/session/deep", .method = HTTP_POST, .handler = api_session_deep};
|
|
if (!register_uri_checked(s, &u)) {
|
|
httpd_stop(s);
|
|
return NULL;
|
|
}
|
|
u = (httpd_uri_t){.uri = "/api/mifare/dictionary-attack", .method = HTTP_POST,
|
|
.handler = api_mifare_dictionary_attack};
|
|
if (!register_uri_checked(s, &u)) {
|
|
httpd_stop(s);
|
|
return NULL;
|
|
}
|
|
u = (httpd_uri_t){.uri = "/api/nfc/emulate-raw", .method = HTTP_POST, .handler = api_nfc_emulate_raw};
|
|
if (!register_uri_checked(s, &u)) {
|
|
httpd_stop(s);
|
|
return NULL;
|
|
}
|
|
u = (httpd_uri_t){.uri = "/ws", .method = HTTP_GET, .handler = ws_handler, .is_websocket = true};
|
|
if (!register_uri_checked(s, &u)) {
|
|
httpd_stop(s);
|
|
return NULL;
|
|
}
|
|
u = (httpd_uri_t){.uri = "/*", .method = HTTP_GET, .handler = static_any};
|
|
if (!register_uri_checked(s, &u)) {
|
|
httpd_stop(s);
|
|
return NULL;
|
|
}
|
|
|
|
return s;
|
|
}
|
|
|
|
esp_err_t app_net_init(void)
|
|
{
|
|
esp_err_t err = nvs_flash_init();
|
|
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
|
err = nvs_flash_erase();
|
|
if (err != ESP_OK) {
|
|
return err;
|
|
}
|
|
err = nvs_flash_init();
|
|
}
|
|
if (err != ESP_OK) {
|
|
return err;
|
|
}
|
|
|
|
err = esp_netif_init();
|
|
if (err != ESP_OK) {
|
|
return err;
|
|
}
|
|
err = esp_event_loop_create_default();
|
|
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
|
|
return err;
|
|
}
|
|
esp_netif_t *ap_if = esp_netif_create_default_wifi_ap();
|
|
if (!ap_if) {
|
|
return ESP_FAIL;
|
|
}
|
|
s_nfc_op_mu = xSemaphoreCreateMutex();
|
|
if (!s_nfc_op_mu) {
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
|
|
wifi_init_config_t wcfg = WIFI_INIT_CONFIG_DEFAULT();
|
|
err = esp_wifi_init(&wcfg);
|
|
if (err != ESP_OK) {
|
|
return err;
|
|
}
|
|
wifi_config_t ap = {0};
|
|
strncpy((char *)ap.ap.ssid, SOFTAP_SSID, sizeof(ap.ap.ssid));
|
|
ap.ap.ssid_len = (uint8_t)strlen(SOFTAP_SSID);
|
|
ap.ap.channel = 6;
|
|
ap.ap.max_connection = 8;
|
|
ap.ap.authmode = WIFI_AUTH_OPEN;
|
|
err = esp_wifi_set_mode(WIFI_MODE_AP);
|
|
if (err != ESP_OK) {
|
|
return err;
|
|
}
|
|
err = esp_wifi_set_config(WIFI_IF_AP, &ap);
|
|
if (err != ESP_OK) {
|
|
return err;
|
|
}
|
|
err = esp_wifi_start();
|
|
if (err != ESP_OK) {
|
|
return err;
|
|
}
|
|
|
|
esp_vfs_spiffs_conf_t sp = {
|
|
.base_path = "/spiffs",
|
|
.partition_label = "storage",
|
|
.max_files = 16,
|
|
.format_if_mount_failed = true,
|
|
};
|
|
err = esp_vfs_spiffs_register(&sp);
|
|
if (err != ESP_OK) {
|
|
return err;
|
|
}
|
|
|
|
err = mdns_init();
|
|
if (err == ESP_OK) {
|
|
(void)mdns_hostname_set("pn532tool");
|
|
(void)mdns_instance_name_set("PN532 NFC Toolkit");
|
|
(void)mdns_service_add(NULL, "_http", "_tcp", 80, NULL, 0);
|
|
} else {
|
|
ESP_LOGW(TAG, "mDNS init failed: %s", esp_err_to_name(err));
|
|
}
|
|
|
|
s_server = start_server();
|
|
if (!s_server) {
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
if (xTaskCreate(scan_loop_task, "nfc_scan", 20480, NULL, 5, &s_scan_task) != pdPASS) {
|
|
httpd_stop(s_server);
|
|
s_server = NULL;
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
if (xTaskCreate(reconnect_task, "pn532_reconnect", 4096, NULL, 3, NULL) != pdPASS) {
|
|
ESP_LOGW(TAG, "reconnect task create failed — hot-plug retry disabled");
|
|
}
|
|
return ESP_OK;
|
|
}
|