Add PN532 toolkit firmware, web UI, and embedded SPIFFS assets
Includes ESP-IDF NFC stack (deep capture, 4K Classic geometry, UL write API, open SoftAP), React dashboard with live tag diagnostics, and docs. README updated for APIs and lab Wi-Fi defaults. Made-with: Cursor
This commit is contained in:
5
firmware/CMakeLists.txt
Normal file
5
firmware/CMakeLists.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
# PN532 NFC Toolkit — ESP-IDF root
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(pn532_nfc_toolkit)
|
||||
6
firmware/components/net_service/CMakeLists.txt
Normal file
6
firmware/components/net_service/CMakeLists.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
idf_component_register(
|
||||
SRCS "app_net.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES esp_http_server http_parser esp_wifi esp_netif nvs_flash mdns esp_timer
|
||||
json spiffs vfs freertos nfc_engine pn532_host
|
||||
)
|
||||
908
firmware/components/net_service/app_net.c
Normal file
908
firmware/components/net_service/app_net.c
Normal file
@@ -0,0 +1,908 @@
|
||||
#include "net_service/app_net.h"
|
||||
#include "esp_log.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/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 "http_parser.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 MAX_JSON 4096
|
||||
#define WS_BROADCAST_BUF 4096
|
||||
|
||||
static httpd_handle_t s_server;
|
||||
static bool s_scan = true;
|
||||
static TaskHandle_t s_scan_task;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/** 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)
|
||||
{
|
||||
(void)channel;
|
||||
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_frame_async(s_server, fds[i], &pkt);
|
||||
}
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t send_json(httpd_req_t *req, cJSON *j, int status)
|
||||
{
|
||||
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");
|
||||
httpd_resp_set_status(req, status == 200 ? "200 OK" : "400 Bad Request");
|
||||
esp_err_t e = httpd_resp_send(req, p, HTTPD_RESP_USE_STRLEN);
|
||||
free(p);
|
||||
return e;
|
||||
}
|
||||
|
||||
static esp_err_t api_status(httpd_req_t *req)
|
||||
{
|
||||
cJSON *o = cJSON_CreateObject();
|
||||
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);
|
||||
uint8_t ic = 0, hi = 0, lo = 0;
|
||||
if (pn532_get_firmware_version(&ic, &hi, &lo) == ESP_OK) {
|
||||
cJSON *pn = cJSON_CreateObject();
|
||||
cJSON_AddNumberToObject(pn, "ic", ic);
|
||||
cJSON_AddNumberToObject(pn, "fwHi", hi);
|
||||
cJSON_AddNumberToObject(pn, "fwLo", lo);
|
||||
cJSON_AddItemToObject(o, "pn532", pn);
|
||||
}
|
||||
cJSON_AddBoolToObject(o, "scanning", s_scan);
|
||||
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();
|
||||
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"), 400);
|
||||
}
|
||||
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);
|
||||
}
|
||||
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)
|
||||
{
|
||||
char drain[128];
|
||||
(void)recv_body_capped(req, drain, sizeof(drain));
|
||||
|
||||
nfc_tag_info_t tag;
|
||||
esp_err_t e = nfc_poll_passive_target(&tag);
|
||||
if (e == ESP_ERR_NOT_FOUND) {
|
||||
cJSON *o = cJSON_CreateObject();
|
||||
cJSON_AddBoolToObject(o, "present", false);
|
||||
return send_json(req, o, 200);
|
||||
}
|
||||
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_AddBoolToObject(o, "present", true);
|
||||
cJSON_AddItemToObject(o, "tag", nfc_tag_to_json(&tag));
|
||||
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);
|
||||
}
|
||||
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)) {
|
||||
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)
|
||||
{
|
||||
uint8_t gs[32];
|
||||
size_t gl = 0;
|
||||
esp_err_t e = pn532_get_general_status(gs, sizeof(gs), &gl);
|
||||
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();
|
||||
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_mifare_read(httpd_req_t *req)
|
||||
{
|
||||
char buf[512];
|
||||
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;
|
||||
const char *key_hex = cJSON_IsString(jk) ? jk->valuestring : NULL;
|
||||
cJSON *jb = cJSON_GetObjectItem(j, "keyB");
|
||||
bool key_b = cJSON_IsTrue(jb);
|
||||
cJSON_Delete(j);
|
||||
if (block < 0 || !key_hex) {
|
||||
return send_json(req, cJSON_CreateString("block/key required"), 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;
|
||||
if (nfc_poll_passive_target(&tag) != ESP_OK) {
|
||||
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) {
|
||||
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) {
|
||||
return send_json(req, cJSON_CreateString("read failed"), 400);
|
||||
}
|
||||
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)
|
||||
{
|
||||
char buf[512];
|
||||
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;
|
||||
const char *key_hex = cJSON_IsString(jk) ? jk->valuestring : NULL;
|
||||
const char *data_hex = cJSON_IsString(jd) ? jd->valuestring : NULL;
|
||||
cJSON *jb = cJSON_GetObjectItem(j, "keyB");
|
||||
bool key_b = cJSON_IsTrue(jb);
|
||||
cJSON_Delete(j);
|
||||
if (block < 0 || !key_hex || !data_hex) {
|
||||
return send_json(req, cJSON_CreateString("block/key/data required"), 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;
|
||||
if (nfc_poll_passive_target(&tag) != ESP_OK) {
|
||||
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) {
|
||||
return send_json(req, cJSON_CreateString("auth failed"), 400);
|
||||
}
|
||||
if (nfc_mifare_write_block((uint8_t)block, blk) != ESP_OK) {
|
||||
return send_json(req, cJSON_CreateString("write failed"), 400);
|
||||
}
|
||||
return send_json(req, cJSON_CreateObject(), 200);
|
||||
}
|
||||
|
||||
static esp_err_t api_ul_read(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);
|
||||
}
|
||||
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) {
|
||||
return send_json(req, cJSON_CreateString("page required"), 400);
|
||||
}
|
||||
uint8_t d[4];
|
||||
if (nfc_ultralight_read_page((uint8_t)page, d) != ESP_OK) {
|
||||
return send_json(req, cJSON_CreateString("read failed"), 400);
|
||||
}
|
||||
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)
|
||||
{
|
||||
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");
|
||||
cJSON *jd = cJSON_GetObjectItem(j, "data");
|
||||
int page = cJSON_IsNumber(jp) ? (int)cJSON_GetNumberValue(jp) : -1;
|
||||
const char *data_hex = cJSON_IsString(jd) ? jd->valuestring : NULL;
|
||||
cJSON_Delete(j);
|
||||
if (page < 0 || !data_hex) {
|
||||
return send_json(req, cJSON_CreateString("page and data (8 hex) required"), 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);
|
||||
}
|
||||
if (nfc_ultralight_write_page((uint8_t)page, d) != ESP_OK) {
|
||||
return send_json(req, cJSON_CreateString("write failed"), 400);
|
||||
}
|
||||
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_ota_stub(httpd_req_t *req)
|
||||
{
|
||||
(void)req;
|
||||
cJSON *o = cJSON_CreateString("Use idf.py app-flash or extend with esp_https_ota + bundle URL");
|
||||
char *p = cJSON_PrintUnformatted(o);
|
||||
cJSON_Delete(o);
|
||||
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_status(req, "501 Not Implemented");
|
||||
esp_err_t e = httpd_resp_send(req, p, HTTPD_RESP_USE_STRLEN);
|
||||
free(p);
|
||||
return e;
|
||||
}
|
||||
|
||||
static esp_err_t api_mifare_dictionary_attack(httpd_req_t *req)
|
||||
{
|
||||
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;
|
||||
if (nfc_poll_passive_target(&tag) != ESP_OK) {
|
||||
return send_json(req, cJSON_CreateString("no tag present"), 400);
|
||||
}
|
||||
int attempts = 0;
|
||||
cJSON *out = nfc_mifare_dictionary_attack(&tag, s0, s1, extra_n ? extra : NULL, extra_n, variations, &attempts);
|
||||
if (!out) {
|
||||
return send_json(req, cJSON_CreateString("attack failed"), 400);
|
||||
}
|
||||
char *p = cJSON_PrintUnformatted(out);
|
||||
cJSON_Delete(out);
|
||||
if (!p) {
|
||||
return send_json(req, cJSON_CreateString("print failed"), 400);
|
||||
}
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
|
||||
httpd_resp_set_status(req, "200 OK");
|
||||
esp_err_t e = httpd_resp_send(req, p, HTTPD_RESP_USE_STRLEN);
|
||||
free(p);
|
||||
return e;
|
||||
}
|
||||
|
||||
static esp_err_t api_nfc_emulate_raw(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("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");
|
||||
const char *hex = cJSON_IsString(jh) ? jh->valuestring : NULL;
|
||||
cJSON_Delete(j);
|
||||
uint8_t bin[260];
|
||||
size_t blen = 0;
|
||||
if (!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;
|
||||
esp_err_t err = pn532_send_cmd(bin, blen, resp, sizeof(resp), &rlen, 800);
|
||||
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);
|
||||
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)
|
||||
{
|
||||
char buf[1024];
|
||||
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");
|
||||
const char *hex = cJSON_IsString(jf) ? jf->valuestring : NULL;
|
||||
cJSON_Delete(j);
|
||||
size_t blen = 0;
|
||||
uint8_t bin[260];
|
||||
if (!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;
|
||||
esp_err_t e = pn532_send_cmd(bin, blen, resp, sizeof(resp), &rlen, 300);
|
||||
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);
|
||||
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)
|
||||
{
|
||||
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");
|
||||
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
|
||||
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[96];
|
||||
snprintf(path, sizeof path, "/spiffs%s", req->uri);
|
||||
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");
|
||||
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
|
||||
} else if (strstr(req->uri, ".css")) {
|
||||
httpd_resp_set_type(req, "text/css");
|
||||
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
|
||||
}
|
||||
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 < 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 scan_loop_task(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
nfc_tag_info_t last;
|
||||
memset(&last, 0, sizeof(last));
|
||||
while (1) {
|
||||
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;
|
||||
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);
|
||||
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);
|
||||
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 {
|
||||
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 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};
|
||||
httpd_register_uri_handler(s, &u);
|
||||
|
||||
u = (httpd_uri_t){.uri = "/api/status", .method = HTTP_GET, .handler = api_status};
|
||||
httpd_register_uri_handler(s, &u);
|
||||
u = (httpd_uri_t){.uri = "/api/nfc/poll", .method = HTTP_POST, .handler = api_nfc_poll};
|
||||
httpd_register_uri_handler(s, &u);
|
||||
u = (httpd_uri_t){.uri = "/api/nfc/scan", .method = HTTP_POST, .handler = api_scan};
|
||||
httpd_register_uri_handler(s, &u);
|
||||
u = (httpd_uri_t){.uri = "/api/pn532/general-status", .method = HTTP_GET, .handler = api_general_status};
|
||||
httpd_register_uri_handler(s, &u);
|
||||
u = (httpd_uri_t){.uri = "/api/mifare/read-block", .method = HTTP_POST, .handler = api_mifare_read};
|
||||
httpd_register_uri_handler(s, &u);
|
||||
u = (httpd_uri_t){.uri = "/api/mifare/write-block", .method = HTTP_POST, .handler = api_mifare_write};
|
||||
httpd_register_uri_handler(s, &u);
|
||||
u = (httpd_uri_t){.uri = "/api/ul/read-page", .method = HTTP_POST, .handler = api_ul_read};
|
||||
httpd_register_uri_handler(s, &u);
|
||||
u = (httpd_uri_t){.uri = "/api/ul/write-page", .method = HTTP_POST, .handler = api_ul_write};
|
||||
httpd_register_uri_handler(s, &u);
|
||||
u = (httpd_uri_t){.uri = "/api/raw/pn532", .method = HTTP_POST, .handler = api_raw_pn532};
|
||||
httpd_register_uri_handler(s, &u);
|
||||
u = (httpd_uri_t){.uri = "/api/ota", .method = HTTP_POST, .handler = api_ota_stub};
|
||||
httpd_register_uri_handler(s, &u);
|
||||
u = (httpd_uri_t){.uri = "/api/session/export", .method = HTTP_GET, .handler = api_session_export};
|
||||
httpd_register_uri_handler(s, &u);
|
||||
u = (httpd_uri_t){.uri = "/api/session/clear", .method = HTTP_POST, .handler = api_session_clear};
|
||||
httpd_register_uri_handler(s, &u);
|
||||
u = (httpd_uri_t){.uri = "/api/session/deep", .method = HTTP_POST, .handler = api_session_deep};
|
||||
httpd_register_uri_handler(s, &u);
|
||||
u = (httpd_uri_t){.uri = "/api/mifare/dictionary-attack", .method = HTTP_POST,
|
||||
.handler = api_mifare_dictionary_attack};
|
||||
httpd_register_uri_handler(s, &u);
|
||||
u = (httpd_uri_t){.uri = "/api/nfc/emulate-raw", .method = HTTP_POST, .handler = api_nfc_emulate_raw};
|
||||
httpd_register_uri_handler(s, &u);
|
||||
u = (httpd_uri_t){.uri = "/ws", .method = HTTP_GET, .handler = ws_handler, .is_websocket = true};
|
||||
httpd_register_uri_handler(s, &u);
|
||||
u = (httpd_uri_t){.uri = "/*", .method = HTTP_GET, .handler = static_any};
|
||||
httpd_register_uri_handler(s, &u);
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
esp_err_t app_net_init(void)
|
||||
{
|
||||
ESP_ERROR_CHECK(nvs_flash_init());
|
||||
|
||||
esp_netif_init();
|
||||
esp_event_loop_create_default();
|
||||
esp_netif_create_default_wifi_ap();
|
||||
|
||||
wifi_init_config_t wcfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||
ESP_ERROR_CHECK(esp_wifi_init(&wcfg));
|
||||
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;
|
||||
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
|
||||
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &ap));
|
||||
ESP_ERROR_CHECK(esp_wifi_start());
|
||||
|
||||
esp_vfs_spiffs_conf_t sp = {
|
||||
.base_path = "/spiffs",
|
||||
.partition_label = "storage",
|
||||
.max_files = 16,
|
||||
.format_if_mount_failed = true,
|
||||
};
|
||||
ESP_ERROR_CHECK(esp_vfs_spiffs_register(&sp));
|
||||
|
||||
mdns_init();
|
||||
mdns_hostname_set("pn532tool");
|
||||
mdns_instance_name_set("PN532 NFC Toolkit");
|
||||
mdns_service_add(NULL, "_http", "_tcp", 80, NULL, 0);
|
||||
|
||||
s_server = start_server();
|
||||
if (!s_server) {
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
xTaskCreate(scan_loop_task, "nfc_scan", 20480, NULL, 5, &s_scan_task);
|
||||
return ESP_OK;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include "esp_err.h"
|
||||
#include <stdbool.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
esp_err_t app_net_init(void);
|
||||
void app_net_set_continuous_scan(bool on);
|
||||
bool app_net_continuous_scan(void);
|
||||
esp_err_t app_net_broadcast_json(const char *channel, const char *json_text);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
5
firmware/components/nfc_engine/CMakeLists.txt
Normal file
5
firmware/components/nfc_engine/CMakeLists.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
idf_component_register(
|
||||
SRCS "nfc_engine.c" "jobs.c" "session_capture.c" "nfc_deep.c" "nfc_brute.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES pn532_host esp_common freertos json esp_timer esp_system
|
||||
)
|
||||
26
firmware/components/nfc_engine/include/nfc_engine/jobs.h
Normal file
26
firmware/components/nfc_engine/include/nfc_engine/jobs.h
Normal file
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include "esp_err.h"
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef void (*job_progress_cb_t)(int pct, const char *msg, void *ctx);
|
||||
|
||||
typedef struct {
|
||||
uint32_t id;
|
||||
char name[32];
|
||||
volatile int pct;
|
||||
volatile int done;
|
||||
} nfc_job_t;
|
||||
|
||||
uint32_t jobs_create(const char *name);
|
||||
void jobs_set_progress(uint32_t id, int pct, const char *msg);
|
||||
void jobs_finish(uint32_t id);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include "nfc_engine/nfc_engine.h"
|
||||
#include "cJSON.h"
|
||||
#include "esp_err.h"
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Dictionary attack on MIFARE Classic sector trailer (Key A then Key B per candidate).
|
||||
* `extra` = packed 6-byte keys, `extra_n` = number of keys.
|
||||
* If `variations`, expands each base key with bounded bit/nibble tweaks (not full 2^48 space).
|
||||
* Returns JSON object: { "attempts": N, "sectorHits": [ {sector, keyHex, keyType}, ... ] }
|
||||
* Caller cJSON_Delete().
|
||||
*/
|
||||
cJSON *nfc_mifare_dictionary_attack(nfc_tag_info_t *tag, uint8_t sector_first, uint8_t sector_last,
|
||||
const uint8_t *extra, size_t extra_n, bool variations,
|
||||
int *attempts_out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
18
firmware/components/nfc_engine/include/nfc_engine/nfc_deep.h
Normal file
18
firmware/components/nfc_engine/include/nfc_engine/nfc_deep.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include "nfc_engine/nfc_engine.h"
|
||||
#include "cJSON.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Best-effort deep read for PN532 path: inventory + general status + MIFARE sector sweep
|
||||
* (common keys) and/or Ultralight/NTAG page sweep. Caller must cJSON_Delete() result.
|
||||
*/
|
||||
cJSON *nfc_tag_deep_profile(nfc_tag_info_t *tag);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include "cJSON.h"
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define NFC_MAX_UID_LEN 10
|
||||
#define NFC_BLOCK_LEN 16
|
||||
|
||||
typedef struct {
|
||||
uint8_t uid_len;
|
||||
uint8_t uid[NFC_MAX_UID_LEN];
|
||||
uint16_t atqa;
|
||||
uint8_t sak;
|
||||
uint8_t type_hint; /* 0 unknown, 1 classic, 2 ultralight/ntag */
|
||||
} nfc_tag_info_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t key[6];
|
||||
bool key_b;
|
||||
} nfc_mifare_key_t;
|
||||
|
||||
esp_err_t nfc_engine_init(void);
|
||||
|
||||
esp_err_t nfc_poll_passive_target(nfc_tag_info_t *out);
|
||||
|
||||
esp_err_t nfc_mifare_authenticate_block(const nfc_tag_info_t *tag, uint8_t block_no,
|
||||
const nfc_mifare_key_t *key);
|
||||
esp_err_t nfc_mifare_read_block(uint8_t block_no, uint8_t block[NFC_BLOCK_LEN]);
|
||||
esp_err_t nfc_mifare_write_block(uint8_t block_no, const uint8_t block[NFC_BLOCK_LEN]);
|
||||
|
||||
esp_err_t nfc_ultralight_read_page(uint8_t page, uint8_t data[4]);
|
||||
esp_err_t nfc_ultralight_write_page(uint8_t page, const uint8_t data[4]);
|
||||
|
||||
esp_err_t nfc_ul_fast_read(uint8_t start_page, uint8_t *out, size_t out_max, size_t *got);
|
||||
|
||||
/** Build JSON snapshot of last seen tag + optional blocks (caller frees cJSON). */
|
||||
cJSON *nfc_tag_to_json(const nfc_tag_info_t *tag);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** RAM-only capture buffer (NDJSON lines). When full, NFC deep capture pauses until clear. */
|
||||
void session_capture_init(void);
|
||||
void session_capture_clear(void);
|
||||
|
||||
bool session_capture_is_full(void);
|
||||
bool session_capture_deep_enabled(void);
|
||||
void session_capture_set_deep(bool on);
|
||||
|
||||
size_t session_capture_max(void);
|
||||
void session_capture_get_status(size_t *used_bytes, uint32_t *line_count, bool *full);
|
||||
|
||||
/** Append one NDJSON line (no trailing newline in `line`). Returns false if buffer full. */
|
||||
bool session_capture_append_line(const char *line);
|
||||
|
||||
/** Export raw bytes (entire buffer) for HTTP download. */
|
||||
size_t session_capture_export_size(void);
|
||||
const char *session_capture_export_ptr(void);
|
||||
|
||||
/** Copy up to `cap` bytes (typically cap >= session_capture_export_size()). */
|
||||
void session_capture_copy_to(char *dst, size_t cap, size_t *out_len);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
23
firmware/components/nfc_engine/jobs.c
Normal file
23
firmware/components/nfc_engine/jobs.c
Normal file
@@ -0,0 +1,23 @@
|
||||
#include "nfc_engine/jobs.h"
|
||||
#include "esp_log.h"
|
||||
#include <string.h>
|
||||
|
||||
static const char *TAG = "nfc_jobs";
|
||||
|
||||
uint32_t jobs_create(const char *name)
|
||||
{
|
||||
static uint32_t s_next = 1;
|
||||
(void)name;
|
||||
ESP_LOGI(TAG, "job %s id=%lu", name ? name : "?", (unsigned long)s_next);
|
||||
return s_next++;
|
||||
}
|
||||
|
||||
void jobs_set_progress(uint32_t id, int pct, const char *msg)
|
||||
{
|
||||
ESP_LOGD(TAG, "job %lu %d%% %s", (unsigned long)id, pct, msg ? msg : "");
|
||||
}
|
||||
|
||||
void jobs_finish(uint32_t id)
|
||||
{
|
||||
ESP_LOGI(TAG, "job %lu done", (unsigned long)id);
|
||||
}
|
||||
232
firmware/components/nfc_engine/nfc_brute.c
Normal file
232
firmware/components/nfc_engine/nfc_brute.c
Normal file
@@ -0,0 +1,232 @@
|
||||
#include "nfc_engine/nfc_brute.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_task_wdt.h"
|
||||
#include "esp_timer.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include <string.h>
|
||||
|
||||
static const char *TAG = "nfc_brute";
|
||||
|
||||
/* Community default keys (subset from public Proxmark3 / MCT-style lists). */
|
||||
static const uint8_t k_builtin[][6] = {
|
||||
{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, {0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
|
||||
{0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5}, {0xA5, 0xA4, 0xA3, 0xA2, 0xA1, 0xA0},
|
||||
{0x89, 0xEC, 0xA9, 0x7F, 0x8C, 0x2A}, {0x5C, 0x8F, 0xF9, 0x99, 0x0D, 0xA2},
|
||||
{0x75, 0xCC, 0xB5, 0x9C, 0x9B, 0xED}, {0xD0, 0x1A, 0xFE, 0xEB, 0x89, 0x0A},
|
||||
{0x4B, 0x79, 0x1B, 0xEA, 0x7B, 0xCC}, {0x26, 0x12, 0xC6, 0xDE, 0x84, 0xCA},
|
||||
{0x70, 0x7B, 0x11, 0xFC, 0x14, 0x81}, {0x03, 0xF9, 0x06, 0x76, 0x46, 0xAE},
|
||||
{0x23, 0x52, 0xC5, 0xB5, 0x6D, 0x85}, {0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5},
|
||||
{0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5}, {0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5},
|
||||
{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}, {0x4D, 0x3A, 0x99, 0xC3, 0x51, 0xDD},
|
||||
{0x1A, 0x98, 0x2C, 0x7E, 0x45, 0x9A}, {0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA},
|
||||
{0xFB, 0xFB, 0xFB, 0xFB, 0xFB, 0xFB}, {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7},
|
||||
{0x5A, 0x1B, 0x85, 0xFC, 0xE2, 0x0A}, {0x71, 0x4C, 0x5C, 0x88, 0x6E, 0x97},
|
||||
{0x58, 0x7E, 0xE5, 0xF9, 0x35, 0x0F}, {0xA0, 0x47, 0x8C, 0xC3, 0x90, 0x91},
|
||||
{0x53, 0x3C, 0xB6, 0xC7, 0x23, 0xF6}, {0x8F, 0xD0, 0xA4, 0xF2, 0x56, 0xE9},
|
||||
{0xE0, 0x00, 0x00, 0x00, 0x00, 0x00}, {0xE7, 0xD6, 0x06, 0x4C, 0x58, 0x60},
|
||||
{0xB2, 0x7C, 0xCA, 0xB3, 0x0D, 0xBD}, {0xD2, 0xEC, 0xE8, 0xB9, 0x39, 0x5E},
|
||||
{0x14, 0x94, 0xE8, 0x16, 0x63, 0xD7}, {0x7C, 0x9F, 0xB8, 0x47, 0x42, 0x42},
|
||||
{0x56, 0x93, 0x69, 0xC5, 0xA0, 0xE5}, {0x63, 0x21, 0x93, 0xBE, 0x1C, 0x3C},
|
||||
{0x8E, 0x26, 0x5B, 0xE2, 0x45, 0xBF}, {0xF4, 0x6B, 0x6D, 0xC0, 0xD6, 0xC4},
|
||||
{0x2A, 0xA0, 0x5E, 0xD1, 0x85, 0x6F}, {0xAE, 0x3F, 0xF4, 0xEE, 0xA0, 0xDB},
|
||||
};
|
||||
|
||||
#define NBUILTIN (sizeof(k_builtin) / sizeof(k_builtin[0]))
|
||||
#define MAX_VARIANTS_PER_KEY 14
|
||||
#define MAX_TRIES_BEFORE_WDT 48
|
||||
|
||||
static int push_variant(const uint8_t base[6], int idx, uint8_t out[6])
|
||||
{
|
||||
memcpy(out, base, 6);
|
||||
switch (idx) {
|
||||
case 0:
|
||||
return 0;
|
||||
case 1:
|
||||
out[5] ^= 0xFF;
|
||||
return 0;
|
||||
case 2:
|
||||
out[0] ^= 0xFF;
|
||||
return 0;
|
||||
case 3:
|
||||
out[5] ^= 0xAA;
|
||||
return 0;
|
||||
case 4:
|
||||
out[5] ^= 0x55;
|
||||
return 0;
|
||||
default: {
|
||||
int n = idx - 5;
|
||||
if (n >= 0 && n < 10) {
|
||||
out[5] = (uint8_t)((out[5] & 0xF0) | (uint8_t)n);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
static bool try_key_on_trailer(nfc_tag_info_t *tag, uint8_t trailer, const uint8_t key[6], bool key_b)
|
||||
{
|
||||
nfc_mifare_key_t k;
|
||||
memcpy(k.key, key, 6);
|
||||
k.key_b = key_b;
|
||||
return nfc_mifare_authenticate_block(tag, trailer, &k) == ESP_OK;
|
||||
}
|
||||
|
||||
/** Trailer block for MIFARE Classic sector index (0..15 for 1K, 0..39 for 4K). */
|
||||
static uint8_t classic_trailer_for_sector(const nfc_tag_info_t *tag, uint8_t sec)
|
||||
{
|
||||
if (tag->sak == 0x19) {
|
||||
if (sec <= 31) {
|
||||
return (uint8_t)(sec * 4 + 3);
|
||||
}
|
||||
if (sec <= 39) {
|
||||
return (uint8_t)(128 + (sec - 32) * 16 + 15);
|
||||
}
|
||||
return 0xFF;
|
||||
}
|
||||
return (uint8_t)(sec * 4 + 3);
|
||||
}
|
||||
|
||||
cJSON *nfc_mifare_dictionary_attack(nfc_tag_info_t *tag, uint8_t sector_first, uint8_t sector_last,
|
||||
const uint8_t *extra, size_t extra_n, bool variations,
|
||||
int *attempts_out)
|
||||
{
|
||||
int64_t t0_us = esp_timer_get_time();
|
||||
int attempts = 0;
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
cJSON *hits = cJSON_CreateArray();
|
||||
if (!root || !hits) {
|
||||
cJSON_Delete(root);
|
||||
cJSON_Delete(hits);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (tag->type_hint != 1) {
|
||||
cJSON_AddStringToObject(root, "error", "not_classic_sak_hint");
|
||||
cJSON_AddItemToObject(root, "sectorHits", hits);
|
||||
if (attempts_out) {
|
||||
*attempts_out = 0;
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
if (sector_first > sector_last) {
|
||||
uint8_t t = sector_first;
|
||||
sector_first = sector_last;
|
||||
sector_last = t;
|
||||
}
|
||||
|
||||
for (uint8_t sec = sector_first; sec <= sector_last; sec++) {
|
||||
uint8_t trailer = classic_trailer_for_sector(tag, sec);
|
||||
if (trailer == 0xFF) {
|
||||
continue;
|
||||
}
|
||||
bool got = false;
|
||||
|
||||
for (size_t bi = 0; bi < NBUILTIN && !got; bi++) {
|
||||
uint8_t trial[6];
|
||||
int maxv = variations ? MAX_VARIANTS_PER_KEY : 1;
|
||||
for (int vi = 0; vi < maxv; vi++) {
|
||||
if (push_variant(k_builtin[bi], vi, trial) != 0) {
|
||||
break;
|
||||
}
|
||||
attempts++;
|
||||
if (try_key_on_trailer(tag, trailer, trial, false)) {
|
||||
char hx[16];
|
||||
for (int i = 0; i < 6; i++) {
|
||||
snprintf(hx + i * 2, 3, "%02X", trial[i]);
|
||||
}
|
||||
hx[12] = 0;
|
||||
cJSON *h = cJSON_CreateObject();
|
||||
cJSON_AddNumberToObject(h, "sector", sec);
|
||||
cJSON_AddStringToObject(h, "keyHex", hx);
|
||||
cJSON_AddStringToObject(h, "keyType", "A");
|
||||
cJSON_AddItemToArray(hits, h);
|
||||
got = true;
|
||||
break;
|
||||
}
|
||||
if (try_key_on_trailer(tag, trailer, trial, true)) {
|
||||
char hx[16];
|
||||
for (int i = 0; i < 6; i++) {
|
||||
snprintf(hx + i * 2, 3, "%02X", trial[i]);
|
||||
}
|
||||
hx[12] = 0;
|
||||
cJSON *h = cJSON_CreateObject();
|
||||
cJSON_AddNumberToObject(h, "sector", sec);
|
||||
cJSON_AddStringToObject(h, "keyHex", hx);
|
||||
cJSON_AddStringToObject(h, "keyType", "B");
|
||||
cJSON_AddItemToArray(hits, h);
|
||||
got = true;
|
||||
break;
|
||||
}
|
||||
if ((attempts % MAX_TRIES_BEFORE_WDT) == 0) {
|
||||
esp_task_wdt_reset();
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t ei = 0; ei < extra_n && !got; ei++) {
|
||||
const uint8_t *ek = extra + ei * 6;
|
||||
uint8_t trial[6];
|
||||
int maxv = variations ? MAX_VARIANTS_PER_KEY : 1;
|
||||
for (int vi = 0; vi < maxv; vi++) {
|
||||
if (push_variant(ek, vi, trial) != 0) {
|
||||
break;
|
||||
}
|
||||
attempts++;
|
||||
if (try_key_on_trailer(tag, trailer, trial, false)) {
|
||||
char hx[16];
|
||||
for (int i = 0; i < 6; i++) {
|
||||
snprintf(hx + i * 2, 3, "%02X", trial[i]);
|
||||
}
|
||||
hx[12] = 0;
|
||||
cJSON *h = cJSON_CreateObject();
|
||||
cJSON_AddNumberToObject(h, "sector", sec);
|
||||
cJSON_AddStringToObject(h, "keyHex", hx);
|
||||
cJSON_AddStringToObject(h, "keyType", "A");
|
||||
cJSON_AddItemToArray(hits, h);
|
||||
got = true;
|
||||
break;
|
||||
}
|
||||
if (try_key_on_trailer(tag, trailer, trial, true)) {
|
||||
char hx[16];
|
||||
for (int i = 0; i < 6; i++) {
|
||||
snprintf(hx + i * 2, 3, "%02X", trial[i]);
|
||||
}
|
||||
hx[12] = 0;
|
||||
cJSON *h = cJSON_CreateObject();
|
||||
cJSON_AddNumberToObject(h, "sector", sec);
|
||||
cJSON_AddStringToObject(h, "keyHex", hx);
|
||||
cJSON_AddStringToObject(h, "keyType", "B");
|
||||
cJSON_AddItemToArray(hits, h);
|
||||
got = true;
|
||||
break;
|
||||
}
|
||||
if ((attempts % MAX_TRIES_BEFORE_WDT) == 0) {
|
||||
esp_task_wdt_reset();
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!got) {
|
||||
cJSON *h = cJSON_CreateObject();
|
||||
cJSON_AddNumberToObject(h, "sector", sec);
|
||||
cJSON_AddBoolToObject(h, "miss", true);
|
||||
cJSON_AddItemToArray(hits, h);
|
||||
}
|
||||
}
|
||||
|
||||
cJSON_AddItemToObject(root, "sectorHits", hits);
|
||||
cJSON_AddNumberToObject(root, "attempts", attempts);
|
||||
cJSON_AddNumberToObject(root, "durationMs", (double)((esp_timer_get_time() - t0_us) / 1000));
|
||||
cJSON_AddStringToObject(root, "note",
|
||||
"Dictionary + bounded variants only — not exhaustive 48-bit keyspace.");
|
||||
if (attempts_out) {
|
||||
*attempts_out = attempts;
|
||||
}
|
||||
ESP_LOGI(TAG, "dictionary attack attempts=%d", attempts);
|
||||
return root;
|
||||
}
|
||||
188
firmware/components/nfc_engine/nfc_deep.c
Normal file
188
firmware/components/nfc_engine/nfc_deep.c
Normal file
@@ -0,0 +1,188 @@
|
||||
#include "nfc_engine/nfc_deep.h"
|
||||
#include "nfc_engine/nfc_engine.h"
|
||||
#include "pn532_host/pn532_core.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_timer.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
static const char *TAG = "nfc_deep";
|
||||
|
||||
#define MAX_UL_PAGES 240
|
||||
|
||||
static const uint8_t k_default_keys[][6] = {
|
||||
{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF},
|
||||
{0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5},
|
||||
{0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7},
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
|
||||
};
|
||||
|
||||
static void block_to_hex(const uint8_t blk[NFC_BLOCK_LEN], char *out33)
|
||||
{
|
||||
for (int i = 0; i < NFC_BLOCK_LEN; i++) {
|
||||
snprintf(out33 + i * 2, 3, "%02X", blk[i]);
|
||||
}
|
||||
out33[32] = 0;
|
||||
}
|
||||
|
||||
static void key_to_hex(const uint8_t k[6], char *out13)
|
||||
{
|
||||
for (int i = 0; i < 6; i++) {
|
||||
snprintf(out13 + i * 2, 3, "%02X", k[i]);
|
||||
}
|
||||
out13[12] = 0;
|
||||
}
|
||||
|
||||
/** Classic 1K: 16 sectors x 4 blocks. Classic 4K: sectors 0–31 x 4 blocks, sectors 32–39 x 16 blocks. */
|
||||
static bool mfc_sector_layout(const nfc_tag_info_t *tag, int sector, int *first_block, int *num_blocks,
|
||||
uint8_t *trailer_block)
|
||||
{
|
||||
bool is4k = (tag->sak == 0x19);
|
||||
if (!is4k) {
|
||||
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;
|
||||
}
|
||||
|
||||
static bool try_sector(nfc_tag_info_t *tag, int sector, cJSON *sec_out)
|
||||
{
|
||||
int fb = 0;
|
||||
int nb = 0;
|
||||
uint8_t trailer = 0;
|
||||
if (!mfc_sector_layout(tag, sector, &fb, &nb, &trailer)) {
|
||||
return false;
|
||||
}
|
||||
cJSON_AddNumberToObject(sec_out, "sector", sector);
|
||||
cJSON_AddNumberToObject(sec_out, "firstBlock", fb);
|
||||
cJSON_AddNumberToObject(sec_out, "trailerBlock", trailer);
|
||||
cJSON_AddNumberToObject(sec_out, "blockCount", nb);
|
||||
|
||||
for (size_t ki = 0; ki < sizeof(k_default_keys) / 6; ki++) {
|
||||
nfc_mifare_key_t key;
|
||||
memcpy(key.key, k_default_keys[ki], 6);
|
||||
key.key_b = false;
|
||||
if (nfc_mifare_authenticate_block(tag, trailer, &key) == ESP_OK) {
|
||||
cJSON_AddStringToObject(sec_out, "keyType", "A");
|
||||
char kh[16];
|
||||
key_to_hex(key.key, kh);
|
||||
cJSON_AddStringToObject(sec_out, "keyHex", kh);
|
||||
cJSON *blocks = cJSON_CreateArray();
|
||||
for (int b = 0; b < nb; b++) {
|
||||
uint8_t bn = (uint8_t)(fb + b);
|
||||
uint8_t blk[NFC_BLOCK_LEN];
|
||||
if (nfc_mifare_read_block(bn, blk) == ESP_OK) {
|
||||
char hx[36];
|
||||
block_to_hex(blk, hx);
|
||||
cJSON_AddItemToArray(blocks, cJSON_CreateString(hx));
|
||||
} else {
|
||||
cJSON_AddItemToArray(blocks, cJSON_CreateNull());
|
||||
}
|
||||
}
|
||||
cJSON_AddItemToObject(sec_out, "blocksHex", blocks);
|
||||
return true;
|
||||
}
|
||||
key.key_b = true;
|
||||
if (nfc_mifare_authenticate_block(tag, trailer, &key) == ESP_OK) {
|
||||
cJSON_AddStringToObject(sec_out, "keyType", "B");
|
||||
char kh[16];
|
||||
key_to_hex(key.key, kh);
|
||||
cJSON_AddStringToObject(sec_out, "keyHex", kh);
|
||||
cJSON *blocks = cJSON_CreateArray();
|
||||
for (int b = 0; b < nb; b++) {
|
||||
uint8_t bn = (uint8_t)(fb + b);
|
||||
uint8_t blk[NFC_BLOCK_LEN];
|
||||
if (nfc_mifare_read_block(bn, blk) == ESP_OK) {
|
||||
char hx[36];
|
||||
block_to_hex(blk, hx);
|
||||
cJSON_AddItemToArray(blocks, cJSON_CreateString(hx));
|
||||
} else {
|
||||
cJSON_AddItemToArray(blocks, cJSON_CreateNull());
|
||||
}
|
||||
}
|
||||
cJSON_AddItemToObject(sec_out, "blocksHex", blocks);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
cJSON_AddBoolToObject(sec_out, "authFailed", true);
|
||||
return false;
|
||||
}
|
||||
|
||||
static void add_mifare_classic(nfc_tag_info_t *tag, cJSON *root)
|
||||
{
|
||||
int sectors = (tag->sak == 0x19) ? 40 : 16;
|
||||
cJSON *arr = cJSON_CreateArray();
|
||||
for (int s = 0; s < sectors; s++) {
|
||||
cJSON *sec = cJSON_CreateObject();
|
||||
(void)try_sector(tag, s, sec);
|
||||
cJSON_AddItemToArray(arr, sec);
|
||||
}
|
||||
cJSON_AddItemToObject(root, "mifareClassic", arr);
|
||||
}
|
||||
|
||||
static void add_ultralight(nfc_tag_info_t *tag, cJSON *root)
|
||||
{
|
||||
(void)tag;
|
||||
cJSON *pages = cJSON_CreateArray();
|
||||
uint8_t buf[4];
|
||||
for (int p = 0; p < MAX_UL_PAGES; p++) {
|
||||
if (nfc_ultralight_read_page((uint8_t)p, buf) != ESP_OK) {
|
||||
break;
|
||||
}
|
||||
char line[12];
|
||||
snprintf(line, sizeof line, "%02X%02X%02X%02X", buf[0], buf[1], buf[2], buf[3]);
|
||||
cJSON_AddItemToArray(pages, cJSON_CreateString(line));
|
||||
}
|
||||
cJSON_AddItemToObject(root, "ultralightPagesHex", pages);
|
||||
}
|
||||
|
||||
cJSON *nfc_tag_deep_profile(nfc_tag_info_t *tag)
|
||||
{
|
||||
cJSON *o = cJSON_CreateObject();
|
||||
if (!o) {
|
||||
return NULL;
|
||||
}
|
||||
cJSON_AddNumberToObject(o, "capturedMs", (double)(esp_timer_get_time() / 1000));
|
||||
cJSON_AddItemToObject(o, "tag", nfc_tag_to_json(tag));
|
||||
|
||||
uint8_t gs[32];
|
||||
size_t gl = 0;
|
||||
if (pn532_get_general_status(gs, sizeof(gs), &gl) == ESP_OK && gl > 0) {
|
||||
cJSON *g = cJSON_CreateArray();
|
||||
for (size_t i = 0; i < gl; i++) {
|
||||
cJSON_AddItemToArray(g, cJSON_CreateNumber(gs[i]));
|
||||
}
|
||||
cJSON_AddItemToObject(o, "pn532GeneralStatus", g);
|
||||
}
|
||||
|
||||
if (tag->type_hint == 1) {
|
||||
add_mifare_classic(tag, o);
|
||||
} else if (tag->type_hint == 2) {
|
||||
add_ultralight(tag, o);
|
||||
} else {
|
||||
cJSON_AddStringToObject(
|
||||
o, "note",
|
||||
"Unknown type from SAK/ATQA — stored inventory + PN532 status only. Use Raw console for ISO14443-4 / other stacks.");
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "deep profile done type_hint=%u", tag->type_hint);
|
||||
return o;
|
||||
}
|
||||
255
firmware/components/nfc_engine/nfc_engine.c
Normal file
255
firmware/components/nfc_engine/nfc_engine.c
Normal file
@@ -0,0 +1,255 @@
|
||||
#include "nfc_engine/nfc_engine.h"
|
||||
#include "pn532_host/pn532_core.h"
|
||||
#include "esp_log.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
static const char *TAG = "nfc_engine";
|
||||
|
||||
static uint8_t s_tg = 1;
|
||||
|
||||
static void hint_type(nfc_tag_info_t *t)
|
||||
{
|
||||
switch (t->sak) {
|
||||
case 0x08:
|
||||
case 0x00:
|
||||
t->type_hint = 2;
|
||||
break;
|
||||
case 0x09:
|
||||
case 0x18:
|
||||
case 0x19:
|
||||
t->type_hint = 1;
|
||||
break;
|
||||
default:
|
||||
t->type_hint = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t nfc_engine_init(void)
|
||||
{
|
||||
esp_err_t e = pn532_core_init();
|
||||
if (e != ESP_OK) {
|
||||
return e;
|
||||
}
|
||||
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");
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t nfc_poll_passive_target(nfc_tag_info_t *out)
|
||||
{
|
||||
memset(out, 0, sizeof(*out));
|
||||
uint8_t resp[64];
|
||||
size_t rlen = 0;
|
||||
esp_err_t e = pn532_in_list_passive_target(1, 0x00, resp, sizeof(resp), &rlen);
|
||||
if (e != ESP_OK) {
|
||||
return e;
|
||||
}
|
||||
if (rlen < 2 || resp[0] != 0x00) {
|
||||
return ESP_ERR_INVALID_RESPONSE;
|
||||
}
|
||||
if (resp[1] < 1) {
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
if (rlen < 8) {
|
||||
return ESP_ERR_INVALID_RESPONSE;
|
||||
}
|
||||
s_tg = resp[2];
|
||||
out->atqa = (uint16_t)(((uint16_t)resp[3] << 8) | resp[4]);
|
||||
out->sak = resp[5];
|
||||
out->uid_len = resp[6];
|
||||
if (out->uid_len > NFC_MAX_UID_LEN || (size_t)(7 + out->uid_len) > rlen) {
|
||||
return ESP_ERR_INVALID_RESPONSE;
|
||||
}
|
||||
memcpy(out->uid, resp + 7, out->uid_len);
|
||||
hint_type(out);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t in_data_tg(const uint8_t *data, size_t data_len, uint8_t *response, size_t response_max,
|
||||
size_t *response_len)
|
||||
{
|
||||
uint8_t buf[64];
|
||||
if (data_len > sizeof(buf) - 3) {
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
buf[0] = PN532_CMD_INDATAEXCHANGE;
|
||||
buf[1] = s_tg;
|
||||
memcpy(buf + 2, data, data_len);
|
||||
return pn532_send_cmd(buf, 2 + data_len, response, response_max, response_len, 500);
|
||||
}
|
||||
|
||||
esp_err_t nfc_mifare_authenticate_block(const nfc_tag_info_t *tag, uint8_t block_no,
|
||||
const nfc_mifare_key_t *key)
|
||||
{
|
||||
uint8_t data[12];
|
||||
data[0] = key->key_b ? PN532_MIFARE_CMD_AUTH_B : PN532_MIFARE_CMD_AUTH_A;
|
||||
data[1] = block_no;
|
||||
memcpy(data + 2, key->key, 6);
|
||||
uint8_t uid_use[4];
|
||||
if (tag->uid_len == 4) {
|
||||
memcpy(uid_use, tag->uid, 4);
|
||||
} else if (tag->uid_len >= 7) {
|
||||
memcpy(uid_use, tag->uid + tag->uid_len - 4, 4);
|
||||
} else if (tag->uid_len > 4) {
|
||||
memcpy(uid_use, tag->uid + tag->uid_len - 4, 4);
|
||||
} else {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
memcpy(data + 8, uid_use, 4);
|
||||
uint8_t resp[32];
|
||||
size_t rlen = 0;
|
||||
esp_err_t e = in_data_tg(data, sizeof(data), resp, sizeof(resp), &rlen);
|
||||
if (e != ESP_OK) {
|
||||
return e;
|
||||
}
|
||||
if (rlen < 1 || resp[0] != 0x00) {
|
||||
return ESP_FAIL;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t nfc_mifare_read_block(uint8_t block_no, uint8_t block[NFC_BLOCK_LEN])
|
||||
{
|
||||
uint8_t d[] = {PN532_MIFARE_CMD_READ, block_no};
|
||||
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 < 1 + NFC_BLOCK_LEN || resp[0] != 0x00) {
|
||||
return ESP_ERR_INVALID_RESPONSE;
|
||||
}
|
||||
memcpy(block, resp + 1, NFC_BLOCK_LEN);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t nfc_mifare_write_block(uint8_t block_no, const uint8_t block[NFC_BLOCK_LEN])
|
||||
{
|
||||
uint8_t d[2 + NFC_BLOCK_LEN];
|
||||
d[0] = PN532_MIFARE_CMD_WRITE;
|
||||
d[1] = block_no;
|
||||
memcpy(d + 2, block, NFC_BLOCK_LEN);
|
||||
uint8_t resp[16];
|
||||
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 < 1 || resp[0] != 0x00) {
|
||||
return ESP_FAIL;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t nfc_ultralight_read_page(uint8_t page, uint8_t data[4])
|
||||
{
|
||||
uint8_t d[] = {PN532_MIFARE_CMD_READ, page};
|
||||
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 < 1 + 16 || resp[0] != 0x00) {
|
||||
return ESP_ERR_INVALID_RESPONSE;
|
||||
}
|
||||
memcpy(data, resp + 1, 4);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t nfc_ultralight_write_page(uint8_t page, const uint8_t data[4])
|
||||
{
|
||||
uint8_t d[6] = {0xA2, page, data[0], data[1], data[2], data[3]};
|
||||
uint8_t resp[16];
|
||||
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 < 1 || resp[0] != 0x00) {
|
||||
return ESP_FAIL;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t nfc_ul_fast_read(uint8_t start_page, uint8_t *out, size_t out_max, size_t *got)
|
||||
{
|
||||
*got = 0;
|
||||
size_t off = 0;
|
||||
uint8_t d[2] = {0x3A, start_page};
|
||||
uint8_t resp[256];
|
||||
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 || resp[0] != 0x00) {
|
||||
return ESP_ERR_INVALID_RESPONSE;
|
||||
}
|
||||
size_t payload = rlen - 1;
|
||||
if (payload > out_max) {
|
||||
payload = out_max;
|
||||
}
|
||||
memcpy(out, resp + 1, payload);
|
||||
off = payload;
|
||||
*got = off;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
cJSON *nfc_tag_to_json(const nfc_tag_info_t *tag)
|
||||
{
|
||||
cJSON *o = cJSON_CreateObject();
|
||||
if (!o) {
|
||||
return NULL;
|
||||
}
|
||||
char uidhex[NFC_MAX_UID_LEN * 2 + 4];
|
||||
for (int i = 0; i < tag->uid_len; i++) {
|
||||
snprintf(uidhex + i * 2, 3, "%02X", tag->uid[i]);
|
||||
}
|
||||
uidhex[tag->uid_len * 2] = 0;
|
||||
cJSON_AddStringToObject(o, "uid", uidhex);
|
||||
cJSON_AddNumberToObject(o, "uidLen", tag->uid_len);
|
||||
cJSON_AddNumberToObject(o, "atqa", tag->atqa);
|
||||
cJSON_AddNumberToObject(o, "sak", tag->sak);
|
||||
cJSON_AddNumberToObject(o, "typeHint", tag->type_hint);
|
||||
|
||||
char aq[8];
|
||||
snprintf(aq, sizeof aq, "%04X", (unsigned)tag->atqa);
|
||||
cJSON_AddStringToObject(o, "atqaHex", aq);
|
||||
char sk[8];
|
||||
snprintf(sk, sizeof sk, "%02X", tag->sak);
|
||||
cJSON_AddStringToObject(o, "sakHex", sk);
|
||||
|
||||
const char *guess = "Unknown / use Raw or RATS";
|
||||
if (tag->type_hint == 1) {
|
||||
guess = (tag->sak == 0x19) ? "MIFARE Classic 4K" : "MIFARE Classic 1K or compatible";
|
||||
} else if (tag->type_hint == 2) {
|
||||
guess = "Ultralight / NTAG / Type 2 family";
|
||||
}
|
||||
cJSON_AddStringToObject(o, "typeGuess", guess);
|
||||
|
||||
uint8_t gs[32];
|
||||
size_t gl = 0;
|
||||
if (pn532_get_general_status(gs, sizeof(gs), &gl) == ESP_OK && gl > 0) {
|
||||
cJSON *arr = cJSON_CreateArray();
|
||||
if (arr) {
|
||||
for (size_t i = 0; i < gl; i++) {
|
||||
cJSON_AddItemToArray(arr, cJSON_CreateNumber(gs[i]));
|
||||
}
|
||||
cJSON_AddItemToObject(o, "pn532GeneralStatus", arr);
|
||||
}
|
||||
}
|
||||
return o;
|
||||
}
|
||||
129
firmware/components/nfc_engine/session_capture.c
Normal file
129
firmware/components/nfc_engine/session_capture.c
Normal file
@@ -0,0 +1,129 @@
|
||||
#include "nfc_engine/session_capture.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include <string.h>
|
||||
|
||||
#define SESSION_CAPTURE_BYTES (48 * 1024)
|
||||
|
||||
static char s_buf[SESSION_CAPTURE_BYTES];
|
||||
static size_t s_len;
|
||||
static uint32_t s_lines;
|
||||
static bool s_full;
|
||||
static bool s_deep;
|
||||
static SemaphoreHandle_t s_mu;
|
||||
|
||||
void session_capture_init(void)
|
||||
{
|
||||
s_mu = xSemaphoreCreateMutex();
|
||||
session_capture_clear();
|
||||
s_deep = false;
|
||||
}
|
||||
|
||||
void session_capture_clear(void)
|
||||
{
|
||||
if (s_mu) {
|
||||
xSemaphoreTake(s_mu, portMAX_DELAY);
|
||||
}
|
||||
s_len = 0;
|
||||
s_lines = 0;
|
||||
s_full = false;
|
||||
s_buf[0] = 0;
|
||||
if (s_mu) {
|
||||
xSemaphoreGive(s_mu);
|
||||
}
|
||||
}
|
||||
|
||||
bool session_capture_is_full(void)
|
||||
{
|
||||
return s_full;
|
||||
}
|
||||
|
||||
bool session_capture_deep_enabled(void) { return s_deep; }
|
||||
|
||||
void session_capture_set_deep(bool on) { s_deep = on; }
|
||||
|
||||
size_t session_capture_max(void) { return sizeof(s_buf) - 2; }
|
||||
|
||||
void session_capture_get_status(size_t *used_bytes, uint32_t *line_count, bool *full)
|
||||
{
|
||||
if (s_mu) {
|
||||
xSemaphoreTake(s_mu, portMAX_DELAY);
|
||||
}
|
||||
if (used_bytes) {
|
||||
*used_bytes = s_len;
|
||||
}
|
||||
if (line_count) {
|
||||
*line_count = s_lines;
|
||||
}
|
||||
if (full) {
|
||||
*full = s_full;
|
||||
}
|
||||
if (s_mu) {
|
||||
xSemaphoreGive(s_mu);
|
||||
}
|
||||
}
|
||||
|
||||
bool session_capture_append_line(const char *line)
|
||||
{
|
||||
if (!line || s_full) {
|
||||
return false;
|
||||
}
|
||||
size_t l = strlen(line);
|
||||
size_t need = l + 1; /* newline */
|
||||
if (s_mu) {
|
||||
xSemaphoreTake(s_mu, portMAX_DELAY);
|
||||
}
|
||||
if (s_full || s_len + need >= sizeof(s_buf)) {
|
||||
s_full = true;
|
||||
if (s_mu) {
|
||||
xSemaphoreGive(s_mu);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
memcpy(s_buf + s_len, line, l);
|
||||
s_len += l;
|
||||
s_buf[s_len++] = '\n';
|
||||
s_buf[s_len] = 0;
|
||||
s_lines++;
|
||||
if (s_len + 2 >= sizeof(s_buf)) {
|
||||
s_full = true;
|
||||
}
|
||||
if (s_mu) {
|
||||
xSemaphoreGive(s_mu);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t session_capture_export_size(void)
|
||||
{
|
||||
if (s_mu) {
|
||||
xSemaphoreTake(s_mu, portMAX_DELAY);
|
||||
}
|
||||
size_t n = s_len;
|
||||
if (s_mu) {
|
||||
xSemaphoreGive(s_mu);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
const char *session_capture_export_ptr(void) { return s_buf; }
|
||||
|
||||
void session_capture_copy_to(char *dst, size_t cap, size_t *out_len)
|
||||
{
|
||||
if (s_mu) {
|
||||
xSemaphoreTake(s_mu, portMAX_DELAY);
|
||||
}
|
||||
size_t n = s_len;
|
||||
if (n > cap) {
|
||||
n = cap;
|
||||
}
|
||||
if (dst && n) {
|
||||
memcpy(dst, s_buf, n);
|
||||
}
|
||||
if (out_len) {
|
||||
*out_len = n;
|
||||
}
|
||||
if (s_mu) {
|
||||
xSemaphoreGive(s_mu);
|
||||
}
|
||||
}
|
||||
7
firmware/components/pn532_host/CMakeLists.txt
Normal file
7
firmware/components/pn532_host/CMakeLists.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
idf_component_register(
|
||||
SRCS
|
||||
"pn532_transport.c"
|
||||
"pn532_core.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES driver esp_timer freertos
|
||||
)
|
||||
84
firmware/components/pn532_host/Kconfig
Normal file
84
firmware/components/pn532_host/Kconfig
Normal file
@@ -0,0 +1,84 @@
|
||||
menu "PN532 Host"
|
||||
|
||||
choice PN532_TRANSPORT
|
||||
prompt "PN532 bus"
|
||||
default PN532_TRANSPORT_SPI
|
||||
config PN532_TRANSPORT_SPI
|
||||
bool "SPI"
|
||||
config PN532_TRANSPORT_I2C
|
||||
bool "I2C"
|
||||
config PN532_TRANSPORT_HSU
|
||||
bool "UART (HSU)"
|
||||
endchoice
|
||||
|
||||
config PN532_SPI_HOST
|
||||
int "SPI host (2=SPI2, 3=SPI3)"
|
||||
default 2
|
||||
depends on PN532_TRANSPORT_SPI
|
||||
|
||||
config PN532_SPI_MOSI_GPIO
|
||||
int "SPI MOSI GPIO"
|
||||
default 11
|
||||
depends on PN532_TRANSPORT_SPI
|
||||
|
||||
config PN532_SPI_MISO_GPIO
|
||||
int "SPI MISO GPIO"
|
||||
default 13
|
||||
depends on PN532_TRANSPORT_SPI
|
||||
|
||||
config PN532_SPI_SCLK_GPIO
|
||||
int "SPI SCLK GPIO"
|
||||
default 12
|
||||
depends on PN532_TRANSPORT_SPI
|
||||
|
||||
config PN532_SPI_CS_GPIO
|
||||
int "SPI CS GPIO"
|
||||
default 10
|
||||
depends on PN532_TRANSPORT_SPI
|
||||
|
||||
config PN532_SPI_CLOCK_HZ
|
||||
int "SPI clock Hz"
|
||||
default 100000
|
||||
depends on PN532_TRANSPORT_SPI
|
||||
|
||||
config PN532_I2C_PORT
|
||||
int "I2C port"
|
||||
default 0
|
||||
depends on PN532_TRANSPORT_I2C
|
||||
|
||||
config PN532_I2C_SDA_GPIO
|
||||
int "I2C SDA GPIO"
|
||||
default 8
|
||||
depends on PN532_TRANSPORT_I2C
|
||||
|
||||
config PN532_I2C_SCL_GPIO
|
||||
int "I2C SCL GPIO"
|
||||
default 9
|
||||
depends on PN532_TRANSPORT_I2C
|
||||
|
||||
config PN532_I2C_ADDR
|
||||
hex "PN532 I2C 7-bit address"
|
||||
default 0x24
|
||||
depends on PN532_TRANSPORT_I2C
|
||||
|
||||
config PN532_HSU_UART_NUM
|
||||
int "UART num for HSU"
|
||||
default 1
|
||||
depends on PN532_TRANSPORT_HSU
|
||||
|
||||
config PN532_HSU_TX_GPIO
|
||||
int "UART TX GPIO"
|
||||
default 17
|
||||
depends on PN532_TRANSPORT_HSU
|
||||
|
||||
config PN532_HSU_RX_GPIO
|
||||
int "UART RX GPIO"
|
||||
default 18
|
||||
depends on PN532_TRANSPORT_HSU
|
||||
|
||||
config PN532_HSU_BAUD
|
||||
int "UART baud"
|
||||
default 115200
|
||||
depends on PN532_TRANSPORT_HSU
|
||||
|
||||
endmenu
|
||||
@@ -0,0 +1,68 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define PN532_HOST_TO_PN532 0xD4
|
||||
#define PN532_PN532_TO_HOST 0xD5
|
||||
|
||||
#define PN532_CMD_GETFIRMWAREVERSION 0x02
|
||||
#define PN532_CMD_GETGENERALSTATUS 0x04
|
||||
#define PN532_CMD_SAMCONFIGURATION 0x14
|
||||
#define PN532_CMD_INLISTPASSIVETARGET 0x4A
|
||||
#define PN532_CMD_INDATAEXCHANGE 0x40
|
||||
#define PN532_CMD_INCOMMUNICATETHRU 0x42
|
||||
#define PN532_CMD_RFCONFIGURATION 0x32
|
||||
#define PN532_CMD_TGINITASTARGET 0x8C
|
||||
#define PN532_CMD_TGGETDATA 0x86
|
||||
#define PN532_CMD_TGSETDATA 0x8E
|
||||
#define PN532_CMD_POWERDOWN 0x16
|
||||
|
||||
#define PN532_MIFARE_CMD_AUTH_A 0x60
|
||||
#define PN532_MIFARE_CMD_AUTH_B 0x61
|
||||
#define PN532_MIFARE_CMD_READ 0x30
|
||||
#define PN532_MIFARE_CMD_WRITE 0xA0
|
||||
#define PN532_MIFARE_CMD_TRANSFER 0xB0
|
||||
|
||||
#define PN532_EEPROM_MAX_CMD_PAYLOAD 254
|
||||
|
||||
esp_err_t pn532_core_init(void);
|
||||
esp_err_t pn532_sam_config_normal(void);
|
||||
esp_err_t pn532_get_firmware_version(uint8_t *ic_ver, uint8_t *fw_ver_hi, uint8_t *fw_ver_lo);
|
||||
esp_err_t pn532_get_general_status(uint8_t *buf, size_t buf_len, size_t *out_len);
|
||||
|
||||
/**
|
||||
* Send full command body after TFI: [cmd] [params...]
|
||||
* response_data is pn532 payload after response TFI (first byte often status 0x00 = OK).
|
||||
*/
|
||||
esp_err_t pn532_send_cmd(const uint8_t *cmd_and_data, size_t len,
|
||||
uint8_t *response, size_t response_max,
|
||||
size_t *response_len, int timeout_ms);
|
||||
|
||||
esp_err_t pn532_in_list_passive_target(uint8_t max_targets, uint8_t baud,
|
||||
uint8_t *response, size_t response_max,
|
||||
size_t *response_len);
|
||||
|
||||
esp_err_t pn532_in_data_exchange(const uint8_t *data, size_t data_len,
|
||||
uint8_t *response, size_t response_max,
|
||||
size_t *response_len);
|
||||
|
||||
esp_err_t pn532_in_communicate_thru(const uint8_t *data, size_t data_len,
|
||||
uint8_t *response, size_t response_max,
|
||||
size_t *response_len);
|
||||
|
||||
/** RF field on/off via RFConfiguration (0x32) item 0x01, RF field */
|
||||
esp_err_t pn532_rf_field(bool on);
|
||||
|
||||
/** Max passive activation / RF retries (0x32 item 0x05) — improves weak-coupling reads */
|
||||
esp_err_t pn532_rf_max_retries(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
PN532_SPI,
|
||||
PN532_I2C,
|
||||
PN532_HSU,
|
||||
} pn532_bus_t;
|
||||
|
||||
esp_err_t pn532_transport_init(void);
|
||||
void pn532_transport_lock(void);
|
||||
void pn532_transport_unlock(void);
|
||||
|
||||
/** Raw PN532 frame body: TFI + payload (caller builds payload after TFI). */
|
||||
esp_err_t pn532_transport_exchange(const uint8_t *tx_body, size_t tx_body_len,
|
||||
uint8_t *rx_body, size_t rx_body_max,
|
||||
size_t *rx_body_len, int timeout_ms);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
114
firmware/components/pn532_host/pn532_core.c
Normal file
114
firmware/components/pn532_host/pn532_core.c
Normal file
@@ -0,0 +1,114 @@
|
||||
#include "pn532_host/pn532_core.h"
|
||||
#include "pn532_host/pn532_transport.h"
|
||||
#include "esp_log.h"
|
||||
#include <string.h>
|
||||
|
||||
static const char *TAG = "pn532_core";
|
||||
|
||||
esp_err_t pn532_send_cmd(const uint8_t *cmd_and_data, size_t len, uint8_t *response, size_t response_max,
|
||||
size_t *response_len, int timeout_ms)
|
||||
{
|
||||
if (!cmd_and_data || !response || !response_len) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
pn532_transport_lock();
|
||||
esp_err_t err =
|
||||
pn532_transport_exchange(cmd_and_data, len, response, response_max, response_len, timeout_ms);
|
||||
pn532_transport_unlock();
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
if (*response_len < 1) {
|
||||
return ESP_ERR_INVALID_RESPONSE;
|
||||
}
|
||||
if (response[0] != 0x00) {
|
||||
ESP_LOGW(TAG, "chip status 0x%02x", response[0]);
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t pn532_core_init(void)
|
||||
{
|
||||
return pn532_transport_init();
|
||||
}
|
||||
|
||||
esp_err_t pn532_get_firmware_version(uint8_t *ic_ver, uint8_t *fw_ver_hi, uint8_t *fw_ver_lo)
|
||||
{
|
||||
uint8_t cmd = PN532_CMD_GETFIRMWAREVERSION;
|
||||
uint8_t resp[16];
|
||||
size_t rlen = 0;
|
||||
esp_err_t e = pn532_send_cmd(&cmd, 1, resp, sizeof(resp), &rlen, 200);
|
||||
if (e != ESP_OK) {
|
||||
return e;
|
||||
}
|
||||
if (rlen < 4) {
|
||||
return ESP_ERR_INVALID_RESPONSE;
|
||||
}
|
||||
*ic_ver = resp[1];
|
||||
*fw_ver_hi = resp[2];
|
||||
*fw_ver_lo = resp[3];
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t pn532_sam_config_normal(void)
|
||||
{
|
||||
uint8_t buf[] = {PN532_CMD_SAMCONFIGURATION, 0x01, 0x14, 0x01};
|
||||
uint8_t resp[8];
|
||||
size_t rlen = 0;
|
||||
return pn532_send_cmd(buf, sizeof(buf), resp, sizeof(resp), &rlen, 200);
|
||||
}
|
||||
|
||||
esp_err_t pn532_get_general_status(uint8_t *buf, size_t buf_len, size_t *out_len)
|
||||
{
|
||||
uint8_t cmd = PN532_CMD_GETGENERALSTATUS;
|
||||
return pn532_send_cmd(&cmd, 1, buf, buf_len, out_len, 200);
|
||||
}
|
||||
|
||||
esp_err_t pn532_rf_field(bool on)
|
||||
{
|
||||
uint8_t buf[] = {PN532_CMD_RFCONFIGURATION, 0x01, on ? 0x01 : 0x00};
|
||||
uint8_t resp[8];
|
||||
size_t rlen = 0;
|
||||
return pn532_send_cmd(buf, sizeof(buf), resp, sizeof(resp), &rlen, 200);
|
||||
}
|
||||
|
||||
esp_err_t pn532_rf_max_retries(void)
|
||||
{
|
||||
/* RFConfiguration 0x05 MaxRetries: ATR/PSL/PassiveActivation — high values = more sensitivity to marginal tags */
|
||||
uint8_t buf[] = {PN532_CMD_RFCONFIGURATION, 0x05, 0xFF, 0xFF, 0xFF};
|
||||
uint8_t resp[8];
|
||||
size_t rlen = 0;
|
||||
return pn532_send_cmd(buf, sizeof(buf), resp, sizeof(resp), &rlen, 200);
|
||||
}
|
||||
|
||||
esp_err_t pn532_in_list_passive_target(uint8_t max_targets, uint8_t baud, uint8_t *response,
|
||||
size_t response_max, size_t *response_len)
|
||||
{
|
||||
uint8_t buf[] = {PN532_CMD_INLISTPASSIVETARGET, max_targets, baud};
|
||||
return pn532_send_cmd(buf, sizeof(buf), response, response_max, response_len, 500);
|
||||
}
|
||||
|
||||
esp_err_t pn532_in_data_exchange(const uint8_t *data, size_t data_len, uint8_t *response,
|
||||
size_t response_max, size_t *response_len)
|
||||
{
|
||||
if (!data || data_len == 0 || data_len > PN532_EEPROM_MAX_CMD_PAYLOAD - 1) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
uint8_t buf[PN532_EEPROM_MAX_CMD_PAYLOAD + 1];
|
||||
buf[0] = PN532_CMD_INDATAEXCHANGE;
|
||||
buf[1] = 0x01; /* logical target 1 */
|
||||
memcpy(buf + 2, data, data_len);
|
||||
return pn532_send_cmd(buf, 2 + data_len, response, response_max, response_len, 500);
|
||||
}
|
||||
|
||||
esp_err_t pn532_in_communicate_thru(const uint8_t *data, size_t data_len, uint8_t *response,
|
||||
size_t response_max, size_t *response_len)
|
||||
{
|
||||
if (!data || data_len > PN532_EEPROM_MAX_CMD_PAYLOAD - 1) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
uint8_t buf[PN532_EEPROM_MAX_CMD_PAYLOAD + 1];
|
||||
buf[0] = PN532_CMD_INCOMMUNICATETHRU;
|
||||
memcpy(buf + 1, data, data_len);
|
||||
return pn532_send_cmd(buf, 1 + data_len, response, response_max, response_len, 500);
|
||||
}
|
||||
361
firmware/components/pn532_host/pn532_transport.c
Normal file
361
firmware/components/pn532_host/pn532_transport.c
Normal file
@@ -0,0 +1,361 @@
|
||||
#include "pn532_host/pn532_transport.h"
|
||||
#include "sdkconfig.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "esp_timer.h"
|
||||
#include "driver/i2c.h"
|
||||
#include "driver/spi_master.h"
|
||||
#include "driver/uart.h"
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include <string.h>
|
||||
#include <sys/param.h>
|
||||
|
||||
#define PN532_HOST_TO_PN532 0xD4
|
||||
#define PN532_TXBUF_MAX 264
|
||||
|
||||
static const char *TAG = "pn532_xport";
|
||||
|
||||
#if defined(CONFIG_PN532_TRANSPORT_SPI)
|
||||
static spi_host_device_t pn532_spi_host(void)
|
||||
{
|
||||
return CONFIG_PN532_SPI_HOST == 3 ? SPI3_HOST : SPI2_HOST;
|
||||
}
|
||||
#endif
|
||||
|
||||
static SemaphoreHandle_t s_bus_mutex;
|
||||
|
||||
#if defined(CONFIG_PN532_TRANSPORT_SPI)
|
||||
static spi_device_handle_t s_spi;
|
||||
static int s_spi_cs_gpio = CONFIG_PN532_SPI_CS_GPIO;
|
||||
|
||||
static esp_err_t spi_wait_ready(int timeout_ms)
|
||||
{
|
||||
uint8_t status = 0;
|
||||
const int64_t end = esp_timer_get_time() / 1000 + timeout_ms;
|
||||
while ((esp_timer_get_time() / 1000) < (uint64_t)end) {
|
||||
spi_transaction_t t = {};
|
||||
uint8_t tx = 0x02; /* SPIstatus read */
|
||||
t.length = 8;
|
||||
t.tx_buffer = &tx;
|
||||
t.rx_buffer = &status;
|
||||
gpio_set_level(s_spi_cs_gpio, 0);
|
||||
esp_err_t e = spi_device_polling_transmit(s_spi, &t);
|
||||
gpio_set_level(s_spi_cs_gpio, 1);
|
||||
if (e != ESP_OK) {
|
||||
return e;
|
||||
}
|
||||
if (status & 0x01) {
|
||||
return ESP_OK;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
}
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
static esp_err_t spi_write_frame(const uint8_t *data, size_t len)
|
||||
{
|
||||
ESP_RETURN_ON_ERROR(spi_wait_ready(200), TAG, "wait before write");
|
||||
gpio_set_level(s_spi_cs_gpio, 0);
|
||||
vTaskDelay(pdMS_TO_TICKS(2));
|
||||
uint8_t hdr = 0x04; /* Data write */
|
||||
spi_transaction_t t0 = {};
|
||||
t0.length = 8;
|
||||
t0.tx_buffer = &hdr;
|
||||
ESP_RETURN_ON_ERROR(spi_device_polling_transmit(s_spi, &t0), TAG, "hdr");
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
spi_transaction_t t = {};
|
||||
t.length = 8;
|
||||
t.tx_buffer = &data[i];
|
||||
ESP_RETURN_ON_ERROR(spi_device_polling_transmit(s_spi, &t), TAG, "w");
|
||||
}
|
||||
gpio_set_level(s_spi_cs_gpio, 1);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t spi_read_bytes(uint8_t *out, size_t len)
|
||||
{
|
||||
ESP_RETURN_ON_ERROR(spi_wait_ready(300), TAG, "wait read");
|
||||
gpio_set_level(s_spi_cs_gpio, 0);
|
||||
vTaskDelay(pdMS_TO_TICKS(2));
|
||||
uint8_t hdr = 0x03; /* Data read */
|
||||
spi_transaction_t t0 = {};
|
||||
t0.length = 8;
|
||||
t0.tx_buffer = &hdr;
|
||||
ESP_RETURN_ON_ERROR(spi_device_polling_transmit(s_spi, &t0), TAG, "rhdr");
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
spi_transaction_t t = {};
|
||||
uint8_t tx = 0xFF;
|
||||
t.length = 8;
|
||||
t.tx_buffer = &tx;
|
||||
t.rx_buffer = &out[i];
|
||||
ESP_RETURN_ON_ERROR(spi_device_polling_transmit(s_spi, &t), TAG, "r");
|
||||
}
|
||||
gpio_set_level(s_spi_cs_gpio, 1);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
#elif defined(CONFIG_PN532_TRANSPORT_I2C)
|
||||
#define PN532_I2C_PORT ((i2c_port_t)CONFIG_PN532_I2C_PORT)
|
||||
|
||||
static esp_err_t i2c_wakeup(void)
|
||||
{
|
||||
const uint8_t w[] = {0x55, 0x55, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
i2c_cmd_handle_t c = i2c_cmd_link_create();
|
||||
i2c_master_start(c);
|
||||
i2c_master_write_byte(c, (CONFIG_PN532_I2C_ADDR << 1) | I2C_MASTER_WRITE, true);
|
||||
i2c_master_write(c, w, sizeof(w), I2C_MASTER_LAST_NACK);
|
||||
i2c_master_stop(c);
|
||||
esp_err_t e = i2c_master_cmd_begin(PN532_I2C_PORT, c, pdMS_TO_TICKS(50));
|
||||
i2c_cmd_link_delete(c);
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
return e;
|
||||
}
|
||||
|
||||
static esp_err_t i2c_write_raw(const uint8_t *buf, size_t len)
|
||||
{
|
||||
i2c_cmd_handle_t c = i2c_cmd_link_create();
|
||||
i2c_master_start(c);
|
||||
i2c_master_write_byte(c, (CONFIG_PN532_I2C_ADDR << 1) | I2C_MASTER_WRITE, true);
|
||||
i2c_master_write(c, buf, len, I2C_MASTER_LAST_NACK);
|
||||
i2c_master_stop(c);
|
||||
esp_err_t e = i2c_master_cmd_begin(PN532_I2C_PORT, c, pdMS_TO_TICKS(200));
|
||||
i2c_cmd_link_delete(c);
|
||||
return e;
|
||||
}
|
||||
|
||||
static esp_err_t i2c_read_raw(uint8_t *buf, size_t len)
|
||||
{
|
||||
i2c_cmd_handle_t c = i2c_cmd_link_create();
|
||||
i2c_master_start(c);
|
||||
i2c_master_write_byte(c, (CONFIG_PN532_I2C_ADDR << 1) | I2C_MASTER_READ, true);
|
||||
if (len > 1) {
|
||||
i2c_master_read(c, buf, len - 1, I2C_MASTER_ACK);
|
||||
}
|
||||
i2c_master_read_byte(c, buf + len - 1, I2C_MASTER_NACK);
|
||||
i2c_master_stop(c);
|
||||
esp_err_t e = i2c_master_cmd_begin(PN532_I2C_PORT, c, pdMS_TO_TICKS(200));
|
||||
i2c_cmd_link_delete(c);
|
||||
return e;
|
||||
}
|
||||
|
||||
#elif defined(CONFIG_PN532_TRANSPORT_HSU)
|
||||
#define PN532_UART ((uart_port_t)CONFIG_PN532_HSU_UART_NUM)
|
||||
|
||||
static esp_err_t hsu_write_raw(const uint8_t *buf, size_t len)
|
||||
{
|
||||
int n = uart_write_bytes(PN532_UART, buf, len);
|
||||
return (n == (int)len) ? ESP_OK : ESP_FAIL;
|
||||
}
|
||||
|
||||
static esp_err_t hsu_read_raw(uint8_t *buf, size_t len, int timeout_ms)
|
||||
{
|
||||
size_t got = 0;
|
||||
int64_t start = esp_timer_get_time();
|
||||
while (got < len) {
|
||||
int n = uart_read_bytes(PN532_UART, buf + got, len - got,
|
||||
pdMS_TO_TICKS(MAX(1, timeout_ms)));
|
||||
if (n > 0) {
|
||||
got += (size_t)n;
|
||||
}
|
||||
if ((esp_timer_get_time() - start) / 1000 > timeout_ms) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return got == len ? ESP_OK : ESP_ERR_TIMEOUT;
|
||||
}
|
||||
#endif
|
||||
|
||||
void pn532_transport_lock(void) { xSemaphoreTake(s_bus_mutex, portMAX_DELAY); }
|
||||
|
||||
void pn532_transport_unlock(void) { xSemaphoreGive(s_bus_mutex); }
|
||||
|
||||
static esp_err_t read_ack(int timeout_ms)
|
||||
{
|
||||
const uint8_t ack_ok[] = {0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00};
|
||||
uint8_t ack[6];
|
||||
#if defined(CONFIG_PN532_TRANSPORT_SPI)
|
||||
ESP_RETURN_ON_ERROR(spi_read_bytes(ack, sizeof(ack)), TAG, "read ack spi");
|
||||
#elif defined(CONFIG_PN532_TRANSPORT_I2C)
|
||||
vTaskDelay(pdMS_TO_TICKS(5));
|
||||
ESP_RETURN_ON_ERROR(i2c_read_raw(ack, sizeof(ack)), TAG, "read ack i2c");
|
||||
#elif defined(CONFIG_PN532_TRANSPORT_HSU)
|
||||
ESP_RETURN_ON_ERROR(hsu_read_raw(ack, sizeof(ack), timeout_ms), TAG, "read ack hsu");
|
||||
#endif
|
||||
if (memcmp(ack, ack_ok, 6) != 0) {
|
||||
ESP_LOG_BUFFER_HEX_LEVEL(TAG, ack, 6, ESP_LOG_WARN);
|
||||
return ESP_ERR_INVALID_RESPONSE;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t read_response_frame(uint8_t *body_out, size_t body_max, size_t *body_len, int timeout_ms)
|
||||
{
|
||||
uint8_t hdr[8];
|
||||
#if defined(CONFIG_PN532_TRANSPORT_SPI)
|
||||
ESP_RETURN_ON_ERROR(spi_read_bytes(hdr, 6), TAG, "hdr spi");
|
||||
#elif defined(CONFIG_PN532_TRANSPORT_I2C)
|
||||
{
|
||||
int64_t t0 = esp_timer_get_time() / 1000;
|
||||
bool ok = false;
|
||||
while (((esp_timer_get_time() / 1000) - t0) < timeout_ms) {
|
||||
uint8_t peek[1];
|
||||
if (i2c_read_raw(peek, 1) == ESP_OK && peek[0] == 0x01) {
|
||||
ok = true;
|
||||
break;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(2));
|
||||
}
|
||||
if (!ok) {
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
ESP_RETURN_ON_ERROR(i2c_read_raw(hdr, 6), TAG, "hdr i2c");
|
||||
}
|
||||
#elif defined(CONFIG_PN532_TRANSPORT_HSU)
|
||||
ESP_RETURN_ON_ERROR(hsu_read_raw(hdr, 6, timeout_ms), TAG, "hdr hsu");
|
||||
#endif
|
||||
if (hdr[0] != 0x00 || hdr[1] != 0x00 || hdr[2] != 0xFF) {
|
||||
ESP_LOG_BUFFER_HEX_LEVEL(TAG, hdr, 6, ESP_LOG_WARN);
|
||||
return ESP_ERR_INVALID_RESPONSE;
|
||||
}
|
||||
uint16_t L = (uint16_t)(hdr[3] * 256 + hdr[4]);
|
||||
uint8_t lcs = hdr[5];
|
||||
if ((uint8_t)((hdr[3] + hdr[4] + lcs) & 0xFF) != 0) {
|
||||
return ESP_ERR_INVALID_CRC;
|
||||
}
|
||||
if (L < 2) {
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
/* Read TFI..data (L bytes) + DCS — L includes TFI through last payload byte */
|
||||
const size_t read_total = (size_t)L + 1;
|
||||
if (read_total > 270) {
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
uint8_t chunk[272];
|
||||
#if defined(CONFIG_PN532_TRANSPORT_SPI)
|
||||
ESP_RETURN_ON_ERROR(spi_read_bytes(chunk, read_total), TAG, "payload spi");
|
||||
#elif defined(CONFIG_PN532_TRANSPORT_I2C)
|
||||
ESP_RETURN_ON_ERROR(i2c_read_raw(chunk, read_total), TAG, "payload i2c");
|
||||
#elif defined(CONFIG_PN532_TRANSPORT_HSU)
|
||||
ESP_RETURN_ON_ERROR(hsu_read_raw(chunk, read_total, timeout_ms), TAG, "payload hsu");
|
||||
#endif
|
||||
uint8_t tfi = chunk[0];
|
||||
if (tfi != 0xD5) {
|
||||
return ESP_ERR_INVALID_RESPONSE;
|
||||
}
|
||||
uint8_t sum = 0;
|
||||
for (uint16_t i = 0; i < L; i++) {
|
||||
sum += chunk[i];
|
||||
}
|
||||
uint8_t dcs = chunk[L];
|
||||
if ((uint8_t)((sum + dcs) & 0xFF) != 0) {
|
||||
return ESP_ERR_INVALID_CRC;
|
||||
}
|
||||
*body_len = (size_t)L - 1;
|
||||
if (*body_len > body_max) {
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
memcpy(body_out, chunk + 1, *body_len);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t pn532_transport_exchange(const uint8_t *tx_body, size_t tx_body_len, uint8_t *rx_body,
|
||||
size_t rx_body_max, size_t *rx_body_len, int timeout_ms)
|
||||
{
|
||||
if (tx_body_len == 0 || tx_body_len > 255) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
uint16_t L = (uint16_t)(1 + tx_body_len);
|
||||
uint8_t lcs = (uint8_t)(0x100 - (uint8_t)(((L >> 8) + (L & 0xFF)) & 0xFF));
|
||||
uint8_t sum = PN532_HOST_TO_PN532;
|
||||
for (size_t i = 0; i < tx_body_len; i++) {
|
||||
sum += tx_body[i];
|
||||
}
|
||||
dcs = (uint8_t)(256 - sum);
|
||||
|
||||
uint8_t frame[PN532_TXBUF_MAX];
|
||||
size_t pos = 0;
|
||||
frame[pos++] = 0x00;
|
||||
frame[pos++] = 0x00;
|
||||
frame[pos++] = 0xFF;
|
||||
frame[pos++] = (uint8_t)((L >> 8) & 0xFF);
|
||||
frame[pos++] = (uint8_t)(L & 0xFF);
|
||||
frame[pos++] = lcs;
|
||||
frame[pos++] = PN532_HOST_TO_PN532;
|
||||
memcpy(frame + pos, tx_body, tx_body_len);
|
||||
pos += tx_body_len;
|
||||
frame[pos++] = dcs;
|
||||
|
||||
#if defined(CONFIG_PN532_TRANSPORT_SPI)
|
||||
ESP_RETURN_ON_ERROR(spi_write_frame(frame, pos), TAG, "spi wr");
|
||||
#elif defined(CONFIG_PN532_TRANSPORT_I2C)
|
||||
i2c_wakeup();
|
||||
ESP_RETURN_ON_ERROR(i2c_write_raw(frame, pos), TAG, "i2c wr");
|
||||
#elif defined(CONFIG_PN532_TRANSPORT_HSU)
|
||||
ESP_RETURN_ON_ERROR(hsu_write_raw(frame, pos), TAG, "hsu wr");
|
||||
#endif
|
||||
|
||||
ESP_RETURN_ON_ERROR(read_ack(timeout_ms), TAG, "ack");
|
||||
return read_response_frame(rx_body, rx_body_max, rx_body_len, timeout_ms);
|
||||
}
|
||||
|
||||
esp_err_t pn532_transport_init(void)
|
||||
{
|
||||
s_bus_mutex = xSemaphoreCreateMutex();
|
||||
if (!s_bus_mutex) {
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
#if defined(CONFIG_PN532_TRANSPORT_SPI)
|
||||
spi_bus_config_t buscfg = {
|
||||
.mosi_io_num = CONFIG_PN532_SPI_MOSI_GPIO,
|
||||
.miso_io_num = CONFIG_PN532_SPI_MISO_GPIO,
|
||||
.sclk_io_num = CONFIG_PN532_SPI_SCLK_GPIO,
|
||||
.quadwp_io_num = -1,
|
||||
.quadhd_io_num = -1,
|
||||
.max_transfer_sz = 512,
|
||||
};
|
||||
spi_host_device_t host = pn532_spi_host();
|
||||
ESP_RETURN_ON_ERROR(spi_bus_initialize(host, &buscfg, SPI_DMA_CH_AUTO), TAG, "spi bus");
|
||||
spi_device_interface_config_t devcfg = {
|
||||
.clock_speed_hz = CONFIG_PN532_SPI_CLOCK_HZ,
|
||||
.mode = 0,
|
||||
.spics_io_num = -1, /* manual CS */
|
||||
.queue_size = 4,
|
||||
};
|
||||
ESP_RETURN_ON_ERROR(spi_bus_add_device(host, &devcfg, &s_spi), TAG, "spi dev");
|
||||
gpio_reset_pin((gpio_num_t)s_spi_cs_gpio);
|
||||
gpio_set_direction((gpio_num_t)s_spi_cs_gpio, GPIO_MODE_OUTPUT);
|
||||
gpio_set_level(s_spi_cs_gpio, 1);
|
||||
#elif defined(CONFIG_PN532_TRANSPORT_I2C)
|
||||
i2c_config_t ic = {
|
||||
.mode = I2C_MODE_MASTER,
|
||||
.sda_io_num = CONFIG_PN532_I2C_SDA_GPIO,
|
||||
.scl_io_num = CONFIG_PN532_I2C_SCL_GPIO,
|
||||
.sda_pullup_en = GPIO_PULLUP_ENABLE,
|
||||
.scl_pullup_en = GPIO_PULLUP_ENABLE,
|
||||
.master = {.clk_speed = 400000},
|
||||
};
|
||||
ESP_RETURN_ON_ERROR(i2c_param_config(PN532_I2C_PORT, &ic), TAG, "i2c cfg");
|
||||
ESP_RETURN_ON_ERROR(i2c_driver_install(PN532_I2C_PORT, I2C_MODE_MASTER, 0, 0, 0), TAG, "i2c drvr");
|
||||
#elif defined(CONFIG_PN532_TRANSPORT_HSU)
|
||||
uart_config_t uc = {
|
||||
.baud_rate = CONFIG_PN532_HSU_BAUD,
|
||||
.data_bits = UART_DATA_8_BITS,
|
||||
.parity = UART_PARITY_DISABLE,
|
||||
.stop_bits = UART_STOP_BITS_1,
|
||||
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
|
||||
.source_clk = UART_SCLK_DEFAULT,
|
||||
};
|
||||
ESP_RETURN_ON_ERROR(uart_driver_install(PN532_UART, 2048, 2048, 0, NULL, 0), TAG, "uart");
|
||||
ESP_RETURN_ON_ERROR(uart_param_config(PN532_UART, &uc), TAG, "uart cfg");
|
||||
ESP_RETURN_ON_ERROR(uart_set_pin(PN532_UART, CONFIG_PN532_HSU_TX_GPIO, CONFIG_PN532_HSU_RX_GPIO,
|
||||
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE),
|
||||
TAG, "uart pins");
|
||||
#endif
|
||||
return ESP_OK;
|
||||
}
|
||||
</think>
|
||||
Fixing a typo in `pn532_transport.c` and correcting the DCS checksum calculation.
|
||||
|
||||
<|tool▁calls▁begin|><|tool▁call▁begin|>
|
||||
Read
|
||||
80
firmware/data/assets/index-EAAhhled.js
Normal file
80
firmware/data/assets/index-EAAhhled.js
Normal file
File diff suppressed because one or more lines are too long
1
firmware/data/assets/index-hoMg1Qkq.css
Normal file
1
firmware/data/assets/index-hoMg1Qkq.css
Normal file
File diff suppressed because one or more lines are too long
20
firmware/data/index.html
Normal file
20
firmware/data/index.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="dark hack-root">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#020408" />
|
||||
<title>PN532 // MAXIMAL_FIELD</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Audiowide&family=JetBrains+Mono:ital,wght@0,400;0,600;0,700;1,400&family=Orbitron:wght@500;600;700;800&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script type="module" crossorigin src="/assets/index-EAAhhled.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-hoMg1Qkq.css">
|
||||
</head>
|
||||
<body class="bg-bubble-950 text-slate-200 antialiased selection:bg-bubble-accent/40 selection:text-bubble-950">
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
12
firmware/flash.sh
Executable file
12
firmware/flash.sh
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
# Flash PN532 toolkit. Requires ESP-IDF 5.x in PATH (run export.sh first).
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||
PORT="${ESPPORT:-${1:-/dev/cu.usbserial-A5069RR4}}"
|
||||
cd "$ROOT"
|
||||
if ! command -v idf.py >/dev/null 2>&1; then
|
||||
echo "idf.py not found. Source your ESP-IDF export.sh, then re-run:" >&2
|
||||
echo " ESPPORT=$PORT $0" >&2
|
||||
exit 1
|
||||
fi
|
||||
idf.py -p "$PORT" flash
|
||||
3
firmware/main/CMakeLists.txt
Normal file
3
firmware/main/CMakeLists.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
idf_component_register(SRCS "main.c" "board_rgb_off.c" INCLUDE_DIRS "." REQUIRES net_service nfc_engine led_strip)
|
||||
|
||||
spiffs_create_partition_image(storage ../data FLASH_IN_PROJECT)
|
||||
18
firmware/main/Kconfig.projbuild
Normal file
18
firmware/main/Kconfig.projbuild
Normal file
@@ -0,0 +1,18 @@
|
||||
menu "Board indicators"
|
||||
|
||||
config BOARD_RGB_LED_ENABLE
|
||||
bool "Turn off onboard addressable RGB at boot (WS2812/SK6812)"
|
||||
default y
|
||||
help
|
||||
ESP32-S3-DevKitC-1 v1.x typically uses one SK6812/WS2812 on GPIO 48.
|
||||
Disable if your board has no addressable LED or uses a different data pin.
|
||||
|
||||
config BOARD_RGB_LED_GPIO
|
||||
int "Addressable LED data GPIO"
|
||||
default 48
|
||||
range 0 48
|
||||
depends on BOARD_RGB_LED_ENABLE
|
||||
help
|
||||
Official DevKitC-1 v1.1: GPIO 48. Older notes sometimes cite GPIO 38 — set in menuconfig if needed.
|
||||
|
||||
endmenu
|
||||
37
firmware/main/board_rgb_off.c
Normal file
37
firmware/main/board_rgb_off.c
Normal file
@@ -0,0 +1,37 @@
|
||||
#include "sdkconfig.h"
|
||||
#include "board_rgb_off.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
#if CONFIG_BOARD_RGB_LED_ENABLE
|
||||
#include "esp_err.h"
|
||||
#include "led_strip.h"
|
||||
#endif
|
||||
|
||||
void board_rgb_led_quiet(void)
|
||||
{
|
||||
#if CONFIG_BOARD_RGB_LED_ENABLE
|
||||
led_strip_handle_t strip = NULL;
|
||||
const led_strip_config_t strip_config = {
|
||||
.strip_gpio_num = CONFIG_BOARD_RGB_LED_GPIO,
|
||||
.max_leds = 1,
|
||||
.led_pixel_format = LED_PIXEL_FORMAT_GRB,
|
||||
.led_model = LED_MODEL_WS2812,
|
||||
.flags = {.invert_out = false},
|
||||
};
|
||||
const led_strip_rmt_config_t rmt_config = {
|
||||
.clk_src = RMT_CLK_SRC_DEFAULT,
|
||||
.resolution_hz = 10 * 1000 * 1000,
|
||||
.flags = {.with_dma = false},
|
||||
};
|
||||
esp_err_t err = led_strip_new_rmt_device(&strip_config, &rmt_config, &strip);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW("board_rgb", "RGB init failed (%s), leaving LED as-is", esp_err_to_name(err));
|
||||
return;
|
||||
}
|
||||
err = led_strip_clear(strip);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW("board_rgb", "RGB clear failed (%s)", esp_err_to_name(err));
|
||||
}
|
||||
led_strip_del(strip);
|
||||
#endif
|
||||
}
|
||||
4
firmware/main/board_rgb_off.h
Normal file
4
firmware/main/board_rgb_off.h
Normal file
@@ -0,0 +1,4 @@
|
||||
#pragma once
|
||||
|
||||
/** One-shot: drive onboard WS2812/SK6812 to black, then release RMT. */
|
||||
void board_rgb_led_quiet(void);
|
||||
3
firmware/main/idf_component.yml
Normal file
3
firmware/main/idf_component.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
## IDF Component Manager — addressable RGB (DevKitC-1)
|
||||
dependencies:
|
||||
espressif/led_strip: "^2.5.5"
|
||||
17
firmware/main/main.c
Normal file
17
firmware/main/main.c
Normal file
@@ -0,0 +1,17 @@
|
||||
#include "esp_log.h"
|
||||
#include "board_rgb_off.h"
|
||||
#include "nfc_engine/nfc_engine.h"
|
||||
#include "nfc_engine/session_capture.h"
|
||||
#include "net_service/app_net.h"
|
||||
|
||||
static const char *TAG = "main";
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
board_rgb_led_quiet();
|
||||
ESP_LOGI(TAG, "PN532 NFC Toolkit starting");
|
||||
ESP_ERROR_CHECK(nfc_engine_init());
|
||||
session_capture_init();
|
||||
ESP_ERROR_CHECK(app_net_init());
|
||||
ESP_LOGI(TAG, "Open AP SSID PN532-Toolkit — http://192.168.4.1");
|
||||
}
|
||||
9
firmware/partitions.csv
Normal file
9
firmware/partitions.csv
Normal file
@@ -0,0 +1,9 @@
|
||||
# 8MB flash layout (ESP32-S3-DevKitC-1 N8). For 16MB, increase storage size.
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
nvs, data, nvs, 0x9000, 0x6000,
|
||||
otadata, data, ota, 0xf000, 0x2000,
|
||||
phy_init, data, phy, 0x11000, 0x1000,
|
||||
factory, app, factory, 0x20000, 0x180000,
|
||||
ota_0, app, ota_0, 0x1A0000,0x180000,
|
||||
ota_1, app, ota_1, 0x320000,0x180000,
|
||||
storage, data, spiffs, 0x4A0000,0x350000,
|
||||
|
34
firmware/sdkconfig.defaults
Normal file
34
firmware/sdkconfig.defaults
Normal file
@@ -0,0 +1,34 @@
|
||||
CONFIG_IDF_TARGET_ESP32S3=y
|
||||
CONFIG_ESPTOOLPY_FLASHMODE_QIO=y
|
||||
CONFIG_ESPTOOLPY_FLASHFREQ_80M=y
|
||||
CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y
|
||||
|
||||
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y
|
||||
# Enable if module has PSRAM (e.g. N8R8)
|
||||
# CONFIG_SPIRAM=y
|
||||
# CONFIG_SPIRAM_MODE_OCT=y
|
||||
# CONFIG_SPIRAM_SPEED_80M=y
|
||||
|
||||
CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y
|
||||
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192
|
||||
CONFIG_FREERTOS_HZ=1000
|
||||
|
||||
CONFIG_HTTPD_WS_SUPPORT=y
|
||||
CONFIG_HTTPD_MAX_REQ_HDR_LEN=1024
|
||||
CONFIG_HTTPD_MAX_URI_LEN=512
|
||||
CONFIG_ESP_HTTP_CLIENT_ENABLE_HTTPS=y
|
||||
|
||||
CONFIG_LWIP_LOCAL_IP4_TTL=64
|
||||
CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM=10
|
||||
CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=32
|
||||
|
||||
CONFIG_PARTITION_TABLE_CUSTOM=y
|
||||
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
|
||||
CONFIG_PARTITION_TABLE_FILENAME="partitions.csv"
|
||||
CONFIG_PARTITION_TABLE_OFFSET=0x8000
|
||||
|
||||
CONFIG_LOG_DEFAULT_LEVEL_INFO=y
|
||||
CONFIG_LOG_MAXIMUM_LEVEL_VERBOSE=y
|
||||
|
||||
# mDNS
|
||||
CONFIG_MDNS_MAX_SERVICES=10
|
||||
Reference in New Issue
Block a user