ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
pn532_mifare_ultralight.cpp
Go to the documentation of this file.
1#include <algorithm>
2#include <array>
3#include <cinttypes>
4#include <memory>
5
6#include "pn532.h"
7#include "esphome/core/log.h"
8
9namespace esphome::pn532 {
10
11ESPHOME_LOG_TAG(TAG, "pn532.mifare_ultralight");
12
13std::unique_ptr<nfc::NfcTag> PN532::read_mifare_ultralight_tag_(nfc::NfcTagUid &uid) {
15 // pages 3 to 6 contain various info we are interested in -- do one read to grab it all
16 if (!this->read_mifare_ultralight_bytes_(3, data)) {
17 return make_unique<nfc::NfcTag>(uid, nfc::NFC_FORUM_TYPE_2);
18 }
19
20 if (!this->is_mifare_ultralight_formatted_(data)) {
21 ESP_LOGW(TAG, "Not NDEF formatted");
22 return make_unique<nfc::NfcTag>(uid, nfc::NFC_FORUM_TYPE_2);
23 }
24
25 uint32_t message_length;
26 uint8_t message_start_index;
27 if (!this->find_mifare_ultralight_ndef_(data, message_length, message_start_index)) {
28 ESP_LOGW(TAG, "Couldn't find NDEF message");
29 return make_unique<nfc::NfcTag>(uid, nfc::NFC_FORUM_TYPE_2);
30 }
31 ESP_LOGVV(TAG, "NDEF message length: %" PRIu32 ", start: %u", message_length, message_start_index);
32
33 if (message_length == 0) {
34 return make_unique<nfc::NfcTag>(uid, nfc::NFC_FORUM_TYPE_2);
35 }
36
37 // data[2] is the CC byte read earlier as part of pages 3-6, same value used by read_mifare_ultralight_capacity_()
38 const uint32_t capacity = data[2] * 8U;
39 const uint32_t total_length = message_length + message_start_index;
40 if (total_length > capacity) {
41 ESP_LOGW(TAG, "NDEF message of %" PRIu32 " bytes at offset %u exceeds tag capacity %" PRIu32, message_length,
42 message_start_index, capacity);
43 return make_unique<nfc::NfcTag>(uid, nfc::NFC_FORUM_TYPE_2);
44 }
45 if (message_length > MIFARE_ULTRALIGHT_MAX_NDEF_SIZE) {
46 ESP_LOGE(TAG, "NDEF message too long: %" PRIu32 " bytes", message_length);
47 return make_unique<nfc::NfcTag>(uid, nfc::NFC_FORUM_TYPE_2);
48 }
49
50 // we already read pages 3-6 earlier, pick up where we left off so we're not re-reading pages
51 constexpr size_t header_message_bytes = MIFARE_READ_SIZE - nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; // pages 4 to 6
52 const uint16_t read_length =
53 total_length > header_message_bytes ? static_cast<uint16_t>(total_length - header_message_bytes) : 0;
54 // A message up to the old 255 byte limit stays on the stack, a longer one borrows the heap for the read
56 std::span<uint8_t> message(buffer.get(), data.size() + read_length);
57 std::copy(data.begin(), data.end(), message.begin());
58 if (read_length) {
59 if (!this->read_mifare_ultralight_bytes_(nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE + 3,
60 message.subspan(data.size()))) {
61 ESP_LOGE(TAG, "Error reading tag data");
62 return make_unique<nfc::NfcTag>(uid, nfc::NFC_FORUM_TYPE_2);
63 }
64 }
65 // skip page 3 as well as any bytes ahead of message_start_index
66 const size_t skip = message_start_index + nfc::MIFARE_ULTRALIGHT_PAGE_SIZE;
67 if (skip >= message.size()) {
68 return make_unique<nfc::NfcTag>(uid, nfc::NFC_FORUM_TYPE_2);
69 }
70
71 return make_unique<nfc::NfcTag>(uid, nfc::NFC_FORUM_TYPE_2,
72 make_unique<nfc::NdefMessage>(std::span<const uint8_t>(message).subspan(skip)));
73}
74
75bool PN532::read_mifare_ultralight_bytes_(uint8_t start_page, std::span<uint8_t> data) {
76 MifareReadData chunk;
77
78 for (uint8_t i = 0; i * MIFARE_READ_SIZE < data.size(); i++) {
79 // a READ returns 4 pages (16 bytes)
80 if (!this->mifare_read_(uint8_t(i * nfc::MIFARE_ULTRALIGHT_READ_SIZE + start_page), chunk)) {
81 return false;
82 }
83 // keep only the bytes still wanted from this read
84 const size_t offset = i * MIFARE_READ_SIZE;
85 const size_t count = std::min(MIFARE_READ_SIZE, data.size() - offset);
86 std::copy_n(chunk.begin(), count, data.begin() + offset);
87 }
88
89 char data_buf[nfc::FORMAT_BYTES_BUFFER_SIZE];
90 ESP_LOGVV(TAG, "Data read: %s", nfc::format_bytes_to(data_buf, data));
91
92 return true;
93}
94
95bool PN532::is_mifare_ultralight_formatted_(const std::span<const uint8_t> page_3_to_6) {
96 const uint8_t p4_offset = nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; // page 4 will begin 4 bytes into the vector
97
98 return (page_3_to_6.size() > p4_offset + 3) &&
99 ((page_3_to_6[p4_offset + 0] != 0xFF) || (page_3_to_6[p4_offset + 1] != 0xFF) ||
100 (page_3_to_6[p4_offset + 2] != 0xFF) || (page_3_to_6[p4_offset + 3] != 0xFF));
101}
102
104 std::array<uint8_t, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE> data;
105 if (this->read_mifare_ultralight_bytes_(3, data)) {
106 ESP_LOGV(TAG, "Tag capacity is %u bytes", data[2] * 8U);
107 return data[2] * 8U;
108 }
109 return 0;
110}
111
112bool PN532::find_mifare_ultralight_ndef_(const std::span<const uint8_t> page_3_to_6, uint32_t &message_length,
113 uint8_t &message_start_index) {
114 const uint8_t p4_offset = nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; // page 4 will begin 4 bytes into the vector
115
116 if (!(page_3_to_6.size() > p4_offset + 6)) {
117 return false;
118 }
119
120 uint8_t tlv_offset;
121 if (page_3_to_6[p4_offset + 0] == 0x03) {
122 tlv_offset = 0;
123 } else if (page_3_to_6[p4_offset + 5] == 0x03) {
124 tlv_offset = 5;
125 } else {
126 return false;
127 }
128
129 // Long-form TLV length: 0x03 0xFF LEN_HI LEN_LO, used whenever the message is >= 255 bytes.
130 // Short-form is just 0x03 LEN. See NFC Forum Type 2 Tag Operation spec section 2.3.
131 if (page_3_to_6[p4_offset + tlv_offset + 1] == 0xFF) {
132 if (page_3_to_6.size() <= p4_offset + tlv_offset + 3) {
133 // long-form marker present but the length bytes weren't read -- fail explicitly instead of
134 // misreporting a 255-byte short-form message
135 return false;
136 }
137 message_length =
138 (static_cast<uint32_t>(page_3_to_6[p4_offset + tlv_offset + 2]) << 8) | page_3_to_6[p4_offset + tlv_offset + 3];
139 message_start_index = tlv_offset + 4;
140 } else {
141 message_length = page_3_to_6[p4_offset + tlv_offset + 1];
142 message_start_index = tlv_offset + 2;
143 }
144 return true;
145}
146
149
150 const auto encoded = message->encode();
151 const uint32_t buffer_length = nfc::get_mifare_ultralight_buffer_size(encoded.size());
152
153 if (buffer_length > capacity) {
154 ESP_LOGE(TAG, "Message length exceeds tag capacity %" PRIu32 " > %" PRIu32, buffer_length, capacity);
155 return false;
156 }
157
159 nfc::fill_ndef_tlv(encoded, buffer_length, buffer);
160
161 uint32_t index = 0;
162 uint8_t current_page = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE;
163
164 while (index < buffer_length) {
166 current_page, std::span<const uint8_t>(&buffer[index], nfc::MIFARE_ULTRALIGHT_PAGE_SIZE))) {
167 return false;
168 }
169 index += nfc::MIFARE_ULTRALIGHT_PAGE_SIZE;
170 current_page++;
171 }
172 return true;
173}
174
177 uint8_t pages = (capacity / nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) + nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE;
178
179 static constexpr std::array<uint8_t, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE> BLANK_DATA = {0x00, 0x00, 0x00, 0x00};
180
181 for (int i = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; i < pages; i++) {
182 if (!this->write_mifare_ultralight_page_(i, BLANK_DATA)) {
183 return false;
184 }
185 }
186 return true;
187}
188
189bool PN532::write_mifare_ultralight_page_(uint8_t page_num, const std::span<const uint8_t> write_data) {
191 PN532_COMMAND_INDATAEXCHANGE,
192 0x01, // One card
193 nfc::MIFARE_CMD_WRITE_ULTRALIGHT,
194 page_num,
195 };
196 for (const uint8_t byte : write_data) {
197 cmd.push_back(byte);
198 }
199
200 PN532Frame response;
201 if (!this->in_data_exchange_(cmd, response)) {
202 ESP_LOGE(TAG, "Error writing page %u", page_num);
203 return false;
204 }
205
206 return true;
207}
208
209} // namespace esphome::pn532
Fixed-capacity vector - sized once through init() or try_init(); push_back never reallocates This avo...
Definition helpers.h:624
Helper class for efficient buffer allocation - uses stack for small sizes, heap for large This is use...
Definition helpers.h:827
void push_back(const T &value)
Definition helpers.h:325
bool find_mifare_ultralight_ndef_(std::span< const uint8_t > page_3_to_6, uint32_t &message_length, uint8_t &message_start_index)
bool mifare_read_(uint8_t address, MifareReadData &data)
Sends MIFARE READ for address and returns the 16 bytes the tag answers with.
Definition pn532.cpp:459
std::unique_ptr< nfc::NfcTag > read_mifare_ultralight_tag_(nfc::NfcTagUid &uid)
bool write_mifare_ultralight_page_(uint8_t page_num, std::span< const uint8_t > write_data)
bool write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message)
bool read_mifare_ultralight_bytes_(uint8_t start_page, std::span< uint8_t > data)
Fills data with the bytes from start_page onward.
virtual bool write_data(std::span< const uint8_t > data)=0
bool in_data_exchange_(std::span< const uint8_t > command, PN532Frame &response)
Sends an InDataExchange command and reads the response; returns false unless the status byte reports ...
Definition pn532.cpp:440
bool is_mifare_ultralight_formatted_(std::span< const uint8_t > page_3_to_6)
const LogString * message
Definition component.cpp:35
unsigned count
Definition gif.cpp:51
uint32_t buffer
Definition gif.cpp:50
char * format_bytes_to(char *buffer, std::span< const uint8_t > bytes)
Format bytes to buffer with ' ' separator (e.g., "04 11 22 33"). Returns buffer for inline use.
Definition nfc.cpp:14
uint32_t get_mifare_ultralight_buffer_size(uint32_t message_length)
Definition nfc.cpp:64
void fill_ndef_tlv(const std::span< const uint8_t > message, const uint32_t buffer_length, FixedVector< uint8_t > &buffer)
Fills buffer with the NDEF TLV (type, length, message, terminator) padded with zeros to buffer_length
Definition nfc.cpp:72
ESPHOME_LOG_TAG(TAG, "pn532")
std::array< uint8_t, MIFARE_READ_SIZE > MifareReadData
Definition pn532.h:59
std::array< uint8_t, MIFARE_READ_SIZE > UltralightHeader
Pages 3 to 6, read in one go: the capability container and the start of the NDEF TLV.
Definition pn532.h:61
uint16_t uint16_t & capacity
Definition helpers.cpp:34
static void uint32_t