diff --git a/.gitmodules b/.gitmodules index e5f52df..8cfcc27 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,7 +1,3 @@ -[submodule "device/components/esp-idf-rc522"] - path = device/components/esp-idf-rc522 - url = https://github.com/sweidac/esp-idf-rc522.git - [submodule "device/components/arduino-esp32"] path = device/components/arduino-esp32 url = https://github.com/espressif/arduino-esp32.git \ No newline at end of file diff --git a/device/components/esp-idf-rc522 b/device/components/esp-idf-rc522 deleted file mode 160000 index beb7e87..0000000 --- a/device/components/esp-idf-rc522 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit beb7e878775d2d1e161a9db79055c242c381da16 diff --git a/device/components/esp-idf-rc522/CMakeLists.txt b/device/components/esp-idf-rc522/CMakeLists.txt new file mode 100644 index 0000000..0198c1b --- /dev/null +++ b/device/components/esp-idf-rc522/CMakeLists.txt @@ -0,0 +1,4 @@ +idf_component_register( + SRCS "rc522.c" + INCLUDE_DIRS "." +) \ No newline at end of file diff --git a/device/components/esp-idf-rc522/LICENSE b/device/components/esp-idf-rc522/LICENSE new file mode 100644 index 0000000..8e9e883 --- /dev/null +++ b/device/components/esp-idf-rc522/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Alija Bobija + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/device/components/esp-idf-rc522/component.mk b/device/components/esp-idf-rc522/component.mk new file mode 100644 index 0000000..27ad11a --- /dev/null +++ b/device/components/esp-idf-rc522/component.mk @@ -0,0 +1 @@ +COMPONENT_ADD_INCLUDEDIRS := . diff --git a/device/components/esp-idf-rc522/rc522.c b/device/components/esp-idf-rc522/rc522.c new file mode 100644 index 0000000..3ad3351 --- /dev/null +++ b/device/components/esp-idf-rc522/rc522.c @@ -0,0 +1,746 @@ +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_system.h" +#include "driver/spi_master.h" +#include "soc/gpio_struct.h" +#include "driver/gpio.h" +#include "esp_timer.h" +#include "esp_log.h" + +#include "rc522.h" + +static const char* TAG = "ESP-RC522"; + +struct rc522 { + bool running; + rc522_config_t* config; + spi_device_handle_t spi; + TaskHandle_t task_handle; + bool scan_started; + uint8_t* lastSerialNumber; +}; + +typedef struct rc522* rc522_handle_t; + +static rc522_handle_t hndl = NULL; + +#define rc522_fw_version() rc522_read(0x37) + +bool rc522_is_inited() { + return hndl != NULL; +} + +static esp_err_t rc522_spi_init() { + if(! hndl || ! hndl->config) { + ESP_LOGE(TAG, "Fail to init SPI. Invalid handle"); + return ESP_ERR_INVALID_STATE; + } + + if(hndl->spi) { + ESP_LOGW(TAG, "SPI already initialized"); + return ESP_ERR_INVALID_STATE; + } + + spi_bus_config_t buscfg = { + .miso_io_num = hndl->config->miso_io, + .mosi_io_num = hndl->config->mosi_io, + .sclk_io_num = hndl->config->sck_io, + .quadwp_io_num = -1, + .quadhd_io_num = -1 + }; + + spi_device_interface_config_t devcfg = { + .clock_speed_hz = 5000000, + .mode = 0, + .spics_io_num = hndl->config->sda_io, + .queue_size = 7, + .flags = SPI_DEVICE_HALFDUPLEX + }; + + esp_err_t err = spi_bus_initialize(hndl->config->spi_host_id, &buscfg, 0); + + if(err != ESP_OK) { + return err; + } + + err = spi_bus_add_device(hndl->config->spi_host_id, &devcfg, &hndl->spi); + + if(err != ESP_OK) { + spi_bus_free(hndl->config->spi_host_id); + hndl->spi = NULL; + } + + return err; +} + +static esp_err_t rc522_write_n(uint8_t addr, uint8_t n, uint8_t *data) { + uint8_t* buffer = (uint8_t*) malloc(n + 1); + buffer[0] = (addr << 1) & 0x7E; + + for (uint8_t i = 1; i <= n; i++) { + buffer[i] = data[i-1]; + } + + spi_transaction_t t; + memset(&t, 0, sizeof(t)); + + t.length = 8 * (n + 1); + t.tx_buffer = buffer; + + esp_err_t ret = spi_device_transmit(hndl->spi, &t); + + free(buffer); + + return ret; +} + +static esp_err_t rc522_write(uint8_t addr, uint8_t val) { + return rc522_write_n(addr, 1, &val); +} + +static uint8_t* rc522_read_n(uint8_t addr, uint8_t n) { + if (n <= 0) { + return NULL; + } + + spi_transaction_t t; + memset(&t, 0, sizeof(t)); + + uint8_t* buffer = (uint8_t*) malloc(n); + + t.flags = SPI_TRANS_USE_TXDATA; + t.length = 8; + t.tx_data[0] = ((addr << 1) & 0x7E) | 0x80; + t.rxlength = 8 * n; + t.rx_buffer = buffer; + + esp_err_t ret = spi_device_transmit(hndl->spi, &t); + assert(ret == ESP_OK); + + return buffer; +} + +static uint8_t rc522_read(uint8_t addr) { + uint8_t* buffer = rc522_read_n(addr, 1); + uint8_t res = buffer[0]; + free(buffer); + + return res; +} + +static esp_err_t rc522_set_bitmask(uint8_t addr, uint8_t mask) { + return rc522_write(addr, rc522_read(addr) | mask); +} + +static esp_err_t rc522_clear_bitmask(uint8_t addr, uint8_t mask) { + return rc522_write(addr, rc522_read(addr) & ~mask); +} + +static esp_err_t rc522_antenna_on() { + esp_err_t ret; + + if(~ (rc522_read(0x14) & 0x03)) { + ret = rc522_set_bitmask(0x14, 0x03); + + if(ret != ESP_OK) { + return ret; + } + } + + return rc522_write(0x26, 0x60); // 43dB gain +} + +static void rc522_task(void* arg); + +esp_err_t rc522_init(rc522_config_t* config) { + if(! config) { + return ESP_ERR_INVALID_ARG; + } + + if(hndl) { + ESP_LOGW(TAG, "Already initialized"); + return ESP_ERR_INVALID_STATE; + } + + if(! (hndl = calloc(1, sizeof(struct rc522)))) { + return ESP_ERR_NO_MEM; + } + + if(! (hndl->config = calloc(1, sizeof(rc522_config_t)))) { + rc522_destroy(); + return ESP_ERR_NO_MEM; + } + + // copy config considering defaults + hndl->config->callback = config->callback; + hndl->config->ndef_callback = config->ndef_callback; + hndl->config->miso_io = config->miso_io == 0 ? RC522_DEFAULT_MISO : config->miso_io; + hndl->config->mosi_io = config->mosi_io == 0 ? RC522_DEFAULT_MOSI : config->mosi_io; + hndl->config->sck_io = config->sck_io == 0 ? RC522_DEFAULT_SCK : config->sck_io; + hndl->config->sda_io = config->sda_io == 0 ? RC522_DEFAULT_SDA : config->sda_io; + hndl->config->spi_host_id = config->spi_host_id == 0 ? RC522_DEFAULT_SPI_HOST : config->spi_host_id; + hndl->config->scan_interval_ms = config->scan_interval_ms < 50 ? RC522_DEFAULT_SCAN_INTERVAL_MS : config->scan_interval_ms; + hndl->config->task_stack_size = config->task_stack_size == 0 ? RC522_DEFAULT_TACK_STACK_SIZE : config->task_stack_size; + hndl->config->task_priority = config->task_priority == 0 ? RC522_DEFAULT_TACK_STACK_PRIORITY : config->task_priority; + + esp_err_t err = rc522_spi_init(); + + if(err != ESP_OK) { + rc522_destroy(); + return err; + } + + // ---------- RW test ------------ + const uint8_t test_addr = 0x24, test_val = 0x25; + for(uint8_t i = test_val; i < test_val + 2; i++) { + if((err = rc522_write(test_addr, i)) != ESP_OK || rc522_read(test_addr) != i) { + ESP_LOGE(TAG, "RW test fail"); + rc522_destroy(); + return err; + } + } + // ------- End of RW test -------- + + rc522_write(0x01, 0x0F); + rc522_write(0x2A, 0x8D); + rc522_write(0x2B, 0x3E); + rc522_write(0x2D, 0x1E); + rc522_write(0x2C, 0x00); + rc522_write(0x15, 0x40); + rc522_write(0x11, 0x3D); + + rc522_antenna_on(); + + hndl->running = true; + if (xTaskCreate(rc522_task, "rc522_task", hndl->config->task_stack_size, NULL, hndl->config->task_priority, &hndl->task_handle) != pdTRUE) { + ESP_LOGE(TAG, "Fail to create rc522 task"); + rc522_destroy(); + return err; + } + + if(err != ESP_OK) { + ESP_LOGE(TAG, "Fail to create timer"); + rc522_destroy(); + return err; + } + + ESP_LOGI(TAG, "Initialized (firmware: 0x%x)", rc522_fw_version()); + return ESP_OK; +} + +uint64_t rc522_sn_to_u64(uint8_t* sn) { + if(!sn) { + return 0; + } + + uint64_t result = 0; + for(int i = 4; i >= 0; i--) { + result |= ((uint64_t) sn[i] << (i * 8)); + } + + return result; +} + +/* Returns pointer to dynamically allocated array of two element */ +static uint8_t* rc522_calculate_crc(uint8_t *data, uint8_t n) { + rc522_clear_bitmask(0x05, 0x04); + rc522_set_bitmask(0x0A, 0x80); + + rc522_write_n(0x09, n, data); + + rc522_write(0x01, 0x03); + + uint8_t i = 255; + uint8_t nn = 0; + + for(;;) { + nn = rc522_read(0x05); + i--; + + if(! (i != 0 && ! (nn & 0x04))) { + break; + } + } + + uint8_t* res = (uint8_t*) malloc(2); + + res[0] = rc522_read(0x22); + res[1] = rc522_read(0x21); + + return res; +} + +static uint8_t* rc522_card_write(uint8_t cmd, uint8_t *data, uint8_t n, uint8_t* res_n) { + uint8_t *result = NULL; + uint8_t irq = 0x00; + uint8_t irq_wait = 0x00; + uint8_t last_bits = 0; + uint8_t nn = 0; + + if(cmd == 0x0E) { + irq = 0x12; + irq_wait = 0x10; + } + else if(cmd == 0x0C) { + irq = 0x77; + irq_wait = 0x30; + } + + rc522_write(0x02, irq | 0x80); + rc522_clear_bitmask(0x04, 0x80); + rc522_set_bitmask(0x0A, 0x80); + rc522_write(0x01, 0x00); + + rc522_write_n(0x09, n, data); + + rc522_write(0x01, cmd); + + if(cmd == 0x0C) { + rc522_set_bitmask(0x0D, 0x80); + } + + uint16_t i = 1000; + + for(;;) { + nn = rc522_read(0x04); + i--; + + if(! (i != 0 && (((nn & 0x01) == 0) && ((nn & irq_wait) == 0)))) { + break; + } + } + + rc522_clear_bitmask(0x0D, 0x80); + + if(i != 0) { + if((rc522_read(0x06) & 0x1B) == 0x00) { + if(cmd == 0x0C) { + nn = rc522_read(0x0A); + last_bits = rc522_read(0x0C) & 0x07; + + if (last_bits != 0) { + *res_n = (nn - 1) + last_bits; + } else { + *res_n = nn; + } + + result = (uint8_t*) malloc(*res_n); + + for(i = 0; i < *res_n; i++) { + result[i] = rc522_read(0x09); + } + } + } + } + + return result; +} + +static uint8_t* rc522_request(uint8_t* res_n) { + uint8_t* result = NULL; + rc522_write(0x0D, 0x07); + + uint8_t req_mode = 0x26; + result = rc522_card_write(0x0C, &req_mode, 1, res_n); + + if(*res_n * 8 != 0x10) { + free(result); + return NULL; + } + + return result; +} + +static uint8_t* rc522_anticoll() { + uint8_t res_n; + + rc522_write(0x0D, 0x00); + uint8_t* result = rc522_card_write(0x0C, (uint8_t[]) { 0x93, 0x20 }, 2, &res_n); + + if(result && res_n != 5) { // all cards/tags serial numbers is 5 bytes long (?) + free(result); + return NULL; + } + + return result; +} + +/* Send HALT and clear crypto bit. Card will only respond to WUPA after this. */ +static void rc522_halt_card() { + uint8_t buf[] = { 0x50, 0x00, 0x00, 0x00 }; + uint8_t* crc = rc522_calculate_crc(buf, 2); + buf[2] = crc[0]; + buf[3] = crc[1]; + free(crc); + + uint8_t res_n; + uint8_t* res = rc522_card_write(0x0C, buf, 4, &res_n); + free(res); + rc522_clear_bitmask(0x08, 0x08); +} + +static uint8_t* rc522_get_tag() { + uint8_t* result = NULL; + uint8_t* res_data = NULL; + uint8_t res_data_n; + + res_data = rc522_request(&res_data_n); + + if(res_data != NULL) { + free(res_data); + result = rc522_anticoll(); + /* NOTE: card is NOT halted here — caller must call rc522_halt_card() */ + } + + return result; +} + +/* URI prefix table for NDEF URI records (RFC 3986 shortened forms) */ +static const char* const RC522_URI_PREFIXES[] = { + "", /* 0x00 no prefix */ + "http://www.", /* 0x01 */ + "https://www.", /* 0x02 */ + "http://", /* 0x03 */ + "https://", /* 0x04 */ + "tel:", /* 0x05 */ + "mailto:", /* 0x06 */ + "ftp://anonymous:anonymous@", /* 0x07 */ + "ftp://ftp.", /* 0x08 */ + "ftps://", /* 0x09 */ + "sftp://", /* 0x0A */ + "smb://", /* 0x0B */ + "nfs://", /* 0x0C */ + "ftp://", /* 0x0D */ + "dav://", /* 0x0E */ + "news:", /* 0x0F */ + "telnet://", /* 0x10 */ + "imap:", /* 0x11 */ + "rtsp://", /* 0x12 */ + "urn:", /* 0x13 */ + "pop:", /* 0x14 */ + "sip:", /* 0x15 */ + "sips:", /* 0x16 */ + "tftp:", /* 0x17 */ + "btspp://", /* 0x18 */ + "btl2cap://", /* 0x19 */ + "btgoep://", /* 0x1A */ + "tcpobex://", /* 0x1B */ + "irdaobex://", /* 0x1C */ + "file://", /* 0x1D */ + "urn:epc:id:", /* 0x1E */ + "urn:epc:tag:", /* 0x1F */ + "urn:epc:pat:", /* 0x20 */ + "urn:epc:raw:", /* 0x21 */ + "urn:epc:", /* 0x22 */ + "urn:nfc:", /* 0x23 */ +}; +#define RC522_URI_PREFIX_COUNT (sizeof(RC522_URI_PREFIXES) / sizeof(RC522_URI_PREFIXES[0])) + +/* Parse an NDEF message TLV block and extract the first URI record URL. + * Returns heap-allocated null-terminated URL string, or NULL if not found. */ +static char* rc522_parse_ndef_url(const uint8_t* data, int len) { + int i = 0; + + while(i < len) { + uint8_t tlv_type = data[i++]; + + if(tlv_type == 0xFE) break; /* Terminator TLV */ + if(tlv_type == 0x00) continue; /* Null TLV — no length byte */ + + if(i >= len) break; + + /* Read TLV length (1 or 3 bytes) */ + uint16_t tlv_len; + if(data[i] == 0xFF) { + i++; + if(i + 1 >= len) break; + tlv_len = ((uint16_t)data[i] << 8) | data[i + 1]; + i += 2; + } else { + tlv_len = data[i++]; + } + + if(tlv_type != 0x03 || tlv_len == 0 || i + (int)tlv_len > len) { + i += tlv_len; + continue; + } + + /* Parse NDEF record header */ + uint8_t header = data[i++]; + uint8_t tnf = header & 0x07; + bool sr = (header & 0x10) != 0; /* Short Record */ + bool il = (header & 0x08) != 0; /* ID Length present */ + + if(i >= len) break; + uint8_t type_len = data[i++]; + + uint32_t payload_len; + if(sr) { + if(i >= len) break; + payload_len = data[i++]; + } else { + if(i + 3 >= len) break; + payload_len = ((uint32_t)data[i] << 24) | ((uint32_t)data[i+1] << 16) + | ((uint32_t)data[i+2] << 8) | data[i+3]; + i += 4; + } + + uint8_t id_len = 0; + if(il) { + if(i >= len) break; + id_len = data[i++]; + } + + /* Only handle Well-Known URI records (TNF=0x01, Type='U') */ + if(tnf == 0x01 && type_len == 1 && i < len && data[i] == 0x55) { + i++; /* skip type byte 'U' */ + i += id_len; /* skip optional ID */ + + if(payload_len > 0 && i < len) { + uint8_t prefix_code = data[i++]; + payload_len--; + + const char* prefix = (prefix_code < RC522_URI_PREFIX_COUNT) ? RC522_URI_PREFIXES[prefix_code] : ""; + size_t prefix_len = strlen(prefix); + size_t body_len = (payload_len < (uint32_t)(len - i)) ? payload_len : (uint32_t)(len - i); + + if(prefix_len + body_len > 255) body_len = 255 - prefix_len; + + char* url = (char*) malloc(prefix_len + body_len + 1); + if(!url) return NULL; + + memcpy(url, prefix, prefix_len); + memcpy(url + prefix_len, data + i, body_len); + url[prefix_len + body_len] = '\0'; + return url; + } + } + + /* Skip over this record */ + i += type_len + id_len + (int)payload_len; + } + + return NULL; +} + +/* Wake a halted card with WUPA (0x52), perform cascade anticollision (CL1 + optional CL2), + * SELECT the card, read pages 4–15 of user memory (NDEF area), parse, then halt. + * Returns heap-allocated URL string or NULL. Card is always left in HALT state on return. */ +static char* rc522_read_ndef_url() { + uint8_t res_n; + + /* WUPA: wake halted card — identical bit-framing to REQA but byte is 0x52 */ + rc522_write(0x0D, 0x07); + uint8_t wupa_cmd = 0x52; + uint8_t* atqa = rc522_card_write(0x0C, &wupa_cmd, 1, &res_n); + if(!atqa || res_n * 8 != 0x10) { + free(atqa); + return NULL; + } + free(atqa); + + /* CL1 anti-collision */ + rc522_write(0x0D, 0x00); + uint8_t* cl1 = rc522_card_write(0x0C, (uint8_t[]){ 0x93, 0x20 }, 2, &res_n); + if(!cl1 || res_n != 5) { + free(cl1); + rc522_halt_card(); + return NULL; + } + + bool cascade = (cl1[0] == 0x88); /* cascade tag byte indicates 7-byte UID */ + + /* CL1 SELECT */ + uint8_t sel1[9] = { 0x93, 0x70, cl1[0], cl1[1], cl1[2], cl1[3], cl1[4] }; + uint8_t* crc = rc522_calculate_crc(sel1, 7); + sel1[7] = crc[0]; sel1[8] = crc[1]; free(crc); + uint8_t sel_n; + uint8_t* sak1 = rc522_card_write(0x0C, sel1, 9, &sel_n); + free(cl1); + if(!sak1) { + rc522_halt_card(); + return NULL; + } + free(sak1); + + if(cascade) { + /* CL2 anti-collision */ + rc522_write(0x0D, 0x00); + uint8_t* cl2 = rc522_card_write(0x0C, (uint8_t[]){ 0x95, 0x20 }, 2, &res_n); + if(!cl2 || res_n != 5) { + free(cl2); + rc522_halt_card(); + return NULL; + } + + /* CL2 SELECT */ + uint8_t sel2[9] = { 0x95, 0x70, cl2[0], cl2[1], cl2[2], cl2[3], cl2[4] }; + crc = rc522_calculate_crc(sel2, 7); + sel2[7] = crc[0]; sel2[8] = crc[1]; free(crc); + uint8_t* sak2 = rc522_card_write(0x0C, sel2, 9, &sel_n); + free(cl2); + if(!sak2) { + rc522_halt_card(); + return NULL; + } + free(sak2); + } + + /* Card is now SELECTED. Verify NDEF capability container at page 3. + * For NTAG/MIFARE Ultralight the NDEF magic byte is 0xE1 at CC[0]. */ + uint8_t read_cc[4] = { 0x30, 0x03 }; + crc = rc522_calculate_crc(read_cc, 2); + read_cc[2] = crc[0]; read_cc[3] = crc[1]; free(crc); + + uint8_t* cc_data = rc522_card_write(0x0C, read_cc, 4, &res_n); + if(!cc_data || res_n < 4 || cc_data[0] != 0xE1) { + free(cc_data); + rc522_halt_card(); + return NULL; + } + free(cc_data); + + /* Read NDEF user pages 4–15 (12 pages × 4 bytes = 48 bytes). + * READ command (0x30) returns 16 bytes (4 pages) per call. */ + uint8_t ndef_buf[48]; + int ndef_len = 0; + + for(uint8_t page = 4; page <= 12; page += 4) { /* 3 reads × 16 bytes = 48 bytes */ + uint8_t read_cmd[4] = { 0x30, page }; + crc = rc522_calculate_crc(read_cmd, 2); + read_cmd[2] = crc[0]; read_cmd[3] = crc[1]; free(crc); + + uint8_t* page_data = rc522_card_write(0x0C, read_cmd, 4, &res_n); + if(!page_data || res_n < 16) { + free(page_data); + break; + } + + int copy = (48 - ndef_len < 16) ? 48 - ndef_len : 16; + memcpy(ndef_buf + ndef_len, page_data, copy); + ndef_len += copy; + free(page_data); + } + + rc522_halt_card(); + + if(ndef_len == 0) return NULL; + return rc522_parse_ndef_url(ndef_buf, ndef_len); +} + +esp_err_t rc522_start(rc522_start_args_t start_args) { + esp_err_t err = rc522_init(&start_args); + return err != ESP_OK ? err : rc522_start2(); +} + +esp_err_t rc522_start2() { + if(! hndl) { return ESP_ERR_INVALID_STATE; } + + hndl->scan_started = true; + + return ESP_OK; +} + +esp_err_t rc522_pause() { + if(! hndl) { return ESP_ERR_INVALID_STATE; } + + if(! hndl->scan_started) { + return ESP_OK; + } + + hndl->scan_started = false; + + return ESP_OK; +} + +void rc522_destroy() { + if(! hndl) { return; } + + rc522_pause(); // stop timer + hndl->running = false; // task will delete itself + + if(hndl->spi) { + spi_bus_remove_device(hndl->spi); + spi_bus_free(hndl->config->spi_host_id); + hndl->spi = NULL; + } + + free(hndl->config); + hndl->config = NULL; + + free(hndl); + hndl = NULL; +} + +static bool isSameSerialNumber(uint8_t* one, uint8_t* other) { + if (one == NULL && other == NULL) { + return true; + } + + if (one == NULL || other == NULL) { + return false; + } + + return one[0] == other[0] + && one[1] == other[1] + && one[2] == other[2] + && one[3] == other[3] + && one[4] == other[4]; +} + +static void rc522_task(void* arg) { + while(hndl->running) { + if(!hndl->scan_started) { + vTaskDelay(100 / portTICK_PERIOD_MS); + continue; + } + + uint8_t* serial_no = rc522_get_tag(); /* card NOT halted yet */ + + if(!isSameSerialNumber(serial_no, hndl->lastSerialNumber)) { + rc522_tag_callback_t cb = hndl->config->callback; + rc522_ndef_callback_t ndef_cb = hndl->config->ndef_callback; + + if(serial_no == NULL) { + free(hndl->lastSerialNumber); + hndl->lastSerialNumber = NULL; + + if(cb) cb(NULL); + if(ndef_cb) ndef_cb(NULL, NULL); + } else { + if(!hndl->lastSerialNumber) { + hndl->lastSerialNumber = malloc(5); + } + memcpy(hndl->lastSerialNumber, serial_no, 5); + + if(cb) cb(serial_no); + + if(ndef_cb) { + /* rc522_read_ndef_url() handles HALT internally after reading */ + char* url = rc522_read_ndef_url(); + ndef_cb(serial_no, url); + if(url) free(url); + } else { + rc522_halt_card(); + } + } + } else if(serial_no != NULL) { + /* Same card still present — halt it again to prevent continuous REQA responses */ + rc522_halt_card(); + } + + int delay_ms = hndl->config->scan_interval_ms; + if(serial_no != NULL) { + free(serial_no); + serial_no = NULL; + delay_ms *= 2; + } + + vTaskDelay(delay_ms / portTICK_PERIOD_MS); + } + + vTaskDelete(NULL); +} diff --git a/device/components/esp-idf-rc522/rc522.h b/device/components/esp-idf-rc522/rc522.h new file mode 100644 index 0000000..6df5d80 --- /dev/null +++ b/device/components/esp-idf-rc522/rc522.h @@ -0,0 +1,93 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include "driver/spi_master.h" + +#define RC522_DEFAULT_MISO (25) +#define RC522_DEFAULT_MOSI (23) +#define RC522_DEFAULT_SCK (19) +#define RC522_DEFAULT_SDA (22) +#define RC522_DEFAULT_SPI_HOST (VSPI_HOST) +#define RC522_DEFAULT_SCAN_INTERVAL_MS (125) +#define RC522_DEFAULT_TACK_STACK_SIZE (4 * 1024) +#define RC522_DEFAULT_TACK_STACK_PRIORITY (4) + +typedef void(*rc522_tag_callback_t)(uint8_t*); + +/* Called when a tag is detected (url != NULL) or removed (sn == NULL, url == NULL). + * url is the NDEF URI record content, or NULL if no NDEF was found on the tag. + * The caller must NOT free sn or url — they are freed by the library. */ +typedef void(*rc522_ndef_callback_t)(uint8_t* sn, const char* url); + +typedef struct { + int miso_io; /*