diff --git a/src/ConfigHTML.h b/src/ConfigHTML.h index f98d3e5..ddfd7c1 100644 --- a/src/ConfigHTML.h +++ b/src/ConfigHTML.h @@ -49,6 +49,7 @@ const char CONFIG_HTML[] PROGMEM = R"rawliteral( OpenPrintTag TigerTag OpenTag3D + NFC+ Update Troubleshooting Config diff --git a/src/LandingHTML.h b/src/LandingHTML.h index bedee06..6a56696 100644 --- a/src/LandingHTML.h +++ b/src/LandingHTML.h @@ -21,6 +21,7 @@ const char LANDING_HTML[] PROGMEM = R"rawliteral( OpenPrintTag TigerTag OpenTag3D + NFC+ Update Troubleshooting Config @@ -70,6 +71,12 @@ const char LANDING_HTML[] PROGMEM = R"rawliteral(
Write filament data to NTAG215/216 tags using the OpenTag3D format.
+ +
💳
+
NFC+ Registration
+
Register a plain NFC tag in Spoolman using its UID. No data written to the tag.
+
+
Firmware Update
diff --git a/src/OpenPrintTagWriterHTML.h b/src/OpenPrintTagWriterHTML.h index 758c2ab..ec55c5e 100644 --- a/src/OpenPrintTagWriterHTML.h +++ b/src/OpenPrintTagWriterHTML.h @@ -25,6 +25,7 @@ const char OPENPRINTTAG_WRITER_HTML[] PROGMEM = R"rawliteral(
OpenPrintTag TigerTag OpenTag3D + NFC+ Update Troubleshooting Config diff --git a/src/OpenTag3DWriterHTML.h b/src/OpenTag3DWriterHTML.h index d90f250..fe533bf 100644 --- a/src/OpenTag3DWriterHTML.h +++ b/src/OpenTag3DWriterHTML.h @@ -21,6 +21,7 @@ const char OPENTAG3D_WRITER_HTML[] PROGMEM = R"rawliteral( OpenPrintTag TigerTag OpenTag3D + NFC+ Update Troubleshooting Config diff --git a/src/ReaderHTML.h b/src/ReaderHTML.h index 68a421f..ac1494e 100644 --- a/src/ReaderHTML.h +++ b/src/ReaderHTML.h @@ -21,6 +21,7 @@ const char READER_HTML[] PROGMEM = R"rawliteral( OpenPrintTag TigerTag OpenTag3D + NFC+ Update Troubleshooting Config diff --git a/src/TigerTagWriterHTML.h b/src/TigerTagWriterHTML.h index 35206c0..7ec8312 100644 --- a/src/TigerTagWriterHTML.h +++ b/src/TigerTagWriterHTML.h @@ -21,6 +21,7 @@ const char TIGERTAG_WRITER_HTML[] PROGMEM = R"rawliteral( OpenPrintTag TigerTag OpenTag3D + NFC+ Update Troubleshooting Config diff --git a/src/TroubleshootingHTML.h b/src/TroubleshootingHTML.h index 9b32439..52ba332 100644 --- a/src/TroubleshootingHTML.h +++ b/src/TroubleshootingHTML.h @@ -67,6 +67,7 @@ const char TROUBLESHOOTING_HTML[] PROGMEM = R"rawliteral( OpenPrintTag TigerTag OpenTag3D + NFC+ Update Troubleshooting Config diff --git a/src/UIDRegistrationHTML.h b/src/UIDRegistrationHTML.h new file mode 100644 index 0000000..9c825f8 --- /dev/null +++ b/src/UIDRegistrationHTML.h @@ -0,0 +1,369 @@ +#pragma once + +// UID Registration page served at GET /register/uid +// +// API endpoints available to the page: +// GET /api/status — current tag state JSON (includes uid, spoolman_url) +// (Spoolman API calls are made directly from the browser to the configured Spoolman instance) + +const char UID_REGISTRATION_HTML[] PROGMEM = R"rawliteral( + + + + + + UID Registration — SpoolSense + + + +
+ + +
+
+
+

Register Spool in Spoolman

+

Create a Spoolman spool entry for a plain NFC tag. No data is written to the tag — it's used as a UID identifier only.

+
+
+ +
+
+
Tag UID
+
Place tag & click Read
+
+ +
+
+

Basic

+
+
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
Material name is forced to ALL CAPS.
+
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+
Pick a color or type a hex value.
+
+
+
+ +
+ + + +
+ +
+ + + +
+ +
+ +
This creates a spool entry in Spoolman with the tag's UID as the identifier. No data is written to the tag.
+
+
+
+ + +
+ + + + + +)rawliteral"; diff --git a/src/UpdateHTML.h b/src/UpdateHTML.h index c42a700..42a8ba8 100644 --- a/src/UpdateHTML.h +++ b/src/UpdateHTML.h @@ -85,6 +85,7 @@ const char UPDATE_HTML[] PROGMEM = R"rawliteral( OpenPrintTag TigerTag OpenTag3D + NFC+ Update Troubleshooting Config diff --git a/src/WebServerManager.cpp b/src/WebServerManager.cpp index 3958fc7..6b3d95e 100644 --- a/src/WebServerManager.cpp +++ b/src/WebServerManager.cpp @@ -18,6 +18,7 @@ #include "SharedJS.h" #include "ConfigHTML.h" #include "TroubleshootingHTML.h" +#include "UIDRegistrationHTML.h" #include "OpenPrintTagLogo.h" #include "TigerTagLogo.h" #include "UpdateHTML.h" @@ -78,6 +79,7 @@ bool WebServerManager::begin(uint16_t port) { _server.on("/update", HTTP_GET, [this]() { handleUpdatePage(); }); _server.on("/config", HTTP_GET, [this]() { handleConfigPage(); }); _server.on("/troubleshooting", HTTP_GET, [this]() { handleTroubleshootingPage(); }); + _server.on("/register/uid", HTTP_GET, [this]() { handleUIDRegistrationPage(); }); // API _server.on("/api/version", HTTP_GET, [this]() { handleApiVersion(); }); @@ -94,6 +96,7 @@ bool WebServerManager::begin(uint16_t port) { _server.on("/api/format-tag", HTTP_POST, [this]() { handleApiFormatTag(); }); _server.on("/api/write-tigertag", HTTP_POST, [this]() { handleApiWriteTigerTag(); }); _server.on("/api/write-opentag3d", HTTP_POST, [this]() { handleApiWriteOpenTag3D(); }); + _server.on("/api/register-uid", HTTP_POST, [this]() { handleApiRegisterUid(); }); // Allow browser preflight requests (CORS) so the page can be tested // from a local file during development. @@ -186,6 +189,191 @@ void WebServerManager::handleTroubleshootingPage() { _server.send_P(200, "text/html", TROUBLESHOOTING_HTML); } +void WebServerManager::handleUIDRegistrationPage() { + _server.sendHeader("Access-Control-Allow-Origin", "*"); + _server.send_P(200, "text/html", UID_REGISTRATION_HTML); +} + +void WebServerManager::handleApiRegisterUid() { + Serial.println("WebServerManager: POST /api/register-uid received"); + _server.sendHeader("Access-Control-Allow-Origin", "*"); + + StaticJsonDocument<1024> doc; + DeserializationError err = deserializeJson(doc, _server.arg("plain")); + if (err) { + sendError(400, "Invalid JSON"); + return; + } + + const char* uid = doc["uid"] | ""; + const char* manufacturer = doc["manufacturer"] | ""; + const char* material = doc["material"] | "PLA"; + const char* materialName = doc["material_name"] | ""; + const char* color = doc["color"] | "FF0000"; + float initialWeight = doc["initial_weight_g"] | 0.0f; + float remainingWeight = doc["remaining_g"] | 0.0f; + float density = doc["density"] | 0.0f; + float diameter = doc["diameter_mm"] | 1.75f; + int minPrintTemp = doc["min_print_temp"] | 0; + int maxPrintTemp = doc["max_print_temp"] | 0; + int minBedTemp = doc["min_bed_temp"] | 0; + int maxBedTemp = doc["max_bed_temp"] | 0; + + if (strlen(uid) == 0) { + sendError(400, "UID is required"); + return; + } + if (strlen(manufacturer) == 0) { + sendError(400, "Manufacturer is required"); + return; + } + + const char* baseUrl = ConfigurationManager::getInstance().getSpoolmanURL(); + if (!baseUrl || strlen(baseUrl) == 0) { + sendError(500, "Spoolman URL not configured"); + return; + } + + WiFiClient client; + HTTPClient http; + char url[256]; + String response; + int code; + + // --- Step 1: Find or create vendor --- + int vendorId = -1; + snprintf(url, sizeof(url), "%s/api/v1/vendor?name=%s", baseUrl, manufacturer); + http.begin(client, url); + code = http.GET(); + if (code == 200) { + response = http.getString(); + StaticJsonDocument<2048> vendorDoc; + if (!deserializeJson(vendorDoc, response)) { + JsonArray arr = vendorDoc.as(); + for (JsonObject v : arr) { + if (strcasecmp(v["name"] | "", manufacturer) == 0) { + vendorId = v["id"] | -1; + break; + } + } + } + } + http.end(); + + if (vendorId < 0) { + // Create vendor + StaticJsonDocument<128> vendorBody; + vendorBody["name"] = manufacturer; + String vendorJson; + serializeJson(vendorBody, vendorJson); + + snprintf(url, sizeof(url), "%s/api/v1/vendor", baseUrl); + http.begin(client, url); + http.addHeader("Content-Type", "application/json"); + code = http.POST(vendorJson); + if (code == 200 || code == 201) { + response = http.getString(); + StaticJsonDocument<512> vDoc; + if (!deserializeJson(vDoc, response)) { + vendorId = vDoc["id"] | -1; + } + } + http.end(); + } + + // --- Step 2: Create filament --- + StaticJsonDocument<512> filBody; + filBody["name"] = strlen(materialName) > 0 ? materialName : material; + filBody["material"] = material; + if (vendorId > 0) filBody["vendor_id"] = vendorId; + filBody["density"] = density > 0 ? density : 1.24f; + filBody["diameter"] = diameter > 0 ? diameter : 1.75f; + if (strlen(color) > 0) filBody["color_hex"] = color; + + String filJson; + serializeJson(filBody, filJson); + Serial.printf("register-uid: filament payload: %s\n", filJson.c_str()); + + snprintf(url, sizeof(url), "%s/api/v1/filament", baseUrl); + http.begin(client, url); + http.addHeader("Content-Type", "application/json"); + code = http.POST(filJson); + if (code != 200 && code != 201) { + response = http.getString(); + http.end(); + char errMsg[128]; + snprintf(errMsg, sizeof(errMsg), "Failed to create filament (HTTP %d)", code); + sendError(500, errMsg); + return; + } + response = http.getString(); + http.end(); + + int filamentId = -1; + { + StaticJsonDocument<1024> filDoc; + if (!deserializeJson(filDoc, response)) { + filamentId = filDoc["id"] | -1; + } + } + if (filamentId < 0) { + sendError(500, "Failed to parse filament ID from Spoolman response"); + return; + } + + // --- Step 3: Create spool with nfc_id --- + StaticJsonDocument<512> spoolBody; + spoolBody["filament_id"] = filamentId; + if (initialWeight > 0) spoolBody["initial_weight"] = initialWeight; + if (remainingWeight > 0) spoolBody["remaining_weight"] = remainingWeight; + + JsonObject extra = spoolBody.createNestedObject("extra"); + // Spoolman extra fields require JSON-encoded string values (double-quoted) + char quotedUid[128]; + snprintf(quotedUid, sizeof(quotedUid), "\"%s\"", uid); + extra["nfc_id"] = quotedUid; + + String spoolJson; + serializeJson(spoolBody, spoolJson); + Serial.printf("register-uid: spool payload: %s\n", spoolJson.c_str()); + + snprintf(url, sizeof(url), "%s/api/v1/spool", baseUrl); + http.begin(client, url); + http.addHeader("Content-Type", "application/json"); + code = http.POST(spoolJson); + if (code != 200 && code != 201) { + response = http.getString(); + http.end(); + Serial.printf("register-uid: spool creation failed HTTP %d: %s\n", code, response.c_str()); + String errMsg = "Failed to create spool (HTTP " + String(code) + "): " + response; + _server.send(500, "application/json", "{\"error\":\"" + errMsg + "\"}"); + return; + } + response = http.getString(); + http.end(); + + int spoolId = -1; + { + StaticJsonDocument<1024> spoolDoc; + if (!deserializeJson(spoolDoc, response)) { + spoolId = spoolDoc["id"] | -1; + } + } + + // --- Success --- + StaticJsonDocument<256> result; + result["success"] = true; + result["spool_id"] = spoolId; + result["filament_id"] = filamentId; + result["vendor_id"] = vendorId; + result["uid"] = uid; + String resultJson; + serializeJson(result, resultJson); + + _server.send(200, "application/json", resultJson); + Serial.printf("WebServerManager: Registered UID %s as spool %d (filament %d)\n", uid, spoolId, filamentId); +} + // --------------------------------------------------------------------------- // API: Diagnostics // --------------------------------------------------------------------------- diff --git a/src/WebServerManager.h b/src/WebServerManager.h index 19cd9c1..232f844 100644 --- a/src/WebServerManager.h +++ b/src/WebServerManager.h @@ -53,6 +53,8 @@ class WebServerManager { void handleApiPostConfig(); void handleApiDiagnostics(); void handleTroubleshootingPage(); + void handleUIDRegistrationPage(); + void handleApiRegisterUid(); // OTA download state static void otaDownloadTask(void* param);