#include #include #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_command_; 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(std::span data) override { this->written.emplace_back(data.begin(), data.end()); return true; } // only used for ACK frames; index 0 is the I2C status byte bool read_data(PN532Frame &data, size_t len) override { data = {0x01, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00}; return true; } bool read_response(uint8_t command, PN532Frame &data) override { if (this->responses.empty()) return false; data.assign(this->responses.front().begin(), this->responses.front().end()); this->responses.pop_front(); return true; } }; std::vector bytes_of(std::span bytes) { return {bytes.begin(), bytes.end()}; } // 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); } // The frame wraps the command in preamble, start code, LEN, LCS, TFI, DCS and postamble (UM0701-02, 6.2.1.1). TEST(PN532Frame, WrapsCommand) { FakePN532 pn532; ASSERT_TRUE(pn532.write_command_({0x4A, 0x01, 0x00})); ASSERT_EQ(pn532.written.size(), 1u); EXPECT_EQ(pn532.written[0], (std::vector{0x00, 0x00, 0xFF, 0x04, 0xFC, 0xD4, 0x4A, 0x01, 0x00, 0xE1, 0x00})); } // A command that cannot fit a normal information frame is refused rather than truncated. TEST(PN532Frame, RejectsOversizedCommand) { FakePN532 pn532; std::array too_long{}; EXPECT_FALSE(pn532.write_command_(too_long)); EXPECT_TRUE(pn532.written.empty()); EXPECT_TRUE(pn532.write_command_(std::span(too_long).first(PN532_FRAME_MAX_DATA_SIZE))); ASSERT_EQ(pn532.written.size(), 1u); EXPECT_EQ(pn532.written[0].size(), PN532_FRAME_MAX_DATA_SIZE + 8); } // 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)); pn532.responses.push_back({0x00}); EXPECT_TRUE(pn532.write_mifare_classic_block_(4, 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)); pn532.responses.push_back({0x00}); EXPECT_TRUE(pn532.write_mifare_ultralight_page_(4, page)); } TEST(PN532Mifare, ClassicReadRejectsBadResponses) { FakePN532 pn532; MifareReadData data{}; pn532.responses.emplace_back(); // empty response EXPECT_FALSE(pn532.read_mifare_classic_block_(4, data)); 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); EXPECT_TRUE(pn532.read_mifare_classic_block_(4, data)); EXPECT_EQ(bytes_of(data), std::vector(16, 0xAB)); } // The NDEF TLV is type 0x03, a one-byte length below 255 (three bytes otherwise), the message, terminator 0xFE, // then zero padding out to the requested length. TEST(PN532Ndef, FillsTlv) { FixedVector buffer; const std::array message = {0xD1, 0x01, 0x02}; nfc::fill_ndef_tlv(message, 8, buffer); EXPECT_EQ(bytes_of(std::span(buffer)), (std::vector{0x03, 0x03, 0xD1, 0x01, 0x02, 0xFE, 0x00, 0x00})); std::vector long_message(300, 0xAA); nfc::fill_ndef_tlv(long_message, 320, buffer); ASSERT_EQ(buffer.size(), 320u); EXPECT_EQ(buffer[0], 0x03); EXPECT_EQ(buffer[1], 0xFF); EXPECT_EQ(buffer[2], 0x01); // 300 = 0x012C EXPECT_EQ(buffer[3], 0x2C); EXPECT_EQ(buffer[4], 0xAA); EXPECT_EQ(buffer[303], 0xAA); EXPECT_EQ(buffer[304], 0xFE); EXPECT_EQ(buffer[319], 0x00); } // 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); UltralightReadBuffer 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; UltralightReadBuffer 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