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