From 6c5010032b1adaa186d164a78451ae9d5f6b88ab Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sat, 26 Sep 2026 14:44:48 -0500 Subject: [PATCH] [pn532] Check InDataExchange status and fix tag handling (#19591) Co-authored-by: Claude Opus 5.5 (1M context) --- esphome/components/pn532/pn532.cpp | 104 ++++++++----- esphome/components/pn532/pn532.h | 60 ++++--- .../components/pn532/pn532_mifare_classic.cpp | 50 +++--- .../pn532/pn532_mifare_ultralight.cpp | 37 ++--- esphome/components/pn532_i2c/pn532_i2c.cpp | 13 +- esphome/components/pn532_spi/pn532_spi.cpp | 3 +- tests/components/pn532/pn532_test.cpp | 147 ++++++++++++++++++ 7 files changed, 301 insertions(+), 113 deletions(-) create mode 100644 tests/components/pn532/pn532_test.cpp diff --git a/esphome/components/pn532/pn532.cpp b/esphome/components/pn532/pn532.cpp index 8ef7721726..17ee11d7d9 100644 --- a/esphome/components/pn532/pn532.cpp +++ b/esphome/components/pn532/pn532.cpp @@ -1,6 +1,7 @@ #include "pn532.h" #include +#include "esphome/core/application.h" #include "esphome/core/log.h" #include "esphome/core/hal.h" @@ -25,7 +26,8 @@ void PN532::setup() { } std::vector version_data; - if (!this->read_response(PN532_COMMAND_VERSION_DATA, version_data)) { + // GetFirmwareVersion returns IC, Ver, Rev and Support + if (!this->read_response(PN532_COMMAND_VERSION_DATA, version_data) || version_data.size() < 3) { ESP_LOGE(TAG, "Error getting version"); this->mark_failed(); return; @@ -35,8 +37,8 @@ void PN532::setup() { if (!this->write_command_({ PN532_COMMAND_SAMCONFIGURATION, 0x01, // normal mode - 0x14, // zero timeout (not in virtual card mode) - 0x01, + 0x14, // timeout: 20 x 50 ms (only used in virtual card mode) + 0x01, // use IRQ })) { ESP_LOGE(TAG, "No wakeup ack"); this->mark_failed(); @@ -90,8 +92,8 @@ bool PN532::powerdown() { ESP_LOGE(TAG, "Error reading PN532 powerdown response"); return false; } - if (response[0] != 0x00) { - ESP_LOGE(TAG, "Error on PN532 powerdown: %02x", response[0]); + if (response.empty() || response[0] != 0x00) { + ESP_LOGE(TAG, "Powerdown error: %02x", response.empty() ? 0xFF : response[0]); return false; } ESP_LOGV(TAG, "Powerdown successful"); @@ -150,7 +152,7 @@ void PN532::loop() { return; } - uint8_t num_targets = read[0]; + uint8_t num_targets = read.empty() ? 0 : read[0]; if (num_targets != 1) { // no tags found or too many if (!this->current_uid_.empty()) { @@ -163,12 +165,20 @@ void PN532::loop() { return; } + // target data for 106 kbps type A: NbTg, Tg, SENS_RES (2 bytes), SEL_RES, NFCIDLength, NFCID1 (UM0701-02, 7.3.5) + if (read.size() < 6) { + this->turn_off_rf_(); + return; + } + const uint8_t sel_res = read[4]; uint8_t nfcid_length = read[5]; - if (nfcid_length > nfc::NFC_UID_MAX_LENGTH || read.size() < 6U + nfcid_length) { + if (nfcid_length == 0 || nfcid_length > nfc::NFC_UID_MAX_LENGTH || read.size() < 6U + nfcid_length) { // oops, pn532 returned invalid data + this->turn_off_rf_(); return; } nfc::NfcTagUid nfcid(read.begin() + 6, read.begin() + 6 + nfcid_length); + const uint8_t tag_type = tag_type_from_sel_res(sel_res); bool report = true; for (auto *bin_sens : this->binary_sensors_) { @@ -188,7 +198,7 @@ void PN532::loop() { this->current_uid_ = nfcid; if (next_task_ == READ) { - auto tag = this->read_tag_(nfcid); + auto tag = this->read_tag_(nfcid, tag_type); for (auto *trigger : this->triggers_ontag_) trigger->process(tag); @@ -206,13 +216,13 @@ void PN532::loop() { } } else if (next_task_ == CLEAN) { ESP_LOGD(TAG, " Tag cleaning"); - if (!this->clean_tag_(nfcid)) { + if (!this->clean_tag_(nfcid, tag_type)) { ESP_LOGE(TAG, " Tag was not fully cleaned successfully"); } ESP_LOGD(TAG, " Tag cleaned!"); } else if (next_task_ == FORMAT) { ESP_LOGD(TAG, " Tag formatting"); - if (!this->format_tag_(nfcid)) { + if (!this->format_tag_(nfcid, tag_type)) { ESP_LOGE(TAG, "Error formatting tag as NDEF"); } ESP_LOGD(TAG, " Tag formatted!"); @@ -220,16 +230,15 @@ void PN532::loop() { if (this->next_task_message_to_write_ != nullptr) { ESP_LOGD(TAG, " Tag writing"); ESP_LOGD(TAG, " Tag formatting"); - if (!this->format_tag_(nfcid)) { + if (!this->format_tag_(nfcid, tag_type)) { ESP_LOGE(TAG, " Tag could not be formatted for writing"); } else { ESP_LOGD(TAG, " Writing NDEF data"); - if (!this->write_tag_(nfcid, this->next_task_message_to_write_)) { + if (!this->write_tag_(nfcid, tag_type, this->next_task_message_to_write_.get())) { ESP_LOGE(TAG, " Failed to write message to tag"); } ESP_LOGD(TAG, " Finished writing NDEF data"); - delete this->next_task_message_to_write_; - this->next_task_message_to_write_ = nullptr; + this->next_task_message_to_write_.reset(); this->on_finished_write_callback_.call(); } } @@ -307,16 +316,17 @@ void PN532::send_nack_() { enum PN532ReadReady PN532::read_ready_(bool block) { if (this->rd_ready_ == READY) { if (block) { - this->rd_start_time_.reset(); + this->rd_started_ = false; this->rd_ready_ = WOULDBLOCK; } return READY; } - if (!this->rd_start_time_.has_value()) { + if (!this->rd_started_) { this->rd_start_time_ = millis(); + this->rd_started_ = true; } - const uint32_t rd_start_time = *this->rd_start_time_; + const uint32_t rd_start_time = this->rd_start_time_; while (true) { if (this->is_read_ready()) { @@ -340,7 +350,7 @@ enum PN532ReadReady PN532::read_ready_(bool block) { auto rdy = this->rd_ready_; if (block || rdy == TIMEOUT) { - this->rd_start_time_.reset(); + this->rd_started_ = false; this->rd_ready_ = WOULDBLOCK; } return rdy; @@ -355,21 +365,16 @@ void PN532::turn_off_rf_() { }); } -std::unique_ptr PN532::read_tag_(nfc::NfcTagUid &uid) { - uint8_t type = nfc::guess_tag_type(uid.size()); - - if (type == nfc::TAG_TYPE_MIFARE_CLASSIC) { +std::unique_ptr PN532::read_tag_(nfc::NfcTagUid &uid, const uint8_t tag_type) { + if (tag_type == nfc::TAG_TYPE_MIFARE_CLASSIC) { ESP_LOGD(TAG, "Mifare classic"); return this->read_mifare_classic_tag_(uid); - } else if (type == nfc::TAG_TYPE_2) { + } else if (tag_type == nfc::TAG_TYPE_2) { ESP_LOGD(TAG, "Mifare ultralight"); return this->read_mifare_ultralight_tag_(uid); - } else if (type == nfc::TAG_TYPE_UNKNOWN) { - ESP_LOGV(TAG, "Cannot determine tag type"); - return make_unique(uid); - } else { - return make_unique(uid); } + ESP_LOGV(TAG, "Reading tag type %u is not supported", tag_type); + return make_unique(uid); } void PN532::read_mode() { @@ -386,43 +391,58 @@ void PN532::format_mode() { } void PN532::write_mode(nfc::NdefMessage *message) { this->next_task_ = WRITE; - this->next_task_message_to_write_ = message; + this->next_task_message_to_write_.reset(message); ESP_LOGD(TAG, "Waiting to write next tag"); } -bool PN532::clean_tag_(nfc::NfcTagUid &uid) { - uint8_t type = nfc::guess_tag_type(uid.size()); - if (type == nfc::TAG_TYPE_MIFARE_CLASSIC) { +bool PN532::clean_tag_(nfc::NfcTagUid &uid, const uint8_t tag_type) { + if (tag_type == nfc::TAG_TYPE_MIFARE_CLASSIC) { return this->format_mifare_classic_mifare_(uid); - } else if (type == nfc::TAG_TYPE_2) { + } else if (tag_type == nfc::TAG_TYPE_2) { return this->clean_mifare_ultralight_(); } ESP_LOGE(TAG, "Unsupported Tag for formatting"); return false; } -bool PN532::format_tag_(nfc::NfcTagUid &uid) { - uint8_t type = nfc::guess_tag_type(uid.size()); - if (type == nfc::TAG_TYPE_MIFARE_CLASSIC) { +bool PN532::format_tag_(nfc::NfcTagUid &uid, const uint8_t tag_type) { + if (tag_type == nfc::TAG_TYPE_MIFARE_CLASSIC) { return this->format_mifare_classic_ndef_(uid); - } else if (type == nfc::TAG_TYPE_2) { + } else if (tag_type == nfc::TAG_TYPE_2) { return this->clean_mifare_ultralight_(); } ESP_LOGE(TAG, "Unsupported Tag for formatting"); return false; } -bool PN532::write_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message) { - uint8_t type = nfc::guess_tag_type(uid.size()); - if (type == nfc::TAG_TYPE_MIFARE_CLASSIC) { +bool PN532::write_tag_(nfc::NfcTagUid &uid, const uint8_t tag_type, nfc::NdefMessage *message) { + if (tag_type == nfc::TAG_TYPE_MIFARE_CLASSIC) { return this->write_mifare_classic_tag_(uid, message); - } else if (type == nfc::TAG_TYPE_2) { + } else if (tag_type == nfc::TAG_TYPE_2) { return this->write_mifare_ultralight_tag_(uid, message); } - ESP_LOGE(TAG, "Unsupported Tag for formatting"); + ESP_LOGE(TAG, "Unsupported Tag for writing"); return false; } +bool PN532::in_data_exchange_(const std::vector &command, std::vector &response) { + // formatting a tag takes seconds of back-to-back exchanges inside loop(), longer than the task watchdog allows + App.feed_wdt(); + if (!this->write_command_(command)) { + return false; + } + // output: Status, DataIn; a status of 0x00 means the exchange with the target succeeded (UM0701-02, 7.3.8) + if (!this->read_response(PN532_COMMAND_INDATAEXCHANGE, response) || response.empty()) { + return false; + } + if (response[0] != 0x00) { + ESP_LOGV(TAG, "InDataExchange failed, status 0x%02X", response[0]); + return false; + } + response.erase(response.begin()); + return true; +} + void PN532::dump_config() { ESP_LOGCONFIG(TAG, "PN532:"); switch (this->error_code_) { diff --git a/esphome/components/pn532/pn532.h b/esphome/components/pn532/pn532.h index 629a697aa5..c518afc491 100644 --- a/esphome/components/pn532/pn532.h +++ b/esphome/components/pn532/pn532.h @@ -19,12 +19,28 @@ static const uint8_t PN532_COMMAND_INDATAEXCHANGE = 0x40; static const uint8_t PN532_COMMAND_INLISTPASSIVETARGET = 0x4A; static const uint8_t PN532_COMMAND_POWERDOWN = 0x16; -enum PN532ReadReady { +enum PN532ReadReady : uint8_t { WOULDBLOCK = 0, TIMEOUT, READY, }; +// SEL_RES (SAK) bits, as reported by InListPassiveTarget for ISO/IEC 14443 type A targets (NXP AN10833) +static constexpr uint8_t SEL_RES_MIFARE_CLASSIC = 0x08; +static constexpr uint8_t SEL_RES_ISO_DEP = 0x20; +static constexpr uint8_t SEL_RES_TNP3XXX = 0x01; // MIFARE Classic 1K compatible + +/// Tag type (nfc::TAG_TYPE_*) from a type A target's SEL_RES byte +inline uint8_t tag_type_from_sel_res(uint8_t sel_res) { + if ((sel_res & SEL_RES_MIFARE_CLASSIC) || sel_res == SEL_RES_TNP3XXX) + return nfc::TAG_TYPE_MIFARE_CLASSIC; + if (sel_res & SEL_RES_ISO_DEP) + return nfc::TAG_TYPE_4; + if (sel_res == 0x00) + return nfc::TAG_TYPE_2; + return nfc::TAG_TYPE_UNKNOWN; +} + class PN532BinarySensor; class PN532 : public PollingComponent { @@ -67,11 +83,14 @@ class PN532 : public PollingComponent { virtual bool read_data(std::vector &data, uint8_t len) = 0; virtual bool read_response(uint8_t command, std::vector &data) = 0; - std::unique_ptr read_tag_(nfc::NfcTagUid &uid); + std::unique_ptr read_tag_(nfc::NfcTagUid &uid, uint8_t tag_type); - bool format_tag_(nfc::NfcTagUid &uid); - bool clean_tag_(nfc::NfcTagUid &uid); - bool write_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message); + bool format_tag_(nfc::NfcTagUid &uid, uint8_t tag_type); + bool clean_tag_(nfc::NfcTagUid &uid, uint8_t tag_type); + bool write_tag_(nfc::NfcTagUid &uid, uint8_t tag_type, nfc::NdefMessage *message); + /// Sends an InDataExchange command and reads the response; returns false unless the status byte reports success. + /// On success, `response` holds the data returned by the target, without the status byte. + bool in_data_exchange_(const std::vector &command, std::vector &response); std::unique_ptr read_mifare_classic_tag_(nfc::NfcTagUid &uid); bool read_mifare_classic_block_(uint8_t block_num, std::vector &data); @@ -91,27 +110,32 @@ class PN532 : public PollingComponent { bool write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message); bool clean_mifare_ultralight_(); - bool updates_enabled_{true}; - bool requested_read_{false}; - std::vector binary_sensors_; - std::vector triggers_ontag_; - std::vector triggers_ontagremoved_; - nfc::NfcTagUid current_uid_; - nfc::NdefMessage *next_task_message_to_write_; - optional rd_start_time_{}; - enum PN532ReadReady rd_ready_ { WOULDBLOCK }; - enum NfcTask { + enum NfcTask : uint8_t { READ = 0, CLEAN, FORMAT, WRITE, - } next_task_{READ}; - enum PN532Error { + }; + enum PN532Error : uint8_t { NONE = 0, WAKEUP_FAILED, SAM_COMMAND_FAILED, - } error_code_{NONE}; + }; + + // members are ordered by alignment, widest first, to minimize padding CallbackManager on_finished_write_callback_; + std::vector binary_sensors_; + std::vector triggers_ontag_; + std::vector triggers_ontagremoved_; + std::unique_ptr next_task_message_to_write_; + nfc::NfcTagUid current_uid_; + uint32_t rd_start_time_{0}; // valid only while rd_started_ is set + PN532ReadReady rd_ready_{WOULDBLOCK}; + NfcTask next_task_{READ}; + PN532Error error_code_{NONE}; + bool rd_started_{false}; + bool updates_enabled_{true}; + bool requested_read_{false}; }; class PN532BinarySensor final : public binary_sensor::BinarySensor { diff --git a/esphome/components/pn532/pn532_mifare_classic.cpp b/esphome/components/pn532/pn532_mifare_classic.cpp index 37674080d8..166f4ab776 100644 --- a/esphome/components/pn532/pn532_mifare_classic.cpp +++ b/esphome/components/pn532/pn532_mifare_classic.cpp @@ -36,14 +36,15 @@ std::unique_ptr PN532::read_mifare_classic_tag_(nfc::NfcTagUid &uid if (nfc::mifare_classic_is_first_block(current_block)) { if (!this->auth_mifare_classic_block_(uid, current_block, nfc::MIFARE_CMD_AUTH_A, nfc::NDEF_KEY)) { ESP_LOGE(TAG, "Error, Block authentication failed for %d", current_block); + return make_unique(uid, nfc::MIFARE_CLASSIC); } } std::vector block_data; - if (this->read_mifare_classic_block_(current_block, block_data)) { - buffer.insert(buffer.end(), block_data.begin(), block_data.end()); - } else { + if (!this->read_mifare_classic_block_(current_block, block_data)) { ESP_LOGE(TAG, "Error reading block %d", current_block); + return make_unique(uid, nfc::MIFARE_CLASSIC); } + buffer.insert(buffer.end(), block_data.begin(), block_data.end()); index += nfc::MIFARE_CLASSIC_BLOCK_SIZE; current_block++; @@ -63,20 +64,18 @@ std::unique_ptr PN532::read_mifare_classic_tag_(nfc::NfcTagUid &uid } bool PN532::read_mifare_classic_block_(uint8_t block_num, std::vector &data) { - if (!this->write_command_({ - PN532_COMMAND_INDATAEXCHANGE, - 0x01, // One card - nfc::MIFARE_CMD_READ, - block_num, - })) { + if (!this->in_data_exchange_( + { + PN532_COMMAND_INDATAEXCHANGE, + 0x01, // One card + nfc::MIFARE_CMD_READ, + block_num, + }, + data) || + data.size() != nfc::MIFARE_CLASSIC_BLOCK_SIZE) { return false; } - if (!this->read_response(PN532_COMMAND_INDATAEXCHANGE, data) || data[0] != 0x00) { - return false; - } - data.erase(data.begin()); - char data_buf[nfc::FORMAT_BYTES_BUFFER_SIZE]; ESP_LOGVV(TAG, " Block %d: %s", block_num, nfc::format_bytes_to(data_buf, data)); return true; @@ -90,14 +89,14 @@ bool PN532::auth_mifare_classic_block_(nfc::NfcTagUid &uid, uint8_t block_num, u block_num, // Block number }); data.insert(data.end(), key, key + 6); - data.insert(data.end(), uid.begin(), uid.end()); - if (!this->write_command_(data)) { - ESP_LOGE(TAG, "Authentication failed - Block %d", block_num); + // the command takes exactly 4 UID bytes (UM0701-02, 7.3.8); for 7-byte UIDs these are the last 4, as in libnfc + if (uid.size() < 4) { return false; } + data.insert(data.end(), uid.end() - 4, uid.end()); std::vector response; - if (!this->read_response(PN532_COMMAND_INDATAEXCHANGE, response) || response[0] != 0x00) { + if (!this->in_data_exchange_(data, response)) { ESP_LOGE(TAG, "Authentication failed - Block 0x%02x", block_num); return false; } @@ -167,6 +166,8 @@ bool PN532::format_mifare_classic_ndef_(nfc::NfcTagUid &uid) { ESP_LOGD(TAG, "Sector 0 formatted to NDEF"); + bool error = false; + for (int block = 4; block < 64; block += 4) { if (!this->auth_mifare_classic_block_(uid, block + 3, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY)) { return false; @@ -174,23 +175,28 @@ bool PN532::format_mifare_classic_ndef_(nfc::NfcTagUid &uid) { if (block == 4) { if (!this->write_mifare_classic_block_(block, EMPTY_NDEF_MESSAGE.data(), EMPTY_NDEF_MESSAGE.size())) { ESP_LOGE(TAG, "Unable to write block %d", block); + error = true; } } else { if (!this->write_mifare_classic_block_(block, BLANK_BLOCK.data(), BLANK_BLOCK.size())) { ESP_LOGE(TAG, "Unable to write block %d", block); + error = true; } } if (!this->write_mifare_classic_block_(block + 1, BLANK_BLOCK.data(), BLANK_BLOCK.size())) { ESP_LOGE(TAG, "Unable to write block %d", block + 1); + error = true; } if (!this->write_mifare_classic_block_(block + 2, BLANK_BLOCK.data(), BLANK_BLOCK.size())) { ESP_LOGE(TAG, "Unable to write block %d", block + 2); + error = true; } if (!this->write_mifare_classic_block_(block + 3, NDEF_TRAILER.data(), NDEF_TRAILER.size())) { ESP_LOGE(TAG, "Unable to write trailer block %d", block + 3); + error = true; } } - return true; + return !error; } bool PN532::write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, size_t len) { @@ -201,13 +207,9 @@ bool PN532::write_mifare_classic_block_(uint8_t block_num, const uint8_t *data, block_num, }); cmd.insert(cmd.end(), data, data + len); - if (!this->write_command_(cmd)) { - ESP_LOGE(TAG, "Error writing block %d", block_num); - return false; - } std::vector response; - if (!this->read_response(PN532_COMMAND_INDATAEXCHANGE, response)) { + if (!this->in_data_exchange_(cmd, response)) { ESP_LOGE(TAG, "Error writing block %d", block_num); return false; } diff --git a/esphome/components/pn532/pn532_mifare_ultralight.cpp b/esphome/components/pn532/pn532_mifare_ultralight.cpp index eb3d13a7e0..d918c9d52d 100644 --- a/esphome/components/pn532/pn532_mifare_ultralight.cpp +++ b/esphome/components/pn532/pn532_mifare_ultralight.cpp @@ -1,3 +1,4 @@ +#include #include #include @@ -51,24 +52,20 @@ bool PN532::read_mifare_ultralight_bytes_(uint8_t start_page, uint16_t num_bytes std::vector response; for (uint8_t i = 0; i * read_increment < num_bytes; i++) { - if (!this->write_command_({ - PN532_COMMAND_INDATAEXCHANGE, - 0x01, // One card - nfc::MIFARE_CMD_READ, - uint8_t(i * nfc::MIFARE_ULTRALIGHT_READ_SIZE + start_page), - })) { + // a READ returns 4 pages (16 bytes) + if (!this->in_data_exchange_( + { + PN532_COMMAND_INDATAEXCHANGE, + 0x01, // One card + nfc::MIFARE_CMD_READ, + uint8_t(i * nfc::MIFARE_ULTRALIGHT_READ_SIZE + start_page), + }, + response) || + response.size() != read_increment) { return false; } - - if (!this->read_response(PN532_COMMAND_INDATAEXCHANGE, response) || response[0] != 0x00) { - return false; - } - uint16_t bytes_offset = (i + 1) * read_increment; - auto pages_in_end_itr = bytes_offset <= num_bytes ? response.end() : response.end() - (bytes_offset - num_bytes); - - if ((pages_in_end_itr > response.begin()) && (pages_in_end_itr <= response.end())) { - data.insert(data.end(), response.begin() + 1, pages_in_end_itr); - } + const uint16_t remaining = num_bytes - i * read_increment; + data.insert(data.end(), response.begin(), response.begin() + std::min(read_increment, remaining)); } char data_buf[nfc::FORMAT_BYTES_BUFFER_SIZE]; @@ -87,7 +84,7 @@ bool PN532::is_mifare_ultralight_formatted_(const std::vector &page_3_t uint16_t PN532::read_mifare_ultralight_capacity_() { std::vector data; - if (this->read_mifare_ultralight_bytes_(3, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE, data)) { + if (this->read_mifare_ultralight_bytes_(3, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE, data) && data.size() > 2) { ESP_LOGV(TAG, "Tag capacity is %u bytes", data[2] * 8U); return data[2] * 8U; } @@ -174,13 +171,9 @@ bool PN532::write_mifare_ultralight_page_(uint8_t page_num, const uint8_t *write page_num, }); cmd.insert(cmd.end(), write_data, write_data + len); - if (!this->write_command_(cmd)) { - ESP_LOGE(TAG, "Error writing page %u", page_num); - return false; - } std::vector response; - if (!this->read_response(PN532_COMMAND_INDATAEXCHANGE, response)) { + if (!this->in_data_exchange_(cmd, response)) { ESP_LOGE(TAG, "Error writing page %u", page_num); return false; } diff --git a/esphome/components/pn532_i2c/pn532_i2c.cpp b/esphome/components/pn532_i2c/pn532_i2c.cpp index 7f4d78461b..4160076a7d 100644 --- a/esphome/components/pn532_i2c/pn532_i2c.cpp +++ b/esphome/components/pn532_i2c/pn532_i2c.cpp @@ -12,11 +12,12 @@ namespace esphome::pn532_i2c { static const char *const TAG = "pn532_i2c"; bool PN532I2C::is_read_ready() { - uint8_t ready; - if (!this->read_bytes_raw(&ready, 1)) { + uint8_t status; + if (!this->read_bytes_raw(&status, 1)) { return false; } - return ready == 0x01; + // only bit 0 (RDY) of the status byte is defined (UM0701-02, 6.2.4) + return status & 0x01; } bool PN532I2C::write_data(const std::vector &data) { @@ -30,9 +31,9 @@ bool PN532I2C::read_data(std::vector &data, uint8_t len) { return false; } + // the PN532 prefixes every frame with a status byte data.resize(len + 1); - this->read_bytes_raw(data.data(), len + 1); - return true; + return this->read_bytes_raw(data.data(), len + 1); } bool PN532I2C::read_response(uint8_t command, std::vector &data) { @@ -73,7 +74,7 @@ bool PN532I2C::read_response(uint8_t command, std::vector &data) { checksum = ~checksum + 1; if (data[len + 1] != checksum) { - ESP_LOGV(TAG, "read data invalid checksum! %02X != %02X", data[len], checksum); + ESP_LOGV(TAG, "read data invalid checksum! %02X != %02X", data[len + 1], checksum); return false; } diff --git a/esphome/components/pn532_spi/pn532_spi.cpp b/esphome/components/pn532_spi/pn532_spi.cpp index 13d9aebc20..112adc4ecb 100644 --- a/esphome/components/pn532_spi/pn532_spi.cpp +++ b/esphome/components/pn532_spi/pn532_spi.cpp @@ -25,7 +25,8 @@ void PN532Spi::setup() { bool PN532Spi::is_read_ready() { this->enable(); this->write_byte(0x02); - bool ready = this->read_byte() == 0x01; + // only bit 0 (RDY) of the status byte is defined (UM0701-02, 6.2.5) + const bool ready = this->read_byte() & 0x01; this->disable(); return ready; } diff --git a/tests/components/pn532/pn532_test.cpp b/tests/components/pn532/pn532_test.cpp new file mode 100644 index 0000000000..38c369acbc --- /dev/null +++ b/tests/components/pn532/pn532_test.cpp @@ -0,0 +1,147 @@ +#include + +#include + +#include "esphome/components/pn532/pn532.h" + +namespace esphome::pn532 { + +namespace { + +// Stands in for the bus: acknowledges every command and answers with queued response payloads. +class FakePN532 : public PN532 { + public: + using PN532::auth_mifare_classic_block_; + using PN532::read_mifare_ultralight_bytes_; + using PN532::read_mifare_classic_block_; + using PN532::write_mifare_classic_block_; + using PN532::write_mifare_ultralight_page_; + + std::deque> responses; + std::vector> written; + + protected: + bool is_read_ready() override { return true; } + bool write_data(const std::vector &data) override { + this->written.push_back(data); + return true; + } + // only used for ACK frames; index 0 is the I2C status byte + bool read_data(std::vector &data, uint8_t len) override { + data = {0x01, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00}; + return true; + } + bool read_response(uint8_t command, std::vector &data) override { + if (this->responses.empty()) + return false; + data = this->responses.front(); + this->responses.pop_front(); + return true; + } +}; + +// Extracts the command bytes (after TFI) from a normal information frame +std::vector frame_data(const std::vector &frame) { + // preamble, start code (2), LEN, LCS, TFI, data..., DCS, postamble + return std::vector(frame.begin() + 6, frame.end() - 2); +} + +} // namespace + +TEST(PN532TagType, FromSelRes) { + EXPECT_EQ(tag_type_from_sel_res(0x08), nfc::TAG_TYPE_MIFARE_CLASSIC); // Classic 1K + EXPECT_EQ(tag_type_from_sel_res(0x18), nfc::TAG_TYPE_MIFARE_CLASSIC); // Classic 4K + EXPECT_EQ(tag_type_from_sel_res(0x09), nfc::TAG_TYPE_MIFARE_CLASSIC); // Mini + EXPECT_EQ(tag_type_from_sel_res(0x01), nfc::TAG_TYPE_MIFARE_CLASSIC); // TNP3xxx + EXPECT_EQ(tag_type_from_sel_res(0x00), nfc::TAG_TYPE_2); // Ultralight / NTAG + EXPECT_EQ(tag_type_from_sel_res(0x20), nfc::TAG_TYPE_4); // ISO-DEP (phones, DESFire) + EXPECT_EQ(tag_type_from_sel_res(0x40), nfc::TAG_TYPE_UNKNOWN); +} + +// A failed write (status byte other than 0x00) must be reported as a failure. +TEST(PN532Mifare, ClassicWriteChecksStatus) { + FakePN532 pn532; + const uint8_t block[16] = {}; + pn532.responses.push_back({0x14}); // authentication error + EXPECT_FALSE(pn532.write_mifare_classic_block_(4, block, sizeof(block))); + pn532.responses.push_back({0x00}); + EXPECT_TRUE(pn532.write_mifare_classic_block_(4, block, sizeof(block))); +} + +TEST(PN532Mifare, UltralightWriteChecksStatus) { + FakePN532 pn532; + const uint8_t page[4] = {}; + pn532.responses.push_back({0x01}); // timeout + EXPECT_FALSE(pn532.write_mifare_ultralight_page_(4, page, sizeof(page))); + pn532.responses.push_back({0x00}); + EXPECT_TRUE(pn532.write_mifare_ultralight_page_(4, page, sizeof(page))); +} + +TEST(PN532Mifare, ClassicReadRejectsBadResponses) { + FakePN532 pn532; + std::vector data; + pn532.responses.emplace_back(); // empty response + EXPECT_FALSE(pn532.read_mifare_classic_block_(4, data)); + data.clear(); + pn532.responses.push_back({0x00, 0x01, 0x02}); // short block + EXPECT_FALSE(pn532.read_mifare_classic_block_(4, data)); + + std::vector good(17, 0xAB); + good[0] = 0x00; + pn532.responses.push_back(good); + data.clear(); + EXPECT_TRUE(pn532.read_mifare_classic_block_(4, data)); + EXPECT_EQ(data, std::vector(16, 0xAB)); +} + +// Authentication carries exactly 4 UID bytes: the last 4 of a 7-byte UID. +TEST(PN532Mifare, AuthSendsFourUidBytes) { + FakePN532 pn532; + nfc::NfcTagUid uid = {0x04, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66}; + pn532.responses.push_back({0x00}); + EXPECT_TRUE(pn532.auth_mifare_classic_block_(uid, 4, nfc::MIFARE_CMD_AUTH_A, nfc::NDEF_KEY)); + ASSERT_EQ(pn532.written.size(), 1u); + const auto cmd = frame_data(pn532.written[0]); + // InDataExchange, Tg, Cmd, Addr, key (6), UID (4) + ASSERT_EQ(cmd.size(), 14u); + EXPECT_EQ(std::vector(cmd.end() - 4, cmd.end()), (std::vector{0x33, 0x44, 0x55, 0x66})); +} + +// Reads in 16-byte chunks, keeps only the bytes asked for, and advances 4 pages per READ. +TEST(PN532Mifare, UltralightReadTrimsLastChunk) { + FakePN532 pn532; + std::vector first(17), second(17); + first[0] = second[0] = 0x00; // status + for (uint8_t i = 0; i < 16; i++) { + first[i + 1] = i; + second[i + 1] = 0x10 + i; + } + pn532.responses.push_back(first); + pn532.responses.push_back(second); + + std::vector data; + ASSERT_TRUE(pn532.read_mifare_ultralight_bytes_(4, 20, data)); + ASSERT_EQ(data.size(), 20u); + EXPECT_EQ(data[15], 15); + EXPECT_EQ(data[16], 0x10); + EXPECT_EQ(data[19], 0x13); + + ASSERT_EQ(pn532.written.size(), 2u); + EXPECT_EQ(frame_data(pn532.written[0]).back(), 4); // READ page 4 + EXPECT_EQ(frame_data(pn532.written[1]).back(), 8); // then page 8 +} + +TEST(PN532Mifare, UltralightReadRejectsBadResponses) { + FakePN532 pn532; + std::vector data; + pn532.responses.push_back({0x00, 0x01, 0x02}); // short response + EXPECT_FALSE(pn532.read_mifare_ultralight_bytes_(4, 16, data)); + + std::vector failed(17, 0x00); + failed[0] = 0x01; // timeout status + pn532.responses.push_back(failed); + data.clear(); + EXPECT_FALSE(pn532.read_mifare_ultralight_bytes_(4, 16, data)); +} + +} // namespace esphome::pn532