diff --git a/src/HardwareNFCConnection.cpp b/src/HardwareNFCConnection.cpp index 4936683..2088023 100644 --- a/src/HardwareNFCConnection.cpp +++ b/src/HardwareNFCConnection.cpp @@ -463,3 +463,22 @@ void HardwareNFCConnection::logDiagnostics() { Serial.println("HardwareNFC DIAG: --- end test ---"); #endif } + +bool HardwareNFCConnection::ntagGetVersion(uint8_t* versionOut) { + if (!iso14443a_ || !versionOut) return false; + + uint8_t cmd = 0x60; + if (!iso14443a_->sendData(&cmd, 1, 0x00)) return false; + + uint32_t rxStatus; + uint16_t rxLen = 0; + for (int i = 0; i < 10; i++) { + delay(1); + iso14443a_->readRegister(RX_STATUS, &rxStatus); + rxLen = rxStatus & 0x000001ff; + if (rxLen >= 8) break; + } + if (rxLen < 8) return false; + + return iso14443a_->readData(8, versionOut) != nullptr; +} diff --git a/src/HardwareNFCConnection.h b/src/HardwareNFCConnection.h index d6fa785..86390e1 100644 --- a/src/HardwareNFCConnection.h +++ b/src/HardwareNFCConnection.h @@ -24,6 +24,7 @@ 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_; } + bool ntagGetVersion(uint8_t* versionOut) override; void getReaderInfo(char* buf, size_t len) const override; // Diagnostics: log RF_STATUS, IRQ_STATUS, SYSTEM_STATUS registers void logDiagnostics() override; diff --git a/src/HardwareNFCConnectionPN532.cpp b/src/HardwareNFCConnectionPN532.cpp index 2401458..037efb6 100644 --- a/src/HardwareNFCConnectionPN532.cpp +++ b/src/HardwareNFCConnectionPN532.cpp @@ -216,3 +216,17 @@ void HardwareNFCConnectionPN532::logDiagnostics() { Serial.println("PN532: No response during diagnostics"); } } + +bool HardwareNFCConnectionPN532::ntagGetVersion(uint8_t* versionOut) { + if (!pn532_ || !ready_ || !versionOut) return false; + + uint8_t cmd = 0x60; + uint8_t response[8]; + uint8_t responseLength = sizeof(response); + + if (!pn532_->inDataExchange(&cmd, 1, response, &responseLength)) return false; + if (responseLength < 8) return false; + + memcpy(versionOut, response, 8); + return true; +} diff --git a/src/HardwareNFCConnectionPN532.h b/src/HardwareNFCConnectionPN532.h index 61e0a60..8abd97b 100644 --- a/src/HardwareNFCConnectionPN532.h +++ b/src/HardwareNFCConnectionPN532.h @@ -18,6 +18,7 @@ class HardwareNFCConnectionPN532 : public NFCConnectionI { bool detectTag(uint8_t* uid, uint8_t* uidLength) override; uint8_t getLastSAK() const override { return lastSAK_; } uint16_t getLastATQA() const override { return lastATQA_; } + bool ntagGetVersion(uint8_t* versionOut) override; 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; diff --git a/src/NFCConnectionI.h b/src/NFCConnectionI.h index 25f2d5b..b3b35f5 100644 --- a/src/NFCConnectionI.h +++ b/src/NFCConnectionI.h @@ -27,6 +27,10 @@ class NFCConnectionI { virtual uint8_t getLastSAK() const { return 0; } virtual uint16_t getLastATQA() const { return 0; } + // NTAG GET_VERSION (0x60) — returns 8-byte version info for NTAG/Ultralight EV1. + // versionOut must point to an 8-byte buffer. Returns true on success. + virtual bool ntagGetVersion(uint8_t* versionOut) { return false; } + // Set current UID for addressed read/write commands virtual void setCurrentUid(const uint8_t* uid, uint8_t length) = 0; diff --git a/src/NFCManager.cpp b/src/NFCManager.cpp index 8247bef..4534969 100644 --- a/src/NFCManager.cpp +++ b/src/NFCManager.cpp @@ -309,6 +309,7 @@ void NFCManager::scanLoop() { currentSpool.present = true; currentSpool.blank_tag_present = false; currentSpool.kind = TagKind::BambuTag; + currentSpool.variant = NtagVariant::Unknown; currentSpool.tag_data_valid = false; lastTigerTagValid_ = false; memcpy(lastSeenUid, uid, uidLength); @@ -486,6 +487,7 @@ void NFCManager::scanLoop() { currentSpool.uid_length = uidLength; currentSpool.present = true; currentSpool.blank_tag_present = false; + currentSpool.variant = scan.variant; memcpy(lastSeenUid, uid, uidLength); lastSeenUidLength = uidLength; lastSeenValid = true; @@ -1214,10 +1216,22 @@ void NFCManager::sendOpenSpoolMessage(const char* uid, const OpenSpoolData& os) ApplicationManager::getInstance().sendMessage(msg); } +static NtagVariant mapStorageByte(uint8_t storage) { + switch (storage) { + case 0x0F: return NtagVariant::NTAG213; + case 0x11: return NtagVariant::NTAG215; + case 0x13: return NtagVariant::NTAG216; + case 0x06: return NtagVariant::UltralightEV1_48; + case 0x09: return NtagVariant::UltralightEV1_128; + default: return NtagVariant::Unknown; + } +} + TagScanResult NFCManager::classifyTag(const uint8_t* uid, uint8_t uid_length) { TagScanResult result; result.present = true; result.tag_data_valid = false; + result.variant = NtagVariant::Unknown; // Protocol inferred from UID length: ISO15693 always 8 bytes, ISO14443A always 4 or 7. if (uid_length == 8) { result.protocol = TagProtocol::ISO15693; @@ -1228,12 +1242,17 @@ TagScanResult NFCManager::classifyTag(const uint8_t* uid, uint8_t uid_length) { // Check SAK to distinguish NTAG/Ultralight from MIFARE Classic uint8_t sak = connection_->getLastSAK(); if (sak == 0x08 || sak == 0x18) { - // SAK 0x08 = MIFARE Classic 1K, 0x18 = MIFARE Classic 4K - // Likely a Bambu Lab spool tag result.kind = TagKind::BambuTag; Serial.printf("NFCManager: MIFARE Classic detected (SAK=0x%02X) — treating as Bambu tag\n", sak); } else { result.kind = TagKind::GenericUidTag; + // GET_VERSION to identify exact NTAG model + uint8_t version[8]; + if (connection_->ntagGetVersion(version)) { + result.variant = mapStorageByte(version[6]); + Serial.printf("NFCManager: %s detected (%d pages)\n", + ntagVariantName(result.variant), ntagUsablePages(result.variant)); + } } } uint8_t len = uid_length < 8 ? uid_length : 8; @@ -1589,6 +1608,17 @@ static opt_error_t applyWriteUpdate(opt_tag_t& tag, const NFCWriteRequest& reque } } +bool NFCManager::checkWriteCapacity(uint8_t startPage, uint8_t pageCount, const char* writeType) { + uint16_t maxPages = ntagUsablePages(currentSpool.variant); + if (maxPages == 0) return true; // Unknown variant — skip check + if (startPage + pageCount > maxPages) { + Serial.printf("NFCManager: %s rejected — needs %d pages (start=%d), tag has %d (%s)\n", + writeType, pageCount, startPage, maxPages, ntagVariantName(currentSpool.variant)); + return false; + } + return true; +} + bool NFCManager::executeWrite(const NFCWriteRequest& request) { // Handle FORMAT_NEW — formatNewSpool() manages its own mutex if (request.type == NFCWriteType::FORMAT_NEW) { @@ -1648,6 +1678,7 @@ bool NFCManager::executeWrite(const NFCWriteRequest& request) { xSemaphoreGive(tagMutex); // Write 40 bytes = 10 pages starting at page 4 + if (!checkWriteCapacity(4, 10, "WRITE_TIGERTAG")) return false; bool ok = connection_->writeISO14443Pages(4, 10, request.data.tigertag_data, 40); if (ok) { Serial.println("NFCManager: WRITE_TIGERTAG succeeded"); @@ -1758,6 +1789,7 @@ bool NFCManager::executeWrite(const NFCWriteRequest& request) { // Write to NTAG pages starting at page 4 uint8_t pagesNeeded = (uint8_t)(idx / 4); Serial.printf("NFCManager: WRITE_OPENTAG3D - writing %u bytes (%u pages), payload=%d\n", idx, pagesNeeded, payloadLen); + if (!checkWriteCapacity(4, pagesNeeded, "WRITE_OPENTAG3D")) return false; bool ok = connection_->writeISO14443Pages(4, pagesNeeded, ndefBuf, idx); if (ok) { Serial.printf("NFCManager: WRITE_OPENTAG3D succeeded (%u bytes, %u pages)\n", idx, pagesNeeded); @@ -1844,6 +1876,7 @@ bool NFCManager::executeWrite(const NFCWriteRequest& request) { uint8_t pagesNeeded = (uint8_t)(idx / 4); Serial.printf("NFCManager: WRITE_OPENSPOOL - writing %u bytes (%u pages)\n", idx, pagesNeeded); + if (!checkWriteCapacity(4, pagesNeeded, "WRITE_OPENSPOOL")) return false; bool ok = connection_->writeISO14443Pages(4, pagesNeeded, ndefBuf, idx); if (ok) { Serial.printf("NFCManager: WRITE_OPENSPOOL succeeded (%u bytes, %u pages)\n", idx, pagesNeeded); diff --git a/src/NFCManager.h b/src/NFCManager.h index f7d104f..5a8f5f6 100644 --- a/src/NFCManager.h +++ b/src/NFCManager.h @@ -130,6 +130,7 @@ class NFCManager { void sendTagRemovedMessage(); void processWriteQueue(); bool executeWrite(const NFCWriteRequest& request); + bool checkWriteCapacity(uint8_t startPage, uint8_t pageCount, const char* writeType); void sendSpoolUpdatedMessage(uint32_t request_id, NFCWriteType type, bool success); // Deduplication diff --git a/src/NFCTypes.h b/src/NFCTypes.h index 73e073c..480d42a 100644 --- a/src/NFCTypes.h +++ b/src/NFCTypes.h @@ -12,6 +12,37 @@ enum class TagProtocol : uint8_t { Unknown }; +enum class NtagVariant : uint8_t { + Unknown = 0, + NTAG213, // 45 pages, 144 usable bytes + NTAG215, // 135 pages, 504 usable bytes + NTAG216, // 231 pages, 888 usable bytes + UltralightEV1_48, // 20 pages, 48 usable bytes + UltralightEV1_128 // 41 pages, 128 usable bytes +}; + +inline uint16_t ntagUsablePages(NtagVariant v) { + switch (v) { + case NtagVariant::NTAG213: return 45; + case NtagVariant::NTAG215: return 135; + case NtagVariant::NTAG216: return 231; + case NtagVariant::UltralightEV1_48: return 20; + case NtagVariant::UltralightEV1_128:return 41; + default: return 0; + } +} + +inline const char* ntagVariantName(NtagVariant v) { + switch (v) { + case NtagVariant::NTAG213: return "NTAG213"; + case NtagVariant::NTAG215: return "NTAG215"; + case NtagVariant::NTAG216: return "NTAG216"; + case NtagVariant::UltralightEV1_48: return "Ultralight EV1 48B"; + case NtagVariant::UltralightEV1_128:return "Ultralight EV1 128B"; + default: return "Unknown"; + } +} + enum class TagKind : uint8_t { OpenPrintTag, // ordinal 0 — memset to zero produces safe default GenericUidTag, // UID-only tag (e.g. NTAG215) — ISO14443A @@ -26,6 +57,7 @@ enum class TagKind : uint8_t { struct TagScanResult { TagProtocol protocol; TagKind kind; + NtagVariant variant; char uid_hex[17]; // null-terminated UID hex string (up to 8 bytes = 16 hex chars) bool present; bool tag_data_valid; @@ -35,6 +67,7 @@ struct CurrentSpoolState { bool present; bool blank_tag_present; // Deprecated: use kind == TagKind::BlankTag instead TagKind kind; + NtagVariant variant; char spool_id[17]; uint8_t uid[8]; // ISO15693 uses 8-byte UID uint8_t uid_length; diff --git a/src/WebServerManager.cpp b/src/WebServerManager.cpp index 2349920..02a919d 100644 --- a/src/WebServerManager.cpp +++ b/src/WebServerManager.cpp @@ -1047,6 +1047,10 @@ void WebServerManager::handleApiStatus() { doc["uid"] = state.spool_id; doc["tag_data_valid"] = state.tag_data_valid; doc["tag_kind"] = tagKindToString(state.kind); + if (state.variant != NtagVariant::Unknown) { + doc["ntag_variant"] = ntagVariantName(state.variant); + doc["ntag_pages"] = ntagUsablePages(state.variant); + } if (state.kind == TagKind::TigerTag) { // TigerTag — include parsed TigerTag data