Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/ApplicationManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ void ApplicationManager::handleSpoolDetected(const AppMessage& msg) {
else if (strcmp(s.tag_format, "TigerTag") == 0) spool.tagType = 2;
else if (strcmp(s.tag_format, "OpenTag3D") == 0) spool.tagType = 3;
else if (strcmp(s.tag_format, "BambuTag") == 0) spool.tagType = 4;
else if (strcmp(s.tag_format, "OpenSpool") == 0) spool.tagType = 6;
else spool.tagType = 0;
display_->showSpool(spool);
} else if (display_) {
Expand Down
1 change: 1 addition & 0 deletions src/ConfigHTML.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const char CONFIG_HTML[] PROGMEM = R"rawliteral(
<a href="/writer/openprinttag">OpenPrintTag</a>
<a href="/writer/tigertag">TigerTag</a>
<a href="/writer/opentag3d">OpenTag3D</a>
<a href="/writer/openspool">OpenSpool</a>
<a href="/register/uid">NFC+</a>
<a href="/update">Update</a>
<a href="/troubleshooting">Troubleshooting</a>
Expand Down
2 changes: 1 addition & 1 deletion src/DisplayI.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ struct DisplaySpoolData {
char colorHex[8]; // "RRGGBB" no leading #
float remainingWeight; // grams
float totalWeight; // grams
uint8_t tagType; // 0=unknown, 1=OpenPrintTag, 2=TigerTag, 3=OpenTag3D, 4=Bambu, 5=NFC+
uint8_t tagType; // 0=unknown, 1=OpenPrintTag, 2=TigerTag, 3=OpenTag3D, 4=Bambu, 5=NFC+, 6=OpenSpool
};

// Display interface — implemented by LCDManager and TFTManager.
Expand Down
7 changes: 7 additions & 0 deletions src/LandingHTML.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const char LANDING_HTML[] PROGMEM = R"rawliteral(
<a href="/writer/openprinttag">OpenPrintTag</a>
<a href="/writer/tigertag">TigerTag</a>
<a href="/writer/opentag3d">OpenTag3D</a>
<a href="/writer/openspool">OpenSpool</a>
<a href="/register/uid">NFC+</a>
<a href="/update">Update</a>
<a href="/troubleshooting">Troubleshooting</a>
Expand Down Expand Up @@ -71,6 +72,12 @@ const char LANDING_HTML[] PROGMEM = R"rawliteral(
<div class="tool-desc">Write filament data to NTAG215/216 tags using the OpenTag3D format.</div>
</a>

<a href="/writer/openspool" class="tool-card">
<img src="/img/openspool.png" alt="OpenSpool" style="height:48px;border-radius:6px" />
<div class="tool-name">OpenSpool Writer</div>
<div class="tool-desc">Write filament data to NTAG215/216 tags using the OpenSpool format.</div>
</a>

<a href="/register/uid" class="tool-card">
<div class="tool-icon">&#128179;</div>
<div class="tool-name">NFC+ Registration</div>
Expand Down
208 changes: 207 additions & 1 deletion src/NFCManager.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "NFCManager.h"
#include "ConversionUtils.h"
#include "TigerTagParser.h"
#include "OpenSpoolParser.h"
#ifndef NATIVE_TEST
#include "ApplicationManager.h"
#include "HardwareNFCConnection.h"
Expand Down Expand Up @@ -112,6 +113,15 @@ bool NFCManager::getLastOpenTag3DData(opentag3d_t& out) {
return valid;
}

bool NFCManager::getLastOpenSpoolData(OpenSpoolData& out) {
if (tagMutex == nullptr) return false;
if (xSemaphoreTake(tagMutex, pdMS_TO_TICKS(50)) != pdTRUE) return false;
bool valid = lastOpenSpoolValid_;
if (valid) out = lastOpenSpool_;
xSemaphoreGive(tagMutex);
return valid;
}

void NFCManager::setGenericTagSpoolInfo(const GenericTagSpoolInfo& info) {
if (tagMutex && xSemaphoreTake(tagMutex, pdMS_TO_TICKS(50)) == pdTRUE) {
lastGenericTagSpoolInfo_ = info;
Expand Down Expand Up @@ -310,10 +320,12 @@ void NFCManager::scanLoop() {
Serial.printf("NFCManager: Bambu Lab tag — UID=%s (encrypted, no data access)\n", scan.uid_hex);
sendGenericTagMessage();
} else if (scan.kind == TagKind::GenericUidTag) {
// ISO14443A tag — try TigerTag, then OpenTag3D, fall back to UID-only
// ISO14443A tag — try TigerTag, then OpenTag3D, then OpenSpool, fall back to UID-only
bool isTigerTag = false;
bool isOpenTag3D = false;
bool isOpenSpool = false;
TigerTagData tigerData;
OpenSpoolData openSpoolData;
opentag3d_t ot3dData;
memset(&tigerData, 0, sizeof(tigerData));
memset(&ot3dData, 0, sizeof(ot3dData));
Expand Down Expand Up @@ -414,6 +426,51 @@ void NFCManager::scanLoop() {
}
}
}

// Check for OpenSpool (application/json + "protocol":"openspool")
if (!isOpenTag3D) {
const char* jsonMime = "application/json";
size_t jsonMimeLen = strlen(jsonMime);
if (typeLen == jsonMimeLen &&
memcmp(pageData + ndefStart + headerSize, jsonMime, jsonMimeLen) == 0) {
uint32_t osPayloadLen = 0;
if (sr) {
osPayloadLen = pageData[ndefStart + 2];
} else {
osPayloadLen = ((uint32_t)pageData[ndefStart + 2] << 24) |
((uint32_t)pageData[ndefStart + 3] << 16) |
((uint32_t)pageData[ndefStart + 4] << 8) |
pageData[ndefStart + 5];
}
uint16_t osOffset = ndefStart + headerSize + typeLen;
uint8_t osPayload[256] = {0};
uint16_t osBytes = 0;

if (osOffset + osPayloadLen <= bytesRead) {
osBytes = (uint16_t)osPayloadLen;
if (osBytes > sizeof(osPayload)) osBytes = sizeof(osPayload);
memcpy(osPayload, pageData + osOffset, osBytes);
} else {
uint8_t osStartPage = 4 + (osOffset / 4);
uint16_t osPagesNeeded = (uint16_t)((osPayloadLen + 3) / 4) + 1;
if (osPagesNeeded > 50) osPagesNeeded = 50;
uint8_t osExtData[256] = {0};
uint16_t osExtRead = connection_->readISO14443Pages(
osStartPage, (uint8_t)osPagesNeeded, osExtData, sizeof(osExtData));
uint16_t osOffInPage = osOffset % 4;
if (osExtRead > osOffInPage) {
osBytes = osExtRead - osOffInPage;
if (osBytes > osPayloadLen) osBytes = (uint16_t)osPayloadLen;
if (osBytes > sizeof(osPayload)) osBytes = sizeof(osPayload);
memcpy(osPayload, osExtData + osOffInPage, osBytes);
}
}

if (osBytes > 0 && parseOpenSpool(osPayload, osBytes, openSpoolData)) {
isOpenSpool = true;
}
}
}
Comment on lines +429 to +473
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Deep nesting increases cognitive complexity.

The OpenSpool detection logic adds another level of nesting (up to 13 levels per static analysis). Consider extracting the OpenSpool NDEF check into a helper function to match the structure of tigerTagCheckMagic and improve maintainability.

🧰 Tools
🪛 Clang (14.0.6)

[note] 422-422: +10, including nesting penalty of 9, nesting level increased to 10

(clang)


[note] 425-425: +11, including nesting penalty of 10, nesting level increased to 11

(clang)


[note] 428-428: +12, including nesting penalty of 11, nesting level increased to 12

(clang)


[note] 430-430: +1, nesting level increased to 12

(clang)


[note] 440-440: +12, including nesting penalty of 11, nesting level increased to 12

(clang)


[note] 444-444: +1, nesting level increased to 12

(clang)


[note] 452-452: +13, including nesting penalty of 12, nesting level increased to 13

(clang)


[note] 460-460: +12, including nesting penalty of 11, nesting level increased to 12

(clang)


[warning] 424-424: variable 'jsonMimeLen' is not initialized

(cppcoreguidelines-init-variables)


[warning] 427-427: variable 'osPayloadLen' is not initialized

(cppcoreguidelines-init-variables)


[warning] 428-428: if with identical then and else branches

(bugprone-branch-clone)


[note] 430-430: else branch starts here

(clang)


[warning] 436-436: variable 'osOffset' is not initialized

(cppcoreguidelines-init-variables)


[warning] 438-438: variable 'osBytes' is not initialized

(cppcoreguidelines-init-variables)


[warning] 445-445: variable 'osStartPage' is not initialized

(cppcoreguidelines-init-variables)


[warning] 446-446: variable 'osPagesNeeded' is not initialized

(cppcoreguidelines-init-variables)


[warning] 449-449: variable 'osExtRead' is not initialized

(cppcoreguidelines-init-variables)


[warning] 451-451: variable 'osOffInPage' is not initialized

(cppcoreguidelines-init-variables)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/NFCManager.cpp` around lines 420 - 464, Extract the nested OpenSpool NDEF
detection block into a helper function (e.g., bool checkOpenSpoolNdef(const
uint8_t* pageData, size_t bytesRead, size_t ndefStart, size_t headerSize, size_t
typeLen, bool sr, /*out*/ OpenSpoolType& openSpoolData)) that encapsulates the
MIME check for "application/json", payload length computation (short vs long
record), page boundary handling using connection_->readISO14443Pages, buffer
bounds checks, and the call to parseOpenSpool; then replace the large nested
block in NFCManager.cpp with a single call that sets isOpenSpool =
checkOpenSpoolNdef(pageData, bytesRead, ndefStart, headerSize, typeLen, sr,
openSpoolData). Ensure the helper uses the same symbols (pageData, ndefStart,
headerSize, typeLen, sr, bytesRead, connection_->readISO14443Pages,
parseOpenSpool, openSpoolData) and preserves existing size limits and fallback
logic.

}
}
break;
Expand Down Expand Up @@ -449,14 +506,26 @@ void NFCManager::scanLoop() {
lastOpenTag3D_ = ot3dData;
lastOpenTag3DValid_ = true;
lastTigerTagValid_ = false;
lastOpenSpoolValid_ = false;
Serial.printf("NFCManager: OpenTag3D detected — %s %s %.2fmm %ug\n",
ot3dData.manufacturer, ot3dData.base_material,
opentag3d_diameter_mm(&ot3dData), ot3dData.target_weight_g);
} else if (isOpenSpool) {
currentSpool.kind = TagKind::OpenSpoolTag;
currentSpool.tag_data_valid = false;
lastOpenSpool_ = openSpoolData;
lastOpenSpoolValid_ = true;
lastTigerTagValid_ = false;
lastOpenTag3DValid_ = false;
Serial.printf("NFCManager: OpenSpool detected — %s %s #%s\n",
openSpoolData.brand, openSpoolData.material,
openSpoolData.color_hex);
} else {
currentSpool.kind = TagKind::GenericUidTag;
currentSpool.tag_data_valid = false;
lastTigerTagValid_ = false;
lastOpenTag3DValid_ = false;
lastOpenSpoolValid_ = false;
}
xSemaphoreGive(tagMutex);
} else {
Expand All @@ -467,6 +536,8 @@ void NFCManager::scanLoop() {
sendTigerTagMessage(tigerData);
} else if (isOpenTag3D) {
sendOpenTag3DMessage(ot3dData);
} else if (isOpenSpool) {
sendOpenSpoolMessage(currentSpool.spool_id, openSpoolData);
} else {
sendGenericTagMessage();
}
Expand Down Expand Up @@ -1093,6 +1164,56 @@ void NFCManager::sendOpenTag3DMessage(const opentag3d_t& ot3d) {
ApplicationManager::getInstance().sendMessage(msg);
}

void NFCManager::sendOpenSpoolMessage(const char* uid, const OpenSpoolData& os) {
AppMessage msg;
msg.type = AppMessageType::SPOOL_DETECTED;
auto& s = msg.payload.spoolDetected;
memset(&s, 0, sizeof(s));
Comment on lines +1167 to +1171
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Use member initializer instead of memset on struct containing floats.

memset on a struct containing floating-point members (kg_remaining, density, diameter, initial_weight_g) may produce invalid bit patterns on some platforms. While typically safe on ESP32/IEEE 754, explicit zero-initialization is cleaner.

Proposed fix
 void NFCManager::sendOpenSpoolMessage(const char* uid, const OpenSpoolData& os) {
     AppMessage msg;
     msg.type = AppMessageType::SPOOL_DETECTED;
-    auto& s = msg.payload.spoolDetected;
-    memset(&s, 0, sizeof(s));
+    msg.payload.spoolDetected = {};  // Value-initialize all members
+    auto& s = msg.payload.spoolDetected;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void NFCManager::sendOpenSpoolMessage(const char* uid, const OpenSpoolData& os) {
AppMessage msg;
msg.type = AppMessageType::SPOOL_DETECTED;
auto& s = msg.payload.spoolDetected;
memset(&s, 0, sizeof(s));
void NFCManager::sendOpenSpoolMessage(const char* uid, const OpenSpoolData& os) {
AppMessage msg;
msg.type = AppMessageType::SPOOL_DETECTED;
msg.payload.spoolDetected = {}; // Value-initialize all members
auto& s = msg.payload.spoolDetected;
🧰 Tools
🪛 Clang (14.0.6)

[note] 1158-1158: the definition seen here

(clang)


[warning] 1158-1158: method 'sendOpenSpoolMessage' can be made static

(readability-convert-member-functions-to-static)


[warning] 1158-1158: parameter name 'os' is too short, expected at least 3 characters

(readability-identifier-length)


[warning] 1161-1161: variable name 's' is too short, expected at least 3 characters

(readability-identifier-length)

🪛 Cppcheck (2.20.0)

[portability] 1162-1162: Using memset() on struct which contains a floating point number.

(memsetClassFloat)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/NFCManager.cpp` around lines 1158 - 1162, The code uses memset(&s, 0,
sizeof(s)) to clear the AppMessage::payload.spoolDetected struct inside
NFCManager::sendOpenSpoolMessage; replace that with proper value-initialization
(e.g., assign an empty initializer or default-constructed instance to s) so
floating-point members like kg_remaining, density, diameter, initial_weight_g
are set to 0.0 safely and portably; update the function to use s = {} or s =
SpoolDetected{} (matching the actual struct/type name) instead of memset.


strncpy(s.spool_id, uid, sizeof(s.spool_id) - 1);
strncpy(s.manufacturer, os.brand, sizeof(s.manufacturer) - 1);
strncpy(s.material_name, os.material, sizeof(s.material_name) - 1);

// Material type lookup
s.material_type = 0;
if (strcasecmp(os.material, "PLA") == 0) s.material_type = OPT_MATERIAL_TYPE_PLA;
else if (strcasecmp(os.material, "PETG") == 0) s.material_type = OPT_MATERIAL_TYPE_PETG;
else if (strcasecmp(os.material, "ABS") == 0) s.material_type = OPT_MATERIAL_TYPE_ABS;
else if (strcasecmp(os.material, "ASA") == 0) s.material_type = OPT_MATERIAL_TYPE_ASA;
else if (strcasecmp(os.material, "TPU") == 0) s.material_type = OPT_MATERIAL_TYPE_TPU;
else if (strcasecmp(os.material, "PA") == 0 || strcasecmp(os.material, "Nylon") == 0) s.material_type = OPT_MATERIAL_TYPE_PA6;
else if (strcasecmp(os.material, "PC") == 0) s.material_type = OPT_MATERIAL_TYPE_PC;

Comment on lines +1177 to +1186
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't silently map unsupported OpenSpool materials to PLA.

This branch recognizes far fewer material names than the TigerTag/OpenTag3D paths. Anything outside this list leaves material_type at its default value, so valid OpenSpool materials can be serialized downstream as PLA. This file already uses a shared string-to-material mapper later on; reusing that here would keep the formats aligned and avoid drift.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/NFCManager.cpp` around lines 1168 - 1177, The manual strcasecmp chain
that sets s.material_type from os.material should be replaced with the shared
string-to-material mapper used elsewhere in this file (call that existing mapper
to convert os.material to the enum) so you no longer silently default unknown
OpenSpool names to PLA; remove the if/else block around s.material_type, call
the common mapper with os.material to assign s.material_type, and ensure the
mapper’s fallback for unsupported names yields an explicit "unknown/none" value
(e.g., 0 or OPT_MATERIAL_TYPE_UNKNOWN) rather than treating them as PLA.

// Parse color hex to RGB
if (strlen(os.color_hex) == 6) {
unsigned int r, g, b;
if (sscanf(os.color_hex, "%02x%02x%02x", &r, &g, &b) == 3) {
s.primary_color[0] = (uint8_t)r;
s.primary_color[1] = (uint8_t)g;
s.primary_color[2] = (uint8_t)b;
s.primary_color[3] = 255;
s.has_color = true;
}
}

s.min_print_temp = os.min_temp;
s.max_print_temp = os.max_temp;

strncpy(s.tag_format, "OpenSpool", sizeof(s.tag_format) - 1);
s.spoolman_id = -1;

Serial.println("--- OpenSpool SpoolDetected payload ---");
Serial.printf(" uid: %s\n", s.spool_id);
Serial.printf(" brand: %s\n", s.manufacturer);
Serial.printf(" material: %s\n", s.material_name);
Serial.printf(" color: #%s\n", os.color_hex);
Serial.printf(" nozzle: %d-%d°C\n", s.min_print_temp, s.max_print_temp);
Serial.printf(" version: %s\n", os.version);
Serial.println("---------------------------------------");

ApplicationManager::getInstance().sendMessage(msg);
}

TagScanResult NFCManager::classifyTag(const uint8_t* uid, uint8_t uid_length) {
TagScanResult result;
result.present = true;
Expand Down Expand Up @@ -1651,6 +1772,91 @@ bool NFCManager::executeWrite(const NFCWriteRequest& request) {
return ok;
}

// Handle WRITE_OPENSPOOL — write NDEF-wrapped JSON payload to NTAG pages
if (request.type == NFCWriteType::WRITE_OPENSPOOL) {
if (xSemaphoreTake(tagMutex, pdMS_TO_TICKS(100)) != pdTRUE) {
Serial.println("NFCManager: WRITE_OPENSPOOL - could not acquire tagMutex");
return false;
}
if (request.expected_spool_id[0] != '\0' &&
strcmp(currentSpool.spool_id, request.expected_spool_id) != 0) {
xSemaphoreGive(tagMutex);
Serial.println("NFCManager: WRITE_OPENSPOOL rejected - UID mismatch");
return false;
}
xSemaphoreGive(tagMutex);

if (!rawWritePending_ || rawWriteBufferSize_ == 0) {
Serial.println("NFCManager: WRITE_OPENSPOOL - no raw data available");
return false;
}

const uint8_t* jsonPayload = rawWriteBuffer_;
uint16_t payloadLen = (uint16_t)rawWriteBufferSize_;
rawWritePending_ = false;

const char* mime = "application/json";
uint8_t mimeLen = (uint8_t)strlen(mime);
bool sr = (payloadLen <= 255);
uint8_t ndefHeaderSize = 2 + (sr ? 1 : 4);
uint16_t ndefRecordLen = ndefHeaderSize + mimeLen + payloadLen;

bool longTlv = (ndefRecordLen > 254);
uint16_t tlvHeaderSize = 1 + (longTlv ? 3 : 1);
uint16_t totalSize = tlvHeaderSize + ndefRecordLen + 1;

uint8_t ndefBuf[256];
if (totalSize > sizeof(ndefBuf)) {
Serial.printf("NFCManager: WRITE_OPENSPOOL - NDEF too large (%u bytes)\n", totalSize);
return false;
}

uint16_t idx = 0;
ndefBuf[idx++] = 0x03;
if (longTlv) {
ndefBuf[idx++] = 0xFF;
ndefBuf[idx++] = (uint8_t)(ndefRecordLen >> 8);
ndefBuf[idx++] = (uint8_t)(ndefRecordLen & 0xFF);
} else {
ndefBuf[idx++] = (uint8_t)ndefRecordLen;
}

uint8_t ndefFlags = 0xC0 | 0x02;
if (sr) ndefFlags |= 0x10;
ndefBuf[idx++] = ndefFlags;
ndefBuf[idx++] = mimeLen;
if (sr) {
ndefBuf[idx++] = (uint8_t)payloadLen;
} else {
ndefBuf[idx++] = (uint8_t)((payloadLen >> 24) & 0xFF);
ndefBuf[idx++] = (uint8_t)((payloadLen >> 16) & 0xFF);
ndefBuf[idx++] = (uint8_t)((payloadLen >> 8) & 0xFF);
ndefBuf[idx++] = (uint8_t)(payloadLen & 0xFF);
}

memcpy(ndefBuf + idx, mime, mimeLen);
idx += mimeLen;
memcpy(ndefBuf + idx, jsonPayload, payloadLen);
idx += payloadLen;
ndefBuf[idx++] = 0xFE;

while (idx % 4 != 0) ndefBuf[idx++] = 0x00;

uint8_t pagesNeeded = (uint8_t)(idx / 4);
Serial.printf("NFCManager: WRITE_OPENSPOOL - writing %u bytes (%u pages)\n", idx, pagesNeeded);
bool ok = connection_->writeISO14443Pages(4, pagesNeeded, ndefBuf, idx);
if (ok) {
Serial.printf("NFCManager: WRITE_OPENSPOOL succeeded (%u bytes, %u pages)\n", idx, pagesNeeded);
if (xSemaphoreTake(tagMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
currentSpool.present = false;
xSemaphoreGive(tagMutex);
}
} else {
Serial.println("NFCManager: WRITE_OPENSPOOL failed");
}
return ok;
}

// Handle WRITE_ATOMIC — build complete CBOR map from sidecar fields, write once
if (request.type == NFCWriteType::WRITE_ATOMIC) {
if (!atomicWriteFields_.pending) {
Expand Down
5 changes: 5 additions & 0 deletions src/NFCManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include "NFCTypes.h"
#include "NFCConnectionI.h"
#include "TigerTagParser.h"
#include "OpenSpoolParser.h"
#include "opentag3d_lib.h"

// Sidecar for WRITE_ATOMIC: filled by HTTP handler, consumed by scan task.
Expand Down Expand Up @@ -75,6 +76,7 @@ class NFCManager {
bool getCurrentSpoolState(CurrentSpoolState& out);
bool getLastTigerTagData(TigerTagData& out);
bool getLastOpenTag3DData(opentag3d_t& out);
bool getLastOpenSpoolData(OpenSpoolData& out);
// Returns reader identification string (e.g. "PN5180 v3.4", "PN532 v1.6")
bool getNfcReaderInfo(char* buf, size_t len) const;
void pauseScanTask();
Expand Down Expand Up @@ -123,6 +125,7 @@ class NFCManager {
void sendGenericTagMessage();
void sendTigerTagMessage(const TigerTagData& tt);
void sendOpenTag3DMessage(const opentag3d_t& ot3d);
void sendOpenSpoolMessage(const char* uid, const OpenSpoolData& data);
void sendTagRemovedMessage();
void processWriteQueue();
bool executeWrite(const NFCWriteRequest& request);
Expand Down Expand Up @@ -153,6 +156,8 @@ class NFCManager {
// Last parsed OpenTag3D data (retained for /api/status)
opentag3d_t lastOpenTag3D_;
bool lastOpenTag3DValid_ = false;
OpenSpoolData lastOpenSpool_;
bool lastOpenSpoolValid_ = false;

// Resolved Spoolman data for the current generic UID tag
GenericTagSpoolInfo lastGenericTagSpoolInfo_ = {};
Expand Down
1 change: 1 addition & 0 deletions src/NFCTypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ enum class TagKind : uint8_t {
OpenTag3D, // OpenTag3D format — ISO14443A
TigerTag, // TigerTag format — ISO14443A (NTAG213/215/216)
BambuTag, // Bambu Lab spool — MIFARE Classic (encrypted, UID-only)
OpenSpoolTag, // OpenSpool format — ISO14443A (NTAG215/216, NDEF JSON)
BlankTag,
Unsupported
};
Expand Down
1 change: 1 addition & 0 deletions src/NFCWriteTypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ enum class NFCWriteType : uint8_t {
SET_MAX_BED_TEMP, // Set maximum bed temperature (°C)
WRITE_TIGERTAG, // Write 40-byte TigerTag binary to NTAG pages 4-13
WRITE_OPENTAG3D, // Write NDEF-wrapped OpenTag3D payload to NTAG pages
WRITE_OPENSPOOL, // Write NDEF-wrapped OpenSpool JSON payload to NTAG pages
WRITE_ATOMIC, // Atomic single-pass write: build complete CBOR map, write once
};

Expand Down
1 change: 1 addition & 0 deletions src/OpenPrintTagWriterHTML.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const char OPENPRINTTAG_WRITER_HTML[] PROGMEM = R"rawliteral(
<a href="/writer/openprinttag">OpenPrintTag</a>
<a href="/writer/tigertag">TigerTag</a>
<a href="/writer/opentag3d">OpenTag3D</a>
<a href="/writer/openspool">OpenSpool</a>
<a href="/register/uid">NFC+</a>
<a href="/update">Update</a>
<a href="/troubleshooting">Troubleshooting</a>
Expand Down
Loading