ESPHome 2026.10.2
Loading...
Searching...
No Matches
pn532.cpp
Go to the documentation of this file.
1#include "pn532.h"
2
3#include <algorithm>
4#include <array>
5#include <memory>
7#include "esphome/core/log.h"
8#include "esphome/core/hal.h"
9
10// Based on:
11// - https://cdn-shop.adafruit.com/datasheets/PN532C106_Application+Note_v1.2.pdf
12// - https://www.nxp.com/docs/en/nxp/application-notes/AN133910.pdf
13// - https://www.nxp.com/docs/en/nxp/application-notes/153710.pdf
14
15namespace esphome::pn532 {
16
17static const char *const TAG = "pn532";
18
20 // Get version data
21 if (!this->write_command_({PN532_COMMAND_VERSION_DATA})) {
22 ESP_LOGW(TAG, "Error sending version command, trying again");
23 if (!this->write_command_({PN532_COMMAND_VERSION_DATA})) {
24 ESP_LOGE(TAG, "Error sending version command");
25 this->mark_failed();
26 return;
27 }
28 }
29
30 PN532Frame version_data;
31 // GetFirmwareVersion returns IC, Ver, Rev and Support
32 if (!this->read_response(PN532_COMMAND_VERSION_DATA, version_data) || version_data.size() < 3) {
33 ESP_LOGE(TAG, "Error getting version");
34 this->mark_failed();
35 return;
36 }
37 ESP_LOGD(TAG, "Found chip PN5%02X, Firmware v%d.%d", version_data[0], version_data[1], version_data[2]);
38
39 if (!this->write_command_({
40 PN532_COMMAND_SAMCONFIGURATION,
41 0x01, // normal mode
42 0x14, // timeout: 20 x 50 ms (only used in virtual card mode)
43 0x01, // use IRQ
44 })) {
45 ESP_LOGE(TAG, "No wakeup ack");
46 this->mark_failed();
47 return;
48 }
49
50 PN532Frame wakeup_result;
51 if (!this->read_response(PN532_COMMAND_SAMCONFIGURATION, wakeup_result)) {
53 this->mark_failed();
54 return;
55 }
56
57 // Set up SAM (secure access module)
58 uint8_t sam_timeout = std::min<uint8_t>(255u, this->update_interval_ / 50);
59 if (!this->write_command_({
60 PN532_COMMAND_SAMCONFIGURATION,
61 0x01, // normal mode
62 sam_timeout, // timeout as multiple of 50ms (actually only for virtual card mode, but shouldn't matter)
63 0x01, // Enable IRQ
64 })) {
66 this->mark_failed();
67 return;
68 }
69
70 PN532Frame sam_result;
71 if (!this->read_response(PN532_COMMAND_SAMCONFIGURATION, sam_result)) {
72 ESP_LOGV(TAG, "Invalid SAM result: (%u)", sam_result.size()); // NOLINT
73 for (uint8_t dat : sam_result) {
74 ESP_LOGV(TAG, " 0x%02X", dat);
75 }
77 this->mark_failed();
78 return;
79 }
80
81 this->turn_off_rf_();
82}
83
85 updates_enabled_ = false;
86 requested_read_ = false;
87 ESP_LOGI(TAG, "Powering down PN532");
88 if (!this->write_command_({PN532_COMMAND_POWERDOWN, 0b10100000})) { // enable i2c,spi wakeup
89 ESP_LOGE(TAG, "Error writing powerdown command to PN532");
90 return false;
91 }
92 PN532Frame response;
93 if (!this->read_response(PN532_COMMAND_POWERDOWN, response)) {
94 ESP_LOGE(TAG, "Error reading PN532 powerdown response");
95 return false;
96 }
97 if (response.empty() || response[0] != 0x00) {
98 ESP_LOGE(TAG, "Powerdown error: %02x", response.empty() ? 0xFF : response[0]);
99 return false;
100 }
101 ESP_LOGV(TAG, "Powerdown successful");
102 delay(1);
103 return true;
104}
105
107 if (!updates_enabled_)
108 return;
109
110#ifdef PN532_BINARY_SENSOR_COUNT
111 for (auto *obj : this->binary_sensors_)
112 obj->on_scan_end();
113#endif
114
115 if (!this->write_command_({
116 PN532_COMMAND_INLISTPASSIVETARGET,
117 0x01, // max 1 card
118 0x00, // baud rate ISO14443A (106 kbit/s)
119 })) {
120 ESP_LOGW(TAG, "Requesting tag read failed!");
121 this->status_set_warning();
122 return;
123 }
124 this->status_clear_warning();
125 this->requested_read_ = true;
126}
127
129 if (!this->requested_read_)
130 return;
131
132 auto ready = this->read_ready_(false);
133 if (ready == WOULDBLOCK)
134 return;
135
136 bool success = false;
137 PN532Frame read;
138
139 if (ready == READY) {
140 success = this->read_response(PN532_COMMAND_INLISTPASSIVETARGET, read);
141 } else {
142 this->send_ack_(); // abort still running InListPassiveTarget
143 }
144
145 this->requested_read_ = false;
146
147 if (!success) {
148 // Something failed
149#ifdef PN532_ON_TAG_REMOVED_TRIGGER_COUNT
150 if (!this->current_uid_.empty()) {
151 auto tag = make_unique<nfc::NfcTag>(this->current_uid_);
152 for (auto *trigger : this->triggers_ontagremoved_)
153 trigger->process(tag);
154 }
155#endif
156 this->current_uid_ = {};
157 this->turn_off_rf_();
158 return;
159 }
160
161 uint8_t num_targets = read.empty() ? 0 : read[0];
162 if (num_targets != 1) {
163 // no tags found or too many
164#ifdef PN532_ON_TAG_REMOVED_TRIGGER_COUNT
165 if (!this->current_uid_.empty()) {
166 auto tag = make_unique<nfc::NfcTag>(this->current_uid_);
167 for (auto *trigger : this->triggers_ontagremoved_)
168 trigger->process(tag);
169 }
170#endif
171 this->current_uid_ = {};
172 this->turn_off_rf_();
173 return;
174 }
175
176 // target data for 106 kbps type A: NbTg, Tg, SENS_RES (2 bytes), SEL_RES, NFCIDLength, NFCID1 (UM0701-02, 7.3.5)
177 if (read.size() < 6) {
178 this->turn_off_rf_();
179 return;
180 }
181 const uint8_t sel_res = read[4];
182 uint8_t nfcid_length = read[5];
183 if (nfcid_length == 0 || nfcid_length > nfc::NFC_UID_MAX_LENGTH || read.size() < 6U + nfcid_length) {
184 // oops, pn532 returned invalid data
185 this->turn_off_rf_();
186 return;
187 }
188 nfc::NfcTagUid nfcid(read.begin() + 6, read.begin() + 6 + nfcid_length);
189 const uint8_t tag_type = tag_type_from_sel_res(sel_res);
190
191 bool report = true;
192#ifdef PN532_BINARY_SENSOR_COUNT
193 for (auto *bin_sens : this->binary_sensors_) {
194 if (bin_sens->process(nfcid)) {
195 report = false;
196 }
197 }
198#endif
199
200 if (nfcid.size() == this->current_uid_.size()) {
201 bool same_uid = true;
202 for (size_t i = 0; i < nfcid.size(); i++)
203 same_uid &= nfcid[i] == this->current_uid_[i];
204 if (same_uid)
205 return;
206 }
207
208 this->current_uid_ = nfcid;
209
210 if (next_task_ == READ) {
211 auto tag = this->read_tag_(nfcid, tag_type);
212#ifdef PN532_ON_TAG_TRIGGER_COUNT
213 for (auto *trigger : this->triggers_ontag_)
214 trigger->process(tag);
215#endif
216
217 if (report) {
218 char uid_buf[nfc::FORMAT_UID_BUFFER_SIZE];
219 ESP_LOGD(TAG, "Found new tag '%s'", nfc::format_uid_to(uid_buf, nfcid));
220 if (tag->has_ndef_message()) {
221 const auto &message = tag->get_ndef_message();
222 const auto &records = message->get_records();
223 ESP_LOGD(TAG, " NDEF formatted records:");
224 for (const auto &record : records) {
225 ESP_LOGD(TAG, " %s - %s", record->get_type().c_str(), record->get_payload().c_str());
226 }
227 }
228 }
229 } else if (next_task_ == CLEAN) {
230 ESP_LOGD(TAG, " Tag cleaning");
231 if (!this->clean_tag_(nfcid, tag_type)) {
232 ESP_LOGE(TAG, " Tag was not fully cleaned successfully");
233 }
234 ESP_LOGD(TAG, " Tag cleaned!");
235 } else if (next_task_ == FORMAT) {
236 ESP_LOGD(TAG, " Tag formatting");
237 if (!this->format_tag_(nfcid, tag_type)) {
238 ESP_LOGE(TAG, "Error formatting tag as NDEF");
239 }
240 ESP_LOGD(TAG, " Tag formatted!");
241 } else if (next_task_ == WRITE) {
242 if (this->next_task_message_to_write_ != nullptr) {
243 ESP_LOGD(TAG, " Tag writing");
244 ESP_LOGD(TAG, " Tag formatting");
245 if (!this->format_tag_(nfcid, tag_type)) {
246 ESP_LOGE(TAG, " Tag could not be formatted for writing");
247 } else {
248 ESP_LOGD(TAG, " Writing NDEF data");
249 if (!this->write_tag_(nfcid, tag_type, this->next_task_message_to_write_.get())) {
250 ESP_LOGE(TAG, " Failed to write message to tag");
251 }
252 ESP_LOGD(TAG, " Finished writing NDEF data");
253 this->next_task_message_to_write_.reset();
254 this->on_finished_write_callback_.call();
255 }
256 }
257 }
258
259 this->read_mode();
260
261 this->turn_off_rf_();
262}
263
264bool PN532::write_command_(const std::span<const uint8_t> data) {
265 if (data.size() > PN532_FRAME_MAX_DATA_SIZE) {
266 return false;
267 }
268 PN532Frame frame;
269 // Preamble
270 frame.push_back(0x00);
271
272 // Start code
273 frame.push_back(0x00);
274 frame.push_back(0xFF);
275
276 // Length of message, TFI + data bytes
277 const uint8_t real_length = data.size() + 1;
278 // LEN
279 frame.push_back(real_length);
280 // LCS (Length checksum)
281 frame.push_back(~real_length + 1);
282
283 // TFI (Frame Identifier, 0xD4 means to PN532, 0xD5 means from PN532)
284 frame.push_back(0xD4);
285 // calculate checksum, TFI is part of checksum
286 uint8_t checksum = 0xD4;
287
288 // DATA
289 for (uint8_t dat : data) {
290 frame.push_back(dat);
291 checksum += dat;
292 }
293
294 // DCS (Data checksum)
295 frame.push_back(~checksum + 1);
296 // Postamble
297 frame.push_back(0x00);
298
299 this->write_data(frame);
300
301 return this->read_ack_();
302}
303
305 ESP_LOGV(TAG, "Reading ACK");
306
307 PN532Frame data;
308 if (!this->read_data(data, 6) || data.size() < 7) {
309 return false;
310 }
311
312 bool matches = (data[1] == 0x00 && // preamble
313 data[2] == 0x00 && // start of packet
314 data[3] == 0xFF && data[4] == 0x00 && // ACK packet code
315 data[5] == 0xFF && data[6] == 0x00); // postamble
316 ESP_LOGV(TAG, "ACK valid: %s", YESNO(matches));
317 return matches;
318}
319
321 static constexpr std::array<uint8_t, 6> ACK_FRAME = {0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00};
322 ESP_LOGV(TAG, "Sending ACK for abort");
323 this->write_data(ACK_FRAME);
324 delay(10);
325}
327 static constexpr std::array<uint8_t, 6> NACK_FRAME = {0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00};
328 ESP_LOGV(TAG, "Sending NACK for retransmit");
329 this->write_data(NACK_FRAME);
330 delay(10);
331}
332
334 if (this->rd_ready_ == READY) {
335 if (block) {
336 this->rd_started_ = false;
337 this->rd_ready_ = WOULDBLOCK;
338 }
339 return READY;
340 }
341
342 // the non-blocking call comes from loop(), where the tick's timestamp is already cached
343 if (!this->rd_started_) {
345 this->rd_started_ = true;
346 }
347 const uint32_t rd_start_time = this->rd_start_time_;
348
349 while (true) {
350 if (this->is_read_ready()) {
351 this->rd_ready_ = READY;
352 break;
353 }
354
355 const uint32_t now = block ? millis() : App.get_loop_component_start_time();
356 if (now - rd_start_time > 100) {
357 ESP_LOGV(TAG, "Timed out waiting for readiness from PN532!");
358 this->rd_ready_ = TIMEOUT;
359 break;
360 }
361
362 if (!block) {
363 this->rd_ready_ = WOULDBLOCK;
364 break;
365 }
366
367 yield();
368 }
369
370 auto rdy = this->rd_ready_;
371 if (block || rdy == TIMEOUT) {
372 this->rd_started_ = false;
373 this->rd_ready_ = WOULDBLOCK;
374 }
375 return rdy;
376}
377
379 ESP_LOGV(TAG, "Turning RF field OFF");
380 this->write_command_({
381 PN532_COMMAND_RFCONFIGURATION,
382 0x01, // RF Field
383 0x00, // Off
384 });
385}
386
387std::unique_ptr<nfc::NfcTag> PN532::read_tag_(nfc::NfcTagUid &uid, const uint8_t tag_type) {
388 if (tag_type == nfc::TAG_TYPE_MIFARE_CLASSIC) {
389 ESP_LOGD(TAG, "Mifare classic");
390 return this->read_mifare_classic_tag_(uid);
391 } else if (tag_type == nfc::TAG_TYPE_2) {
392 ESP_LOGD(TAG, "Mifare ultralight");
393 return this->read_mifare_ultralight_tag_(uid);
394 }
395 ESP_LOGV(TAG, "Reading tag type %u is not supported", tag_type);
396 return make_unique<nfc::NfcTag>(uid);
397}
398
400 this->next_task_ = READ;
401 ESP_LOGD(TAG, "Waiting to read next tag");
402}
404 this->next_task_ = CLEAN;
405 ESP_LOGD(TAG, "Waiting to clean next tag");
406}
408 this->next_task_ = FORMAT;
409 ESP_LOGD(TAG, "Waiting to format next tag");
410}
412 this->next_task_ = WRITE;
413 this->next_task_message_to_write_.reset(message);
414 ESP_LOGD(TAG, "Waiting to write next tag");
415}
416
417bool PN532::clean_tag_(nfc::NfcTagUid &uid, const uint8_t tag_type) {
418 if (tag_type == nfc::TAG_TYPE_MIFARE_CLASSIC) {
419 return this->format_mifare_classic_mifare_(uid);
420 } else if (tag_type == nfc::TAG_TYPE_2) {
421 return this->clean_mifare_ultralight_();
422 }
423 ESP_LOGE(TAG, "Unsupported Tag for formatting");
424 return false;
425}
426
427bool PN532::format_tag_(nfc::NfcTagUid &uid, const uint8_t tag_type) {
428 if (tag_type == nfc::TAG_TYPE_MIFARE_CLASSIC) {
429 return this->format_mifare_classic_ndef_(uid);
430 } else if (tag_type == nfc::TAG_TYPE_2) {
431 return this->clean_mifare_ultralight_();
432 }
433 ESP_LOGE(TAG, "Unsupported Tag for formatting");
434 return false;
435}
436
437bool PN532::write_tag_(nfc::NfcTagUid &uid, const uint8_t tag_type, nfc::NdefMessage *message) {
438 if (tag_type == nfc::TAG_TYPE_MIFARE_CLASSIC) {
439 return this->write_mifare_classic_tag_(uid, message);
440 } else if (tag_type == nfc::TAG_TYPE_2) {
441 return this->write_mifare_ultralight_tag_(uid, message);
442 }
443 ESP_LOGE(TAG, "Unsupported Tag for writing");
444 return false;
445}
446
447bool PN532::in_data_exchange_(const std::span<const uint8_t> command, PN532Frame &response) {
448 // formatting a tag takes seconds of back-to-back exchanges inside loop(), longer than the task watchdog allows
449 App.feed_wdt();
450 if (!this->write_command_(command)) {
451 return false;
452 }
453 // output: Status, DataIn; a status of 0x00 means the exchange with the target succeeded (UM0701-02, 7.3.8)
454 if (!this->read_response(PN532_COMMAND_INDATAEXCHANGE, response) || response.empty()) {
455 return false;
456 }
457 if (response[0] != 0x00) {
458 ESP_LOGV(TAG, "InDataExchange failed, status 0x%02X", response[0]);
459 return false;
460 }
461 std::copy(response.begin() + 1, response.end(), response.begin());
462 response.resize(response.size() - 1);
463 return true;
464}
465
467 PN532Frame response;
468 if (!this->in_data_exchange_(
469 {
470 PN532_COMMAND_INDATAEXCHANGE,
471 0x01, // One card
472 nfc::MIFARE_CMD_READ,
473 address,
474 },
475 response) ||
476 response.size() != data.size()) {
477 return false;
478 }
479 std::copy(response.begin(), response.end(), data.begin());
480 return true;
481}
482
484 ESP_LOGCONFIG(TAG, "PN532:");
485 switch (this->error_code_) {
486 case NONE:
487 break;
488 case WAKEUP_FAILED:
489 ESP_LOGE(TAG, "Wake Up command failed!");
490 break;
492 ESP_LOGE(TAG, "SAM command failed!");
493 break;
494 }
495
496 LOG_UPDATE_INTERVAL(this);
497
498#ifdef PN532_BINARY_SENSOR_COUNT
499 for (auto *child : this->binary_sensors_) {
500 LOG_BINARY_SENSOR(" ", "Tag", child);
501 }
502#endif
503}
504
506 if (data.size() != this->uid_.size())
507 return false;
508
509 for (size_t i = 0; i < data.size(); i++) {
510 if (data[i] != this->uid_[i])
511 return false;
512 }
513
514 this->publish_state(true);
515 this->found_ = true;
516 return true;
517}
518
519} // namespace esphome::pn532
uint8_t checksum
Definition bl0906.h:3
uint8_t address
Definition bl0906.h:4
void feed_wdt()
Feed the task watchdog.
uint32_t IRAM_ATTR HOT get_loop_component_start_time() const
Get the cached time in milliseconds from when the current component started its loop execution.
void mark_failed()
Mark this component as failed.
void status_clear_warning()
Definition component.h:289
Minimal static vector - saves memory by avoiding std::vector overhead.
Definition helpers.h:291
size_t size() const
Definition helpers.h:360
void push_back(const T &value)
Definition helpers.h:330
bool empty() const
Definition helpers.h:362
void resize(size_t n)
Definition helpers.h:340
void publish_state(bool new_state)
Publish a new state to the front-end.
bool process(const nfc::NfcTagUid &data)
Definition pn532.cpp:505
StaticVector< nfc::NfcOnTagTrigger *, PN532_ON_TAG_REMOVED_TRIGGER_COUNT > triggers_ontagremoved_
Definition pn532.h:167
nfc::NfcTagUid current_uid_
Definition pn532.h:170
virtual bool read_data(PN532Frame &data, size_t len)=0
Reads len frame bytes into data behind a leading status byte, so every bus presents the I2C layout.
enum PN532ReadReady read_ready_(bool block)
Definition pn532.cpp:333
virtual bool is_read_ready()=0
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:466
LazyCallbackManager< void()> on_finished_write_callback_
Definition pn532.h:159
bool format_mifare_classic_mifare_(nfc::NfcTagUid &uid)
std::unique_ptr< nfc::NfcTag > read_mifare_ultralight_tag_(nfc::NfcTagUid &uid)
StaticVector< PN532BinarySensor *, PN532_BINARY_SENSOR_COUNT > binary_sensors_
Definition pn532.h:161
bool write_command_(std::span< const uint8_t > data)
Definition pn532.cpp:264
PN532ReadReady rd_ready_
Definition pn532.h:172
bool format_mifare_classic_ndef_(nfc::NfcTagUid &uid)
virtual bool read_response(uint8_t command, PN532Frame &data)=0
Reads the response to command; on success data holds only the bytes that follow the response code.
void dump_config() override
Definition pn532.cpp:483
void setup() override
Definition pn532.cpp:19
bool write_mifare_classic_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message)
std::unique_ptr< nfc::NfcTag > read_mifare_classic_tag_(nfc::NfcTagUid &uid)
std::unique_ptr< nfc::NdefMessage > next_task_message_to_write_
Definition pn532.h:169
void write_mode(nfc::NdefMessage *message)
Definition pn532.cpp:411
void update() override
Definition pn532.cpp:106
bool write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message)
std::unique_ptr< nfc::NfcTag > read_tag_(nfc::NfcTagUid &uid, uint8_t tag_type)
Definition pn532.cpp:387
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:447
StaticVector< nfc::NfcOnTagTrigger *, PN532_ON_TAG_TRIGGER_COUNT > triggers_ontag_
Definition pn532.h:164
bool clean_tag_(nfc::NfcTagUid &uid, uint8_t tag_type)
Definition pn532.cpp:417
uint32_t rd_start_time_
Definition pn532.h:171
bool write_tag_(nfc::NfcTagUid &uid, uint8_t tag_type, nfc::NdefMessage *message)
Definition pn532.cpp:437
void loop() override
Definition pn532.cpp:128
PN532Error error_code_
Definition pn532.h:174
bool format_tag_(nfc::NfcTagUid &uid, uint8_t tag_type)
Definition pn532.cpp:427
const LogString * message
Definition component.cpp:35
void yield(void)
char * format_uid_to(char *buffer, std::span< const uint8_t > uid)
Format UID to buffer with '-' separator (e.g., "04-11-22-33"). Returns buffer for inline use.
Definition nfc.cpp:10
uint8_t tag_type_from_sel_res(uint8_t sel_res)
Tag type (nfc::TAG_TYPE_*) from a type A target's SEL_RES byte.
Definition pn532.h:36
std::array< uint8_t, MIFARE_READ_SIZE > MifareReadData
Definition pn532.h:59
void HOT delay(uint32_t ms)
Definition hal.cpp:85
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:25
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t