esp32/main/pn532.c
raw ยท 12454 bytes
#include <string.h>
#include "driver/uart.h"
#include "esp_err.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "reader.h"
#include "relay.h"
#define PN532_UART UART_NUM_1
#define PN532_TX_GPIO 3
#define PN532_RX_GPIO 4
#define PN532_BAUD 115200
#define COMMAND_TIMEOUT_MS 1000
#define POLL_INTERVAL_MS 300
static const char *TAG = "pn532";
static const uint8_t ack[] = {0x00, 0x00, 0xff, 0x00, 0xff, 0x00};
static void pn532_uart_init(void)
{
const uart_config_t config = {
.baud_rate = PN532_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_ERROR_CHECK(uart_driver_install(PN532_UART, 1024, 0, 0, NULL, 0));
ESP_ERROR_CHECK(uart_param_config(PN532_UART, &config));
ESP_ERROR_CHECK(uart_set_pin(PN532_UART, PN532_TX_GPIO, PN532_RX_GPIO,
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
ESP_ERROR_CHECK(uart_flush_input(PN532_UART));
ESP_LOGI(TAG, "UART ready: TX=GPIO%d RX=GPIO%d baud=%d",
PN532_TX_GPIO, PN532_RX_GPIO, PN532_BAUD);
}
static bool write_bytes(const uint8_t *data, size_t len)
{
ESP_LOG_BUFFER_HEX_LEVEL(TAG, data, len, ESP_LOG_DEBUG);
return uart_write_bytes(PN532_UART, data, len) == (int)len &&
uart_wait_tx_done(PN532_UART, pdMS_TO_TICKS(100)) == ESP_OK;
}
/* One deadline for the entire command, including ACK and fragmented response. */
static bool read_exact(uint8_t *data, size_t len, int64_t deadline)
{
size_t received = 0;
while (received < len) {
int64_t remaining = deadline - esp_timer_get_time();
if (remaining <= 0) {
return false;
}
TickType_t ticks = pdMS_TO_TICKS((remaining + 999) / 1000);
if (ticks == 0) {
ticks = 1;
}
int n = uart_read_bytes(PN532_UART, data + received, len - received, ticks);
if (n < 0) {
return false;
}
received += (size_t)n;
}
return true;
}
/* Returns 0 for ACK, positive for a normal frame, -1 for malformed/timeout.
* Extended frames are unnecessary for these small commands and are rejected.
*/
static bool read_logged(uint8_t *buffer, size_t len,
int64_t deadline, const char *stage)
{
if (!read_exact(buffer, len, deadline)) {
ESP_LOGW(TAG, "RX failed at %s: wanted %u bytes",
stage, (unsigned)len);
return false;
}
#ifdef FRAME_DEBUG
ESP_LOGI(TAG, "RX %s (%u bytes)", stage, (unsigned)len);
ESP_LOG_BUFFER_HEX_LEVEL(TAG, buffer, len, ESP_LOG_INFO);
#endif
return true;
}
static int read_frame(uint8_t *payload, size_t capacity, int64_t deadline)
{
uint8_t prefix[3] = {0};
do {
prefix[0] = prefix[1];
prefix[1] = prefix[2];
if (!read_logged(&prefix[2], 1, deadline, "preamble byte")) {
return -1;
}
} while (prefix[0] != 0 ||
prefix[1] != 0 ||
prefix[2] != 0xff);
uint8_t lengths[2];
if (!read_logged(lengths, sizeof(lengths), deadline, "LEN/LCS")) {
return -1;
}
if (lengths[0] == 0 && lengths[1] == 0xff) {
uint8_t postamble;
if (!read_logged(&postamble, 1, deadline, "ACK postamble")) {
return -1;
}
if (postamble != 0) {
ESP_LOGW(TAG, "invalid ACK postamble");
return -1;
}
return 0;
}
size_t len = lengths[0];
if (len == 0 ||
(uint8_t)(lengths[0] + lengths[1]) != 0 ||
len > capacity) {
ESP_LOGW(TAG, "invalid length: LEN=%u LCS=0x%02X capacity=%u",
(unsigned)len, lengths[1], (unsigned)capacity);
return -1;
}
if (!read_logged(payload, len, deadline, "payload")) {
return -1;
}
uint8_t tail[2];
if (!read_logged(tail, sizeof(tail), deadline, "DCS/postamble")) {
return -1;
}
if (tail[1] != 0) {
ESP_LOGW(TAG, "invalid postamble: 0x%02X", tail[1]);
return -1;
}
uint8_t checksum = tail[0];
for (size_t i = 0; i < len; ++i) {
checksum += payload[i];
}
if (checksum != 0) {
ESP_LOGW(TAG, "invalid checksum: sum=0x%02X", checksum);
return -1;
}
return (int)len;
}
/* Normal frame builder. cmd includes command code, excludes host TFI (D4). */
static int command_full(const uint8_t *cmd, size_t cmd_len, uint8_t *response,
size_t capacity, unsigned timeout_ms)
{
uint8_t frame[256];
if (cmd_len == 0 || cmd_len > sizeof(frame) - 8 || cmd_len + 1 >= 255) {
return -1;
}
size_t len = cmd_len + 1;
frame[0] = 0;
frame[1] = 0;
frame[2] = 0xff;
frame[3] = (uint8_t)len;
frame[4] = (uint8_t)(0 - len);
frame[5] = 0xd4;
memcpy(frame + 6, cmd, cmd_len);
uint8_t checksum = 0xd4;
for (size_t i = 0; i < cmd_len; ++i) {
checksum += cmd[i];
}
frame[6 + cmd_len] = (uint8_t)(0 - checksum);
frame[7 + cmd_len] = 0;
#ifdef FRAME_DEBUG
ESP_LOGI(TAG, "TX command 0x%02X (%u bytes)",
cmd[0], (unsigned)(cmd_len + 8));
ESP_LOG_BUFFER_HEX_LEVEL(TAG, frame, cmd_len + 8, ESP_LOG_INFO);
#endif
bool startup = cmd[0] == 0x02 || cmd[0] == 0x14;
if (startup) {
static const uint8_t wakeup[] = {0x55, 0x55, 0, 0, 0};
uint8_t packet[sizeof(wakeup) + sizeof(frame)];
size_t packet_len = sizeof(wakeup) + cmd_len + 8;
memcpy(packet, wakeup, sizeof(wakeup));
memcpy(packet + sizeof(wakeup), frame, cmd_len + 8);
ESP_LOGI(TAG, "TX startup packet");
ESP_LOG_BUFFER_HEX_LEVEL(
TAG, packet, packet_len, ESP_LOG_INFO);
if (!write_bytes(packet, packet_len)) {
goto failed;
}
} else {
if (!write_bytes(frame, cmd_len + 8)) {
goto failed;
}
}
int64_t deadline =
esp_timer_get_time() + (int64_t)timeout_ms * 1000;
int n;
do {
n = read_frame(response, capacity, deadline);
#ifdef FRAME_DEBUG
if (n > 0) {
ESP_LOGI(TAG, "RX response (%d bytes)", n);
ESP_LOG_BUFFER_HEX_LEVEL(TAG, response, n, ESP_LOG_INFO);
} else if (n == 0) {
ESP_LOGI(TAG, "RX ACK");
} else {
ESP_LOGW(TAG, "RX failed or timed out: %d", n);
}
#else
if (n < 0) {
ESP_LOGW(TAG, "RX failed or timed out: %d", n);
}
#endif
} while (n == 0); /* ACK is not the command response. */
if (n >= 2 &&
response[0] == 0xd5 &&
response[1] == (uint8_t)(cmd[0] + 1)) {
return n;
}
failed:
/* Host ACK aborts a pending PN532 command. Drain before the next command. */
ESP_LOGW(TAG, "command 0x%02X failed or timed out", cmd[0]);
ESP_LOGI(TAG, "TX abort ACK (%u bytes)", (unsigned)sizeof(ack));
ESP_LOG_BUFFER_HEX_LEVEL(TAG, ack, sizeof(ack), ESP_LOG_INFO);
(void)write_bytes(ack, sizeof(ack));
vTaskDelay(pdMS_TO_TICKS(20));
(void)uart_flush_input(PN532_UART);
return -1;
}
static int command(const uint8_t *cmd, size_t length, uint8_t response[128])
{
return command_full(cmd, length, response, 128, COMMAND_TIMEOUT_MS);
}
int pn532_exchange(uint8_t target, const uint8_t *apdu, size_t length, uint8_t *out,
size_t capacity, size_t *out_length)
{
*out_length = 0;
if (target == 0 || length < 4 || length > RELAY_APDU_MAX)
return 1;
uint8_t cmd[RELAY_APDU_MAX + 2] = {0x40, target};
uint8_t response[256];
memcpy(cmd + 2, apdu, length);
int n = command_full(cmd, length + 2, response, sizeof(response), 3000);
if (n < 3)
return 2;
/* Error code + MI/chaining flags. Chained PN532 responses are outside
* this STANDARD-auth transport's bounded APDU contract. */
if (response[2] != 0) {
ESP_LOGW(TAG, "InDataExchange status=0x%02x", response[2]);
return 3;
}
if ((size_t)(n - 3) > capacity)
return 4;
memcpy(out, response + 3, n - 3);
*out_length = n - 3;
return 0;
}
static void release_rf(void)
{
static const uint8_t rf_off[] = {0x32, 0x01, 0x00};
uint8_t response[128];
if (command(rf_off, sizeof(rf_off), response) != 2)
ESP_LOGW(TAG, "RF-off failed");
}
static bool initialize_reader(void)
{
static const uint8_t firmware[] = {0x02};
static const uint8_t sam[] = {0x14, 0x01, 0x14, 0x01};
static const uint8_t retries[] = {0x32, 0x05, 0xff, 0x01, 0x00};
uint8_t response[128];
/* Discard stale input before starting this attempt. */
ESP_ERROR_CHECK(uart_flush_input(PN532_UART));
int n = command(firmware, sizeof(firmware), response);
if (n != 6 || response[2] != 0x32) {
ESP_LOGE(TAG, "invalid firmware response: length=%d", n);
return false;
}
ESP_LOGI(TAG, "PN532 firmware %u.%u, support=0x%02X",
response[3], response[4], response[5]);
vTaskDelay(pdMS_TO_TICKS(100));
if (command(sam, sizeof(sam), response) != 2) {
ESP_LOGE(TAG, "SAMConfiguration failed");
return false;
}
vTaskDelay(pdMS_TO_TICKS(100));
if (command(retries, sizeof(retries), response) != 2) {
return false;
}
return true;
}
/* ECP v2 Home Key, with software CRC-A. These bytes identify a Home;
* they are public routing metadata, never authentication credentials. */
static void build_ecp(uint8_t frame[18], const uint8_t group[8])
{
static const uint8_t prefix[] = {0x6a, 0x02, 0xcb, 0x02, 0x06, 0x02, 0x11, 0x00};
memcpy(frame, prefix, sizeof(prefix));
memcpy(frame + 8, group, 8);
uint16_t crc = 0x6363;
for (unsigned i = 0; i < 16; ++i) {
uint8_t b = frame[i] ^ (uint8_t)crc;
b ^= (uint8_t)(b << 4);
crc = (crc >> 8) ^ ((uint16_t)b << 8) ^ ((uint16_t)b << 3) ^ (b >> 4);
}
frame[16] = (uint8_t)crc;
frame[17] = (uint8_t)(crc >> 8);
}
static bool broadcast_ecp(const uint8_t group[8])
{
/* 106 kbps Type A, full bytes, software CRC (no duplicate hardware CRC).
* InListPassiveTarget restores Type A activation/CRC settings afterward. */
static const uint8_t registers[] = {0x08,
0x63, 0x02, 0x00, /* CIU_TxMode */
0x63, 0x03, 0x00, /* CIU_RxMode */
0x63, 0x05, 0x40, /* CIU_TxAuto: 100% ASK */
0x63, 0x3c, 0x10, /* CIU_Control: initiator, including first scan */
0x63, 0x3d, 0x00}; /* CIU_BitFraming */
static const uint8_t rf_on[] = {0x32, 0x01, 0x03};
static const uint8_t short_timeout[] = {0x32, 0x02, 0x00, 0x0b, 0x08};
static const uint8_t normal_timeout[] = {0x32, 0x02, 0x00, 0x0b, 0x0a};
uint8_t response[128], cmd[19] = {0x42};
if (command(registers, sizeof(registers), response) != 2 ||
command(rf_on, sizeof(rf_on), response) != 2 ||
command(short_timeout, sizeof(short_timeout), response) != 2)
return false;
/* Allow RF to settle before transmitting the proprietary polling frame. */
vTaskDelay(pdMS_TO_TICKS(5));
build_ecp(cmd + 1, group);
int n = command_full(cmd, sizeof(cmd), response, sizeof(response), 250);
/* ECP is a broadcast: PN532 RF timeout 01 is expected, not a UART timeout. */
bool ok = n >= 3 && (response[2] == 0 || (n == 3 && response[2] == 1));
if (!ok)
ESP_LOGW(TAG, "ECP broadcast failed: length=%d", n);
bool restored = command(normal_timeout, sizeof(normal_timeout), response) == 2;
return ok && restored;
}
/* 1 = card, 0 = confirmed no target, -1 = transport/protocol failure. */
static int poll_card(nfc_card_t *card)
{
static const uint8_t poll[] = {0x4a, 0x01, 0x00};
uint8_t response[128];
int n = command(poll, sizeof(poll), response);
int result = -1;
if (n == 3 && response[2] == 0) {
result = 0;
} else if (n >= 8 && response[2] == 1 &&
nfc_uid_valid(response[7]) && n >= 8 + response[7]) {
card->target = response[3];
card->sak = response[6];
card->uid_len = response[7];
memcpy(card->uid, response + 8, card->uid_len);
result = 1;
} else if (n >= 0) {
ESP_LOGW(TAG, "invalid target response");
}
/* RF stays active through the callback's complete APDU session. */
return result;
}
void start_polling(pn532_card_callback_t callback)
{
pn532_uart_init();
vTaskDelay(pdMS_TO_TICKS(1000));
while (!initialize_reader()) {
ESP_LOGE(TAG, "PN532 initialization failed; retrying in 2 seconds");
vTaskDelay(pdMS_TO_TICKS(2000));
}
nfc_presence_t presence = {0};
for (;;) {
nfc_card_t card = {0};
uint8_t group[8];
bool ecp = relay_ecp_group(group);
if (!ecp) {
/* Do not wake Wallet with ordinary polls before Go supplies
* this connection's provisioned Home identifier. */
presence = (nfc_presence_t){0};
release_rf();
vTaskDelay(pdMS_TO_TICKS(POLL_INTERVAL_MS));
continue;
}
/* Announce before activation, including when a phone would otherwise
* answer with its default payment/transit applet. */
int result = !broadcast_ecp(group) ? -1 : poll_card(&card);
bool was_present = presence.present;
if (nfc_presence_update(&presence, result, &card)) {
ESP_LOGI(TAG, "card detected, UID length=%u", card.uid_len);
ESP_LOG_BUFFER_HEX_LEVEL(TAG, card.uid, card.uid_len, ESP_LOG_INFO);
if (callback(&card)) {
nfc_presence_accept(&presence, &card);
}
}
if (was_present && !presence.present) {
ESP_LOGI(TAG, "card removed");
}
release_rf();
vTaskDelay(pdMS_TO_TICKS(POLL_INTERVAL_MS));
}
}