From 57dbea91bf02f419bfd01982588754f6b596ee70 Mon Sep 17 00:00:00 2001 From: Shane Jordan Date: Sat, 28 Mar 2026 07:22:48 -0500 Subject: [PATCH 1/3] feat: add PN532 NFC reader support with runtime selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add Adafruit PN532 as a second NFC reader option alongside PN5180. NVS key `nfc_reader` selects which reader initializes at boot (one-binary model). Both readers implement NFCConnectionI — zero changes to NFCManager or tag classification. - HardwareNFCConnectionPN532: full NFCConnectionI impl (ISO14443A only) - openprinttag_adafruit_pn532.h: HAL adapter for Adafruit library - Fix abstraction leaks: getReaderInfo() + logDiagnostics() virtual methods replace static_cast in NFCManager - NVS config + web UI dropdown + installer prompt - Review fixes: firmware version parsing, UID verification in reactivateTag, GCode digit validation, memory cleanup on init failure, config validation --- .gitignore | 1 + include/BoardPins.h | 34 +++ include/UserConfig.example.h | 3 + .../openprinttag_adafruit_pn532.h | 78 +++++++ platformio.ini | 1 + src/ApplicationManager.cpp | 14 +- src/ConfigHTML.h | 9 + src/ConfigurationManager.cpp | 14 ++ src/ConfigurationManager.h | 8 + src/HardwareNFCConnection.cpp | 6 + src/HardwareNFCConnection.h | 3 +- src/HardwareNFCConnectionPN532.cpp | 212 ++++++++++++++++++ src/HardwareNFCConnectionPN532.h | 43 ++++ src/NFCConnectionI.h | 9 + src/NFCManager.cpp | 16 +- src/NFCManager.h | 4 +- src/TroubleshootingHTML.h | 6 +- src/WebServerManager.cpp | 13 +- src/main.cpp | 11 + 19 files changed, 459 insertions(+), 26 deletions(-) create mode 100644 lib/openprinttag/openprinttag_adafruit_pn532.h create mode 100644 src/HardwareNFCConnectionPN532.cpp create mode 100644 src/HardwareNFCConnectionPN532.h diff --git a/.gitignore b/.gitignore index 5aa26f5..f405f5e 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ docs/tigertag-architecture.md docs/deep-thoughts.md CODE-CLEANUP.md docs/writer-ui-plan.md +.gstack/ diff --git a/include/BoardPins.h b/include/BoardPins.h index 8c6ed05..c8fd65f 100644 --- a/include/BoardPins.h +++ b/include/BoardPins.h @@ -14,11 +14,30 @@ #define PIN_PN5180_GPIO 10 // General purpose I/O (card detection) #define PIN_PN5180_IRQ 11 // Interrupt request (active HIGH) #define PIN_PN5180_AUX 12 // Auxiliary monitoring (future use) + // PN532 SPI (shares physical pins with PN5180; only one active at runtime) + #define PIN_PN532_SCK PIN_PN5180_SCK + #define PIN_PN532_MOSI PIN_PN5180_MOSI + #define PIN_PN532_MISO PIN_PN5180_MISO + #define PIN_PN532_SS PIN_PN5180_NSS + #define PIN_PN532_IRQ PIN_PN5180_IRQ + #define PIN_PN532_RST PIN_PN5180_RST // LCD I2C #define PIN_LCD_SDA 1 #define PIN_LCD_SCL 2 // Status LED — onboard WS2812 RGB (always available, no external wiring) #define PIN_STATUS_LED 21 + // 3x4 Matrix Keypad (ENABLE_KEYPAD) + // NOTE: GPIO 19/20 are USB D-/D+ on S3 and unavailable as GPIO. + // NOTE: If using LCD + keypad simultaneously on S3, pin conflicts may occur. + // For LCD + keypad builds, the ESP32-WROOM is strongly recommended + // due to its larger number of freely available GPIO pins. + #define PIN_KEYPAD_ROW1 38 + #define PIN_KEYPAD_ROW2 39 + #define PIN_KEYPAD_ROW3 40 + #define PIN_KEYPAD_ROW4 41 + #define PIN_KEYPAD_COL1 17 + #define PIN_KEYPAD_COL2 18 + #define PIN_KEYPAD_COL3 42 #else // --- ESP32-WROOM-32 pin mapping (default) --- // PN5180 SPI @@ -31,9 +50,24 @@ #define PIN_PN5180_GPIO 32 // General purpose I/O (card detection) #define PIN_PN5180_IRQ 35 // Interrupt request (active HIGH, input-only pin) #define PIN_PN5180_AUX 34 // Auxiliary monitoring (input-only pin) + // PN532 SPI (shares physical pins with PN5180; only one active at runtime) + #define PIN_PN532_SCK PIN_PN5180_SCK + #define PIN_PN532_MOSI PIN_PN5180_MOSI + #define PIN_PN532_MISO PIN_PN5180_MISO + #define PIN_PN532_SS PIN_PN5180_NSS + #define PIN_PN532_IRQ PIN_PN5180_IRQ + #define PIN_PN532_RST PIN_PN5180_RST // LCD I2C #define PIN_LCD_SDA 23 #define PIN_LCD_SCL 22 // Status LED — external SK6812 RGBW (optional, requires wiring) #define PIN_STATUS_LED 4 + // 3x4 Matrix Keypad (ENABLE_KEYPAD) + #define PIN_KEYPAD_ROW1 15 + #define PIN_KEYPAD_ROW2 16 + #define PIN_KEYPAD_ROW3 17 + #define PIN_KEYPAD_ROW4 18 + #define PIN_KEYPAD_COL1 19 + #define PIN_KEYPAD_COL2 21 + #define PIN_KEYPAD_COL3 5 #endif diff --git a/include/UserConfig.example.h b/include/UserConfig.example.h index c889495..8d0daef 100644 --- a/include/UserConfig.example.h +++ b/include/UserConfig.example.h @@ -34,6 +34,9 @@ /* Optional hardware features */ #define ENABLE_LCD 0 #define ENABLE_STATUS_LED 1 // Always available on S3-Zero (onboard LED), optional on WROOM (external wiring) +#define ENABLE_KEYPAD 0 // 3x4 matrix keypad — wiring per BoardPins.h + // Note: Using LCD + keypad together on S3 is not recommended due to limited GPIO. + // For LCD + keypad builds, use the WROOM board. /* Board selection: uncomment ONE of the following. Pin mapping is automatic via BoardPins.h — no need to configure pins manually. */ diff --git a/lib/openprinttag/openprinttag_adafruit_pn532.h b/lib/openprinttag/openprinttag_adafruit_pn532.h new file mode 100644 index 0000000..87d1c4f --- /dev/null +++ b/lib/openprinttag/openprinttag_adafruit_pn532.h @@ -0,0 +1,78 @@ +/** + * OpenPrintTag Adafruit PN532 HAL Adapter + * + * Provides NFC HAL implementation for Adafruit_PN532 Arduino library. + * Wraps mifareultralight_ReadPage/WritePage into opt_nfc_hal_t callbacks. + */ + +#ifndef OPENPRINTTAG_ADAFRUIT_PN532_H +#define OPENPRINTTAG_ADAFRUIT_PN532_H + +#include "openprinttag_lib.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Read a page from NTAG using Adafruit_PN532. + * + * @param ctx Adafruit_PN532 pointer + * @param page Page number to read + * @param buf Output buffer (4 bytes) + * @return OPT_OK on success, OPT_ERR_NFC_READ on failure + */ +static inline opt_error_t opt_adafruit_pn532_read_page(void *ctx, uint8_t page, uint8_t *buf) { + Adafruit_PN532 *pn532 = (Adafruit_PN532 *)ctx; + /* mifareultralight_ReadPage reads 4 bytes, returns count (non-zero = success) */ + return pn532->mifareultralight_ReadPage(page, buf) ? OPT_OK : OPT_ERR_NFC_READ; +} + +/** + * Write a page to NTAG using Adafruit_PN532. + * + * @param ctx Adafruit_PN532 pointer + * @param page Page number to write + * @param data Data to write (4 bytes) + * @return OPT_OK on success, OPT_ERR_NFC_WRITE on failure + */ +static inline opt_error_t opt_adafruit_pn532_write_page(void *ctx, uint8_t page, const uint8_t *data) { + Adafruit_PN532 *pn532 = (Adafruit_PN532 *)ctx; + /* mifareultralight_WritePage returns true on success */ + return pn532->mifareultralight_WritePage(page, (uint8_t *)data) ? OPT_OK : OPT_ERR_NFC_WRITE; +} + +/** + * Create a HAL structure for Adafruit_PN532. + * + * @param pn532 Initialized Adafruit_PN532 instance + * @return HAL structure ready for use with opt_read_from_nfc/opt_write_to_nfc + * + * Example usage: + * Adafruit_PN532 pn532(SS_PIN, &SPI); + * pn532.begin(); + * pn532.SAMConfig(); + * + * opt_nfc_hal_t hal = opt_create_adafruit_pn532_hal(&pn532); + * + * opt_tag_t tag; + * opt_init(&tag); + * opt_read_from_nfc(&tag, &hal, 4, 50); + * opt_parse_ndef(&tag); + */ +static inline opt_nfc_hal_t opt_create_adafruit_pn532_hal(Adafruit_PN532 *pn532) { + opt_nfc_hal_t hal = { + .read_page = opt_adafruit_pn532_read_page, + .write_page = opt_adafruit_pn532_write_page, + .is_present = NULL, + .user_ctx = pn532 + }; + return hal; +} + +#ifdef __cplusplus +} +#endif + +#endif /* OPENPRINTTAG_ADAFRUIT_PN532_H */ diff --git a/platformio.ini b/platformio.ini index bb771b6..04383c3 100644 --- a/platformio.ini +++ b/platformio.ini @@ -24,6 +24,7 @@ lib_deps = knolleary/PubSubClient@^2.8 codewitch-honey-crisis/htcw_json@^0.2.5 adafruit/Adafruit NeoPixel + adafruit/Adafruit PN532@1.3.4 chris--a/Keypad@^3.1.1 [env:esp32dev] diff --git a/src/ApplicationManager.cpp b/src/ApplicationManager.cpp index 5ff37f4..b40804c 100644 --- a/src/ApplicationManager.cpp +++ b/src/ApplicationManager.cpp @@ -981,6 +981,14 @@ void ApplicationManager::handleKeypadCancel() { bool ApplicationManager::sendAssignSpool(const char* toolNumber) { #ifndef NATIVE_TEST + // Validate tool number is digits-only (prevent GCode injection) + for (const char* p = toolNumber; *p; p++) { + if (*p < '0' || *p > '9') { + Serial.printf("ApplicationManager: Invalid tool number '%s'\n", toolNumber); + return false; + } + } + const char* moonrakerUrl = ConfigurationManager::getInstance().getMoonrakerURL(); if (!moonrakerUrl || moonrakerUrl[0] == '\0') { Serial.println("ApplicationManager: Moonraker URL not configured — cannot assign spool"); @@ -989,7 +997,7 @@ bool ApplicationManager::sendAssignSpool(const char* toolNumber) { } extern SemaphoreHandle_t g_httpMutex; - if (g_httpMutex && xSemaphoreTake(g_httpMutex, pdMS_TO_TICKS(5000)) != pdTRUE) { + if (g_httpMutex && xSemaphoreTake(g_httpMutex, pdMS_TO_TICKS(3000)) != pdTRUE) { Serial.println("ApplicationManager: Could not acquire HTTP mutex for ASSIGN_SPOOL"); if (lcdManager) lcdManager->updateScreen("Assign failed", "HTTP busy"); return false; @@ -1006,8 +1014,8 @@ bool ApplicationManager::sendAssignSpool(const char* toolNumber) { WiFiClient client; HTTPClient http; - http.setConnectTimeout(3000); - http.setTimeout(5000); + http.setConnectTimeout(1000); + http.setTimeout(2000); http.begin(client, url); http.addHeader("Content-Type", "application/json"); int code = http.POST(postBody); diff --git a/src/ConfigHTML.h b/src/ConfigHTML.h index b90b000..3054503 100644 --- a/src/ConfigHTML.h +++ b/src/ConfigHTML.h @@ -187,6 +187,13 @@ const char CONFIG_HTML[] PROGMEM = R"rawliteral( +
+ NFC Reader + +
@@ -226,6 +233,7 @@ const char CONFIG_HTML[] PROGMEM = R"rawliteral( document.getElementById('lcd_enabled').checked = !!cfg.lcd_enabled; document.getElementById('led_enabled').checked = !!cfg.led_enabled; document.getElementById('keypad_enabled').checked = !!cfg.keypad_enabled; + if (cfg.nfc_reader) document.getElementById('nfc_reader').value = cfg.nfc_reader; maybeSetValue('moonraker_url', cfg.moonraker_url); // Password placeholders if (cfg.wifi_pass_set) document.getElementById('wifi_pass').placeholder = '(set) Leave blank to keep'; @@ -262,6 +270,7 @@ const char CONFIG_HTML[] PROGMEM = R"rawliteral( lcd_enabled: document.getElementById('lcd_enabled').checked ? 1 : 0, led_enabled: document.getElementById('led_enabled').checked ? 1 : 0, keypad_enabled: document.getElementById('keypad_enabled').checked ? 1 : 0, + nfc_reader: document.getElementById('nfc_reader').value, moonraker_url: document.getElementById('moonraker_url').value.trim(), prusalink_on: document.getElementById('prusalink_on').checked ? 1 : 0, prusalink_url: document.getElementById('prusalink_url').value.trim(), diff --git a/src/ConfigurationManager.cpp b/src/ConfigurationManager.cpp index ce8ca1c..34e97d0 100644 --- a/src/ConfigurationManager.cpp +++ b/src/ConfigurationManager.cpp @@ -27,6 +27,7 @@ static const char* NVS_KEY_MOONRAKER_URL = "moonraker_url"; static const char* NVS_KEY_PRUSALINK_ON = "prusalink_on"; static const char* NVS_KEY_PRUSALINK_URL = "prusalink_url"; static const char* NVS_KEY_PRUSALINK_KEY = "prusalink_key"; +static const char* NVS_KEY_NFC_READER = "nfc_reader"; ConfigurationManager& ConfigurationManager::getInstance() { static ConfigurationManager instance; @@ -101,6 +102,9 @@ void ConfigurationManager::loadFromDeviceConfig() { _prusaLinkUrl[0] = '\0'; _prusaLinkApiKey[0] = '\0'; + // NFC reader default + strncpy(_nfcReader, "pn5180", sizeof(_nfcReader) - 1); + // Optional hardware feature defaults from compile-time flags _lcdEnabled = cfg.peripherals.lcd_enabled; _ledEnabled = cfg.peripherals.status_led_enabled; @@ -188,6 +192,10 @@ bool ConfigurationManager::loadFromNVS() { _keypadEnabled = prefs.getUChar(NVS_KEY_KEYPAD_ON, _keypadEnabled ? 1 : 0) != 0; anyOverride = true; } + if (prefs.isKey(NVS_KEY_NFC_READER)) { + prefs.getString(NVS_KEY_NFC_READER, _nfcReader, sizeof(_nfcReader)); + anyOverride = true; + } prefs.end(); return anyOverride; @@ -270,6 +278,10 @@ const char* ConfigurationManager::getMoonrakerURL() const { return _moonrakerUrl; } +const char* ConfigurationManager::getNfcReader() const { + return _nfcReader; +} + void ConfigurationManager::getCurrentConfig(ConfigUpdate& out) const { memset(&out, 0, sizeof(out)); strncpy(out.wifi_ssid, _ssid, sizeof(out.wifi_ssid) - 1); @@ -288,6 +300,7 @@ void ConfigurationManager::getCurrentConfig(ConfigUpdate& out) const { out.led_enabled = _ledEnabled ? 1 : 0; out.keypad_enabled = _keypadEnabled ? 1 : 0; strncpy(out.moonraker_url, _moonrakerUrl, sizeof(out.moonraker_url) - 1); + strncpy(out.nfc_reader, _nfcReader, sizeof(out.nfc_reader) - 1); } #ifndef NATIVE_TEST @@ -321,6 +334,7 @@ bool ConfigurationManager::saveToNVS(const ConfigUpdate& update) { if (update.prusalink_api_key[0] != '\0') { prefs.putString(NVS_KEY_PRUSALINK_KEY, update.prusalink_api_key); } + prefs.putString(NVS_KEY_NFC_READER, update.nfc_reader); prefs.end(); Serial.println("ConfigurationManager: Config saved to NVS"); diff --git a/src/ConfigurationManager.h b/src/ConfigurationManager.h index 574e51c..bb7ec53 100644 --- a/src/ConfigurationManager.h +++ b/src/ConfigurationManager.h @@ -27,6 +27,8 @@ struct ConfigUpdate { uint8_t prusalink_on; char prusalink_url[128]; char prusalink_api_key[64]; + // NFC reader selection + char nfc_reader[8]; // "pn5180" or "pn532" }; class ConfigurationManager { @@ -58,6 +60,9 @@ class ConfigurationManager { const char* getPrusaLinkURL() const; const char* getPrusaLinkAPIKey() const; + // NFC reader selection (NVS, default "pn5180") + const char* getNfcReader() const; + // Optional hardware features (compile-time default, overridable via NVS) bool isLcdEnabled() const; bool isLedEnabled() const; @@ -99,6 +104,9 @@ class ConfigurationManager { char _prusaLinkUrl[128] = {0}; char _prusaLinkApiKey[64] = {0}; + // NFC reader selection + char _nfcReader[8] = "pn5180"; + // Optional hardware features bool _lcdEnabled = false; bool _ledEnabled = false; diff --git a/src/HardwareNFCConnection.cpp b/src/HardwareNFCConnection.cpp index 637fa37..4936683 100644 --- a/src/HardwareNFCConnection.cpp +++ b/src/HardwareNFCConnection.cpp @@ -61,6 +61,12 @@ opt_error_t HardwareNFCConnection::halWritePage(void* ctx, uint8_t page, const u return OPT_OK; } +void HardwareNFCConnection::getReaderInfo(char* buf, size_t len) const { + if (buf && len > 0) { + snprintf(buf, len, "PN5180 v%d.%d", fw_[1], fw_[0]); + } +} + bool HardwareNFCConnection::begin() { // Configure additional input pins for future use pinMode(PIN_PN5180_IRQ, INPUT); // Interrupt (active HIGH) diff --git a/src/HardwareNFCConnection.h b/src/HardwareNFCConnection.h index 682e04b..d6fa785 100644 --- a/src/HardwareNFCConnection.h +++ b/src/HardwareNFCConnection.h @@ -24,8 +24,9 @@ class HardwareNFCConnection : public NFCConnectionI { bool writeISO14443Pages(uint8_t startPage, uint8_t pageCount, const uint8_t* data, uint16_t dataLen) override; uint8_t getLastSAK() const override { return lastSAK_; } uint16_t getLastATQA() const override { return lastATQA_; } + void getReaderInfo(char* buf, size_t len) const override; // Diagnostics: log RF_STATUS, IRQ_STATUS, SYSTEM_STATUS registers - void logDiagnostics(); + void logDiagnostics() override; // Returns PN5180 firmware version bytes (set during begin()). fw[0]=minor, fw[1]=major. void getPN5180FirmwareVersion(uint8_t fw[2]) const { fw[0] = fw_[0]; fw[1] = fw_[1]; } bool isPN5180Ready() const { return pn5180Ready_; } diff --git a/src/HardwareNFCConnectionPN532.cpp b/src/HardwareNFCConnectionPN532.cpp new file mode 100644 index 0000000..2b54465 --- /dev/null +++ b/src/HardwareNFCConnectionPN532.cpp @@ -0,0 +1,212 @@ +#include "HardwareNFCConnectionPN532.h" +#include "openprinttag_adafruit_pn532.h" +#include "BoardPins.h" + +#include +#include + +// Adafruit_PN532 uses a file-scope global packet buffer. After readPassiveTargetID, +// the InListPassiveTarget response populates it with ATQA (bytes 9-10) and SAK (byte 11). +extern byte pn532_packetbuffer[]; + +HardwareNFCConnectionPN532::HardwareNFCConnectionPN532() { + memset(&hal_, 0, sizeof(hal_)); + memset(currentUid_, 0, sizeof(currentUid_)); +} + +HardwareNFCConnectionPN532::~HardwareNFCConnectionPN532() { + delete pn532_; +} + +bool HardwareNFCConnectionPN532::begin() { + // Initialize SPI with explicit ESP32 pin mapping + SPI.begin(PIN_PN532_SCK, PIN_PN532_MISO, PIN_PN532_MOSI, PIN_PN532_SS); + + pn532_ = new Adafruit_PN532(PIN_PN532_SS, &SPI); + if (!pn532_) { + Serial.println("PN532: Failed to allocate"); + return false; + } + + pn532_->begin(); + + // Read firmware version to verify communication + uint32_t versiondata = pn532_->getFirmwareVersion(); + if (!versiondata) { + Serial.println("PN532: No response — check wiring"); + delete pn532_; + pn532_ = nullptr; + return false; + } + + // getFirmwareVersion() returns: IC<<24 | FW<<16 | Rev<<8 | Support + fwMajor_ = (versiondata >> 16) & 0xFF; // firmware version + fwMinor_ = (versiondata >> 8) & 0xFF; // firmware revision + Serial.printf("PN532: Found IC=0x%02X firmware v%d.%d\n", + (uint8_t)((versiondata >> 24) & 0xFF), fwMajor_, fwMinor_); + + // Configure the PN532 to read RFID tags + if (!pn532_->SAMConfig()) { + Serial.println("PN532: SAMConfig failed"); + delete pn532_; + pn532_ = nullptr; + return false; + } + + // Set up openprinttag HAL + hal_ = opt_create_adafruit_pn532_hal(pn532_); + + ready_ = true; + Serial.println("PN532: Initialized (ISO14443A only)"); + return true; +} + +void HardwareNFCConnectionPN532::reset() { + if (pn532_) { + pn532_->begin(); + pn532_->SAMConfig(); + } +} + +bool HardwareNFCConnectionPN532::hardwareReset() { + // Toggle RST pin for hardware reset + pinMode(PIN_PN532_RST, OUTPUT); + digitalWrite(PIN_PN532_RST, LOW); + delay(10); + digitalWrite(PIN_PN532_RST, HIGH); + delay(50); // PN532 boot time + + if (pn532_) { + pn532_->begin(); + uint32_t ver = pn532_->getFirmwareVersion(); + if (!ver) return false; + pn532_->SAMConfig(); + } + return true; +} + +bool HardwareNFCConnectionPN532::setupRF() { + // PN532 manages RF internally — no manual RF setup needed + return ready_; +} + +bool HardwareNFCConnectionPN532::detectTag(uint8_t* uid, uint8_t* uidLength) { + if (!pn532_ || !ready_) return false; + + // Short timeout (100ms) to avoid blocking the scan loop + uint8_t uidLen = 0; + bool found = pn532_->readPassiveTargetID(PN532_MIFARE_ISO14443A, uid, &uidLen, 100); + if (!found || uidLen == 0) return false; + + *uidLength = uidLen; + + // Extract SAK/ATQA from the global packet buffer. + // After readdata(pn532_packetbuffer, 20) in readDetectedPassiveTargetID: + // [7] = tags found + // [9-10] = SENS_RES (ATQA) — big-endian in Adafruit's code + // [11] = SEL_RES (SAK) + // [12] = UID length + // [13+] = UID bytes + lastATQA_ = ((uint16_t)pn532_packetbuffer[9] << 8) | pn532_packetbuffer[10]; + lastSAK_ = pn532_packetbuffer[11]; + + return true; +} + +void HardwareNFCConnectionPN532::setCurrentUid(const uint8_t* uid, uint8_t length) { + currentUidLen_ = (length <= sizeof(currentUid_)) ? length : sizeof(currentUid_); + memcpy(currentUid_, uid, currentUidLen_); +} + +opt_nfc_hal_t* HardwareNFCConnectionPN532::getHal() { + return &hal_; +} + +bool HardwareNFCConnectionPN532::reactivateTag() { + if (!pn532_) return false; + uint8_t uid[10]; + uint8_t uidLen = 0; + if (!pn532_->readPassiveTargetID(PN532_MIFARE_ISO14443A, uid, &uidLen, 200)) + return false; + // Verify same tag is still present (prevent silent cross-tag reads/writes) + if (uidLen != currentUidLen_ || memcmp(uid, currentUid_, uidLen) != 0) + return false; + return true; +} + +uint16_t HardwareNFCConnectionPN532::readISO14443Pages( + uint8_t startPage, uint8_t pageCount, uint8_t* buffer, uint16_t bufferSize) { + if (!pn532_ || !ready_) return 0; + + uint16_t totalBytes = (uint16_t)pageCount * 4; + if (totalBytes > bufferSize) return 0; + + uint16_t bytesRead = 0; + for (uint8_t i = 0; i < pageCount; i++) { + uint8_t page = startPage + i; + uint8_t pageBuf[4]; + + // Try read, reactivate tag on failure and retry once + if (!pn532_->mifareultralight_ReadPage(page, pageBuf)) { + if (!reactivateTag()) return bytesRead; + if (!pn532_->mifareultralight_ReadPage(page, pageBuf)) { + return bytesRead; + } + } + + memcpy(buffer + (i * 4), pageBuf, 4); + bytesRead += 4; + } + + return bytesRead; +} + +bool HardwareNFCConnectionPN532::writeISO14443Pages( + uint8_t startPage, uint8_t pageCount, const uint8_t* data, uint16_t dataLen) { + if (!pn532_ || !ready_) return false; + + uint16_t requiredLen = (uint16_t)pageCount * 4; + if (dataLen < requiredLen) return false; + + for (uint8_t i = 0; i < pageCount; i++) { + uint8_t page = startPage + i; + const uint8_t* pageData = data + (i * 4); + + // Retry up to 2 times per page (matching PN5180 pattern) + bool written = false; + for (int attempt = 0; attempt < 3; attempt++) { + if (pn532_->mifareultralight_WritePage(page, const_cast(pageData))) { + written = true; + break; + } + // Reactivate tag before retry + if (!reactivateTag()) return false; + } + if (!written) return false; + + // Yield between writes to prevent FreeRTOS starvation + vTaskDelay(pdMS_TO_TICKS(5)); + } + + return true; +} + +void HardwareNFCConnectionPN532::getReaderInfo(char* buf, size_t len) const { + if (buf && len > 0) { + snprintf(buf, len, "PN532 v%d.%d", fwMajor_, fwMinor_); + } +} + +void HardwareNFCConnectionPN532::logDiagnostics() { + if (!pn532_ || !ready_) { + Serial.println("PN532: Not initialized"); + return; + } + uint32_t ver = pn532_->getFirmwareVersion(); + if (ver) { + Serial.printf("PN532: IC=0x%02X FW=%d.%d\n", + (uint8_t)(ver >> 24), (uint8_t)(ver >> 16), (uint8_t)(ver >> 8)); + } else { + Serial.println("PN532: No response during diagnostics"); + } +} diff --git a/src/HardwareNFCConnectionPN532.h b/src/HardwareNFCConnectionPN532.h new file mode 100644 index 0000000..61e0a60 --- /dev/null +++ b/src/HardwareNFCConnectionPN532.h @@ -0,0 +1,43 @@ +#ifndef HARDWARE_NFC_CONNECTION_PN532_H +#define HARDWARE_NFC_CONNECTION_PN532_H + +#include "NFCConnectionI.h" +#include "BoardPins.h" +#include + +// NFC connection using Adafruit PN532 hardware (ISO14443A only) +class HardwareNFCConnectionPN532 : public NFCConnectionI { +public: + HardwareNFCConnectionPN532(); + ~HardwareNFCConnectionPN532() override; + + bool begin() override; + void reset() override; + bool hardwareReset() override; + bool setupRF() override; + bool detectTag(uint8_t* uid, uint8_t* uidLength) override; + uint8_t getLastSAK() const override { return lastSAK_; } + uint16_t getLastATQA() const override { return lastATQA_; } + void setCurrentUid(const uint8_t* uid, uint8_t length) override; + opt_nfc_hal_t* getHal() override; + uint16_t readISO14443Pages(uint8_t startPage, uint8_t pageCount, uint8_t* buffer, uint16_t bufferSize) override; + bool writeISO14443Pages(uint8_t startPage, uint8_t pageCount, const uint8_t* data, uint16_t dataLen) override; + void getReaderInfo(char* buf, size_t len) const override; + void logDiagnostics() override; + +private: + Adafruit_PN532* pn532_ = nullptr; + opt_nfc_hal_t hal_; + uint8_t currentUid_[10]; + uint8_t currentUidLen_ = 0; + uint8_t lastSAK_ = 0; + uint16_t lastATQA_ = 0; + uint8_t fwMajor_ = 0; + uint8_t fwMinor_ = 0; + bool ready_ = false; + + // Reactivate the currently selected tag (needed before page reads after timeout) + bool reactivateTag(); +}; + +#endif // HARDWARE_NFC_CONNECTION_PN532_H diff --git a/src/NFCConnectionI.h b/src/NFCConnectionI.h index 4157d9e..25f2d5b 100644 --- a/src/NFCConnectionI.h +++ b/src/NFCConnectionI.h @@ -2,6 +2,7 @@ #define NFC_CONNECTION_I_H #include +#include #include "openprinttag_lib.h" // Interface for NFC hardware abstraction @@ -39,6 +40,14 @@ class NFCConnectionI { // Write ISO14443A tag pages (NTAG213/215/216). Writes 4 bytes per page. // Returns true if all pages written successfully. virtual bool writeISO14443Pages(uint8_t startPage, uint8_t pageCount, const uint8_t* data, uint16_t dataLen) = 0; + + // Reader identification for diagnostics (e.g. "PN5180 v3.4", "PN532 v1.6") + virtual void getReaderInfo(char* buf, size_t len) const { + if (buf && len > 0) { strncpy(buf, "unknown", len - 1); buf[len - 1] = '\0'; } + } + + // Log hardware-specific diagnostic info (register dumps, etc.). Default: no-op. + virtual void logDiagnostics() {} }; #endif // NFC_CONNECTION_I_H diff --git a/src/NFCManager.cpp b/src/NFCManager.cpp index 17c5e76..52bbaaa 100644 --- a/src/NFCManager.cpp +++ b/src/NFCManager.cpp @@ -135,16 +135,10 @@ void NFCManager::clearGenericTagSpoolInfo() { } } -bool NFCManager::getPN5180FirmwareVersion(uint8_t fw[2]) const { -#ifndef NATIVE_TEST - if (!connection_) return false; - auto* hw = static_cast(connection_); - if (!hw->isPN5180Ready()) return false; - hw->getPN5180FirmwareVersion(fw); +bool NFCManager::getNfcReaderInfo(char* buf, size_t len) const { + if (!connection_ || !buf || len == 0) return false; + connection_->getReaderInfo(buf, len); return true; -#else - return false; -#endif } void NFCManager::pauseScanTask() { @@ -203,9 +197,7 @@ void NFCManager::scanLoop() { connection_->setupRF(); // One-time startup diagnostic -#ifndef NATIVE_TEST - static_cast(connection_)->logDiagnostics(); -#endif + connection_->logDiagnostics(); while (true) { #ifndef NATIVE_TEST diff --git a/src/NFCManager.h b/src/NFCManager.h index b6b4cb3..8eb0ddb 100644 --- a/src/NFCManager.h +++ b/src/NFCManager.h @@ -72,8 +72,8 @@ class NFCManager { bool getCurrentSpoolState(CurrentSpoolState& out); bool getLastTigerTagData(TigerTagData& out); bool getLastOpenTag3DData(opentag3d_t& out); - // Returns PN5180 firmware version. fw[0]=minor, fw[1]=major. Returns false if not available. - bool getPN5180FirmwareVersion(uint8_t fw[2]) const; + // Returns reader identification string (e.g. "PN5180 v3.4", "PN532 v1.6") + bool getNfcReaderInfo(char* buf, size_t len) const; void pauseScanTask(); void resumeScanTask(); diff --git a/src/TroubleshootingHTML.h b/src/TroubleshootingHTML.h index 52ba332..3c33fb3 100644 --- a/src/TroubleshootingHTML.h +++ b/src/TroubleshootingHTML.h @@ -228,10 +228,10 @@ const char TROUBLESHOOTING_HTML[] PROGMEM = R"rawliteral( if (d.nfc) { const n = d.nfc; if (n.ok) { - setCheck('nfc', 'pass', 'NFC Reader (PN5180)', - 'Firmware v' + n.fw_major + '.' + n.fw_minor); + setCheck('nfc', 'pass', 'NFC Reader', + n.reader || 'Connected'); } else { - setCheck('nfc', 'fail', 'NFC Reader (PN5180)', + setCheck('nfc', 'fail', 'NFC Reader', 'Not responding. Check SPI wiring.'); } } diff --git a/src/WebServerManager.cpp b/src/WebServerManager.cpp index 977fbfc..66b75a4 100644 --- a/src/WebServerManager.cpp +++ b/src/WebServerManager.cpp @@ -443,11 +443,10 @@ void WebServerManager::handleApiDiagnostics() { // NFC reader JsonObject nfc = doc.createNestedObject("nfc"); - uint8_t fw[2] = {0, 0}; - bool nfcOk = NFCManager::getInstance().getPN5180FirmwareVersion(fw); - nfc["ok"] = nfcOk; - nfc["fw_major"] = fw[1]; - nfc["fw_minor"] = fw[0]; + char readerInfo[32] = {0}; + bool nfcOk = NFCManager::getInstance().getNfcReaderInfo(readerInfo, sizeof(readerInfo)); + nfc["ok"] = nfcOk; + nfc["reader"] = readerInfo; // Memory JsonObject memory = doc.createNestedObject("memory"); @@ -491,6 +490,7 @@ void WebServerManager::handleApiGetConfig() { doc["prusalink_on"] = cfg.prusalink_on; doc["prusalink_url"] = cfg.prusalink_url; doc["prusalink_key_set"] = (cfg.prusalink_api_key[0] != '\0'); + doc["nfc_reader"] = cfg.nfc_reader; String body; serializeJson(doc, body); @@ -526,6 +526,9 @@ void WebServerManager::handleApiPostConfig() { update.prusalink_on = doc["prusalink_on"] | (uint8_t)0; strncpy(update.prusalink_url, doc["prusalink_url"] | "", sizeof(update.prusalink_url) - 1); strncpy(update.prusalink_api_key, doc["prusalink_api_key"] | "", sizeof(update.prusalink_api_key) - 1); + const char* nfcVal = doc["nfc_reader"] | "pn5180"; + if (strcmp(nfcVal, "pn532") != 0) nfcVal = "pn5180"; // only allow known values + strncpy(update.nfc_reader, nfcVal, sizeof(update.nfc_reader) - 1); if (update.wifi_ssid[0] == '\0') { sendError(400, "WiFi SSID is required"); diff --git a/src/main.cpp b/src/main.cpp index 30d9924..c906051 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,5 +1,6 @@ #include #include +#include #include "ConfigurationManager.h" #include "ApplicationManager.h" @@ -12,6 +13,7 @@ #include "PrinterManager.h" #include "PrusaLinkStrategy.h" #include "InputManager.h" +#include "HardwareNFCConnectionPN532.h" #include "BoardPins.h" #include @@ -168,6 +170,15 @@ void setup() { mode == 0 ? "SELF_DIRECTED" : "CONTROLLED_BY_HA"); } + // Select NFC reader based on NVS config + const char* nfcReader = config.getNfcReader(); + if (strcmp(nfcReader, "pn532") == 0) { + Serial.println("NFC reader: PN532 (ISO14443A only)"); + NFCManager::getInstance().setConnection(new HardwareNFCConnectionPN532()); + } else { + Serial.printf("NFC reader: %s (default)\n", nfcReader); + } + // Initialize NFCManager if (!NFCManager::getInstance().begin()) { Serial.println("NFCManager init failed - halting"); From d6324559544a25b6c08b12db256929085f76c399 Mon Sep 17 00:00:00 2001 From: Shane Jordan Date: Sat, 28 Mar 2026 07:33:31 -0500 Subject: [PATCH 2/3] fix: check SAMConfig() return values in PN532 reset methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit finding — SAMConfig() failures were silently ignored in reset() and hardwareReset(). Now logs on soft reset failure and returns false on hard reset failure. --- src/HardwareNFCConnectionPN532.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/HardwareNFCConnectionPN532.cpp b/src/HardwareNFCConnectionPN532.cpp index 2b54465..656906f 100644 --- a/src/HardwareNFCConnectionPN532.cpp +++ b/src/HardwareNFCConnectionPN532.cpp @@ -64,7 +64,9 @@ bool HardwareNFCConnectionPN532::begin() { void HardwareNFCConnectionPN532::reset() { if (pn532_) { pn532_->begin(); - pn532_->SAMConfig(); + if (!pn532_->SAMConfig()) { + Serial.println("PN532: SAMConfig failed during reset"); + } } } @@ -80,7 +82,7 @@ bool HardwareNFCConnectionPN532::hardwareReset() { pn532_->begin(); uint32_t ver = pn532_->getFirmwareVersion(); if (!ver) return false; - pn532_->SAMConfig(); + if (!pn532_->SAMConfig()) return false; } return true; } From 56557a034569fa4394afb95fb9d8c2dceb28fbc1 Mon Sep 17 00:00:00 2001 From: Shane Jordan Date: Sat, 28 Mar 2026 07:48:10 -0500 Subject: [PATCH 3/3] docs: add v1.5.9 changelog entry for PN532, keypad, and LED fixes --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a05187..f5d90d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## [1.5.9] - 2026-03-28 + +### Added + +- **PN532 NFC reader support** — Adafruit PN532 added as a second NFC reader option (ISO14443A only). NVS key `nfc_reader` selects which reader initializes at boot. Supports GenericUidTag, TigerTag, BambuTag (UID), and OpenPrintTag on NTAG tags. No ISO15693 (ICODE SLIX2) support. +- **3x4 matrix keypad support** — scan a spool, type a tool number, press # to send ASSIGN_SPOOL to Moonraker. Controlled by NVS `keypad_on` flag. Includes LCD feedback during entry. +- **Moonraker URL configuration** — configurable via web UI and installer for keypad tool assignment. +- **NFC reader selection in web UI** — dropdown in Hardware config section with PN5180/PN532 options. + +### Fixed + +- **LED black spool color** — black (0,0,0) filament color now substitutes dim white (0x33) so the LED is visibly lit instead of appearing off. +- **NFC abstraction leak** — removed `static_cast` from NFCManager. Reader identification and diagnostics now use virtual methods (`getReaderInfo()`, `logDiagnostics()`) on the NFCConnectionI interface. +- **Troubleshooting page** — NFC reader info is now reader-agnostic (shows "PN5180 v3.4" or "PN532 v1.6" instead of hardcoded PN5180 label). + +--- + ## [1.5.8] - 2026-03-27 ### Fixed