Firmware:
- nfc_engine: add nfc_engine_try_init() (non-fatal, sets s_pn532_ready),
nfc_engine_try_reattach() (soft re-init, skips bus re-init),
nfc_engine_is_ready() accessor
- main: replace ESP_ERROR_CHECK(nfc_engine_init) with nfc_engine_try_init;
device boots and serves web UI even with no PN532 connected
- app_net: add reconnect_task — every 5s retries nfc_engine_try_reattach()
and broadcasts {"channel":"pn532","payload":{"connected":true}} over WS
- app_net: scan_loop_task skips polling when !nfc_engine_is_ready()
- app_net/api_status: always emit pn532Connected bool; null-guard pn532 fw object
- find_sector_hit / program_classic_snapshot_locked: null-guard cJSON array items
- session_capture: abort if xSemaphoreCreateMutex() returns NULL
Web:
- NfcWsContext: track pn532Connected state from WS pn532 channel + status fetch on connect
- App.tsx: live HeaderBadge (LIVE/NO RF/WAIT) replacing static text
- Dashboard: READY/SEARCHING pill with fw version when available
- api.ts: add pn532Connected to Status type
- toast.tsx: fix ID collision (Date.now + Math.random)
- Capture: surface status fetch errors
- ReadAnalyze: add error feedback for readUl when no data returned
- WriteClone: busy state on both write buttons
- RawConsole: toast when frame returns error not response
- Emulate: validate hex before send (non-empty, even length, hex chars only)
- Brute: warn and skip invalid custom key lines
Made-with: Cursor
407 lines
12 KiB
C
407 lines
12 KiB
C
#include "nfc_engine/nfc_engine.h"
|
|
#include "pn532_host/pn532_core.h"
|
|
#include "esp_log.h"
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <sys/param.h>
|
|
|
|
static const char *TAG = "nfc_engine";
|
|
|
|
static uint8_t s_tg = 1;
|
|
static volatile bool s_pn532_ready = false;
|
|
|
|
bool nfc_engine_is_ready(void) { return s_pn532_ready; }
|
|
|
|
static void hint_type(nfc_tag_info_t *t)
|
|
{
|
|
switch (t->sak) {
|
|
case 0x08:
|
|
case 0x09:
|
|
case 0x18:
|
|
t->type_hint = 1;
|
|
break;
|
|
case 0x00:
|
|
/* Typical Type 2 inventory tuple is ATQA 0x0044 and 7-byte UID. */
|
|
if ((t->atqa == 0x4400 || t->atqa == 0x0044) && t->uid_len == 7) {
|
|
t->type_hint = 2;
|
|
} else {
|
|
t->type_hint = 0;
|
|
}
|
|
break;
|
|
default:
|
|
t->type_hint = 0;
|
|
break;
|
|
}
|
|
}
|
|
|
|
bool nfc_tag_is_mifare_classic(const nfc_tag_info_t *tag)
|
|
{
|
|
return tag && tag->type_hint == 1;
|
|
}
|
|
|
|
bool nfc_tag_is_mifare_classic_4k(const nfc_tag_info_t *tag)
|
|
{
|
|
return nfc_tag_is_mifare_classic(tag) && tag->sak == 0x18;
|
|
}
|
|
|
|
bool nfc_tag_is_type2(const nfc_tag_info_t *tag)
|
|
{
|
|
return tag && tag->type_hint == 2;
|
|
}
|
|
|
|
int nfc_mifare_sector_count(const nfc_tag_info_t *tag)
|
|
{
|
|
if (!nfc_tag_is_mifare_classic(tag)) {
|
|
return 0;
|
|
}
|
|
return nfc_tag_is_mifare_classic_4k(tag) ? 40 : 16;
|
|
}
|
|
|
|
bool nfc_mifare_sector_layout(const nfc_tag_info_t *tag, int sector, int *first_block, int *num_blocks,
|
|
uint8_t *trailer_block)
|
|
{
|
|
if (!nfc_tag_is_mifare_classic(tag) || !first_block || !num_blocks || !trailer_block) {
|
|
return false;
|
|
}
|
|
if (!nfc_tag_is_mifare_classic_4k(tag)) {
|
|
if (sector < 0 || sector > 15) {
|
|
return false;
|
|
}
|
|
*first_block = sector * 4;
|
|
*num_blocks = 4;
|
|
*trailer_block = (uint8_t)(sector * 4 + 3);
|
|
return true;
|
|
}
|
|
if (sector < 0 || sector > 39) {
|
|
return false;
|
|
}
|
|
if (sector <= 31) {
|
|
*first_block = sector * 4;
|
|
*num_blocks = 4;
|
|
*trailer_block = (uint8_t)(sector * 4 + 3);
|
|
} else {
|
|
int r = sector - 32;
|
|
*first_block = 128 + r * 16;
|
|
*num_blocks = 16;
|
|
*trailer_block = (uint8_t)(128 + r * 16 + 15);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
esp_err_t nfc_engine_init(void)
|
|
{
|
|
esp_err_t e = pn532_core_init();
|
|
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");
|
|
}
|
|
s_pn532_ready = true;
|
|
return ESP_OK;
|
|
}
|
|
|
|
bool nfc_engine_try_init(void)
|
|
{
|
|
esp_err_t e = pn532_core_init();
|
|
if (e != ESP_OK) {
|
|
ESP_LOGW(TAG, "PN532 not found (%s) — AP running, will retry every 5 s", esp_err_to_name(e));
|
|
s_pn532_ready = false;
|
|
return false;
|
|
}
|
|
e = pn532_sam_config_normal();
|
|
if (e != ESP_OK) {
|
|
ESP_LOGW(TAG, "SAM config: %s", esp_err_to_name(e));
|
|
}
|
|
uint8_t ic = 0, hi = 0, lo = 0;
|
|
if (pn532_get_firmware_version(&ic, &hi, &lo) == ESP_OK) {
|
|
ESP_LOGI(TAG, "PN532 fw ic=0x%02x %u.%u", ic, hi, lo);
|
|
}
|
|
if (pn532_rf_max_retries() != ESP_OK) {
|
|
ESP_LOGW(TAG, "RF max retries config failed");
|
|
}
|
|
s_pn532_ready = true;
|
|
return true;
|
|
}
|
|
|
|
bool nfc_engine_try_reattach(void)
|
|
{
|
|
/* Transport already open — just ping the chip and re-apply configuration. */
|
|
esp_err_t e = pn532_sam_config_normal();
|
|
if (e != ESP_OK) {
|
|
return false;
|
|
}
|
|
uint8_t ic = 0, hi = 0, lo = 0;
|
|
if (pn532_get_firmware_version(&ic, &hi, &lo) == ESP_OK) {
|
|
ESP_LOGI(TAG, "PN532 reattached ic=0x%02x %u.%u", ic, hi, lo);
|
|
}
|
|
if (pn532_rf_max_retries() != ESP_OK) {
|
|
ESP_LOGW(TAG, "RF max retries config failed");
|
|
}
|
|
s_pn532_ready = true;
|
|
return true;
|
|
}
|
|
|
|
esp_err_t nfc_poll_passive_target(nfc_tag_info_t *out)
|
|
{
|
|
if (!out) {
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
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) {
|
|
return ESP_ERR_INVALID_RESPONSE;
|
|
}
|
|
if (resp[0] < 1) {
|
|
return ESP_ERR_NOT_FOUND;
|
|
}
|
|
if (rlen < 7) {
|
|
return ESP_ERR_INVALID_RESPONSE;
|
|
}
|
|
s_tg = resp[1];
|
|
out->atqa = (uint16_t)(((uint16_t)resp[2] << 8) | resp[3]);
|
|
out->sak = resp[4];
|
|
out->uid_len = resp[5];
|
|
if (out->uid_len > NFC_MAX_UID_LEN || (size_t)(6 + out->uid_len) > rlen) {
|
|
return ESP_ERR_INVALID_RESPONSE;
|
|
}
|
|
memcpy(out->uid, resp + 6, 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 || !response || !response_len || data_len == 0 || data_len > sizeof(buf) - 2) {
|
|
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)
|
|
{
|
|
if (!tag || !key) {
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
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 < 2 || resp[0] != (uint8_t)(PN532_CMD_INDATAEXCHANGE + 1) || resp[1] != 0x00) {
|
|
return ESP_FAIL;
|
|
}
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t nfc_mifare_read_block(uint8_t block_no, uint8_t block[NFC_BLOCK_LEN])
|
|
{
|
|
if (!block) {
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
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 < 2 + NFC_BLOCK_LEN || resp[0] != (uint8_t)(PN532_CMD_INDATAEXCHANGE + 1) || resp[1] != 0x00) {
|
|
return ESP_ERR_INVALID_RESPONSE;
|
|
}
|
|
memcpy(block, resp + 2, NFC_BLOCK_LEN);
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t nfc_mifare_write_block(uint8_t block_no, const uint8_t block[NFC_BLOCK_LEN])
|
|
{
|
|
if (!block) {
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
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 < 2 || resp[0] != (uint8_t)(PN532_CMD_INDATAEXCHANGE + 1) || resp[1] != 0x00) {
|
|
return ESP_FAIL;
|
|
}
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t nfc_ultralight_read_page(uint8_t page, uint8_t data[4])
|
|
{
|
|
if (!data) {
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
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 < 2 + 16 || resp[0] != (uint8_t)(PN532_CMD_INDATAEXCHANGE + 1) || resp[1] != 0x00) {
|
|
return ESP_ERR_INVALID_RESPONSE;
|
|
}
|
|
memcpy(data, resp + 2, 4);
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t nfc_ultralight_write_page(uint8_t page, const uint8_t data[4])
|
|
{
|
|
if (!data) {
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
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 < 2 || resp[0] != (uint8_t)(PN532_CMD_INDATAEXCHANGE + 1) || resp[1] != 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)
|
|
{
|
|
if (!out || !got || out_max == 0) {
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
*got = 0;
|
|
size_t max_pages = MAX((size_t)1, MIN((size_t)16, out_max / 4));
|
|
size_t remaining_pages = 256U - (size_t)start_page;
|
|
if (max_pages > remaining_pages) {
|
|
max_pages = remaining_pages;
|
|
}
|
|
uint8_t end_page = (uint8_t)(start_page + max_pages - 1);
|
|
uint8_t d[3] = {0x3A, start_page, end_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] != (uint8_t)(PN532_CMD_INDATAEXCHANGE + 1) || resp[1] != 0x00) {
|
|
return ESP_ERR_INVALID_RESPONSE;
|
|
}
|
|
size_t payload = rlen - 2;
|
|
if (payload > out_max) {
|
|
payload = out_max;
|
|
}
|
|
memcpy(out, resp + 2, payload);
|
|
*got = payload;
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t nfc_type2_get_version(uint8_t version[8])
|
|
{
|
|
if (!version) {
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
uint8_t d[] = {0x60};
|
|
uint8_t resp[32];
|
|
size_t rlen = 0;
|
|
esp_err_t e = in_data_tg(d, sizeof(d), resp, sizeof(resp), &rlen);
|
|
if (e != ESP_OK) {
|
|
return e;
|
|
}
|
|
if (rlen < 2 + 8 || resp[0] != (uint8_t)(PN532_CMD_INDATAEXCHANGE + 1) || resp[1] != 0x00) {
|
|
return ESP_ERR_INVALID_RESPONSE;
|
|
}
|
|
memcpy(version, resp + 2, 8);
|
|
return ESP_OK;
|
|
}
|
|
|
|
cJSON *nfc_tag_to_json(const nfc_tag_info_t *tag)
|
|
{
|
|
if (!tag || tag->uid_len > NFC_MAX_UID_LEN) {
|
|
return NULL;
|
|
}
|
|
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 (nfc_tag_is_mifare_classic(tag)) {
|
|
guess = nfc_tag_is_mifare_classic_4k(tag) ? "MIFARE Classic 4K" : "MIFARE Classic 1K or compatible";
|
|
} else if (nfc_tag_is_type2(tag)) {
|
|
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;
|
|
}
|