Filament deduplication — fetch all filaments per vendor and match client-side instead of relying on Spoolman's unreliable server-side filter. Case-insensitive matching on material, color, and name prevents duplicate filament creation. (#92)
HTTP connection reuse — disabled TCP connection reuse for Spoolman API calls. Stale keepalive connections caused intermittent 400 errors on every request after the first. Root cause of the filament churn. (#92)
Filament lookup failsafe — when Spoolman API returns an error, skip filament creation instead of creating a duplicate. Retries on next scan. (#92)
Filament naming convention — filament name built from material + modifier (e.g. "PLA Silk", "PETG CF") for consistent deduplication across tag formats. (#103)
Temperature averaging — when a tag stores min/max nozzle or bed temps, average them for Spoolman's single-value field instead of silently dropping one. (#100)
Diameter default — default to 1.75mm when diameter is not specified by the tag or user, at every layer. TigerTag now converts its diameter ID to mm. (#102)
Spoolman enrichment on reader page — smart tag scans (OpenTag3D, TigerTag, OpenSpool, OpenPrintTag) now trigger a Spoolman UID lookup. Spoolman-sourced fields (remaining weight, bed temp, spool ID) appear inline with a blue "Spoolman" badge. (#97)
Spoolman Enrichment section on writer pages — OpenSpool, TigerTag, and OpenTag3D writers gain a bottom section for fields Spoolman can store that the tag format cannot. Editable and saved to Spoolman on Write Tag with vendor/filament deduplication and user confirmation. (#97)
Read button on all writer pages — queues the scanner to read an existing tag for re-write scenarios. Fills both the tag form and enrichment section from a Spoolman UID match. (#97)
New API endpoints — GET /api/spoolman/find-vendor, GET /api/spoolman/find-filament, POST /api/spoolman/save-enrichment for the enrichment write flow. (#97)
Spool picker UX — results list hidden until user starts typing in the search box.
OpenSpool enrichment bed temp — collapsed to a single field (Spoolman stores one value, not min/max).
Enrichment persistence — Spoolman enrichment data persists on reader page after tag removal until the next scan.
Vendor/filament confirmation — user's choice to decline reusing an existing vendor or filament is now respected (creates new entry instead of silently reusing).
OpenSpool tag format support — reads and writes OpenSpool NFC tags (NTAG215/216 with NDEF JSON payload). Standalone parser detects application/json NDEF records with "protocol":"openspool". Writer page at /writer/openspool with Spoolman spool picker integration. Reader page shows brand, material, color, and nozzle temps. TFT "OS" label (pink). Logo on landing page. Nav link on all pages. (#93)
Duplicate spool prevention — reuse existing spool when Spoolman archive fails instead of creating a duplicate. Prevents the cascade where intermittent API failures caused new spools on every scan. (#91)
Zero weight Spoolman rejection — omit weight, density, and diameter fields from Spoolman API when zero. Fixes 422 errors on Spoolman 0.23.x which rejects "weight": 0. (#91)
JSON injection in write API — OpenSpool write endpoint uses ArduinoJson to build tag payload instead of snprintf with raw user input. (#93)
Spoolman extra fields auto-register — scanner now checks and creates required Spoolman extra fields (dry_temp, dry_time_hours, aspect, nfc_id, tag_format, active_toolhead) on first sync. Versioned NVS flag skips API calls on subsequent boots. Fixes filament auto-create 400 errors on Spoolman 0.23.x. (#87)
Vendor lookup deduplication — fetch all vendors and match client-side instead of unreliable ?name= filter. Prevents duplicate vendor creation on every scan. (#87)
JSON error response escaping — sendError() now escapes quotes, backslashes, newlines, and control characters. Prevents malformed JSON responses. (#77)
Write/OTA return value checks — format-tag and TigerTag write endpoints return 503 when queue full. OTA task creation failure returns 500 and resets state. (#78)
Configurable mDNS hostname — set a custom hostname via the config page for multi-scanner setups (AFC lanes, toolchangers). Default remains spoolsense.local. Also sets WiFi DHCP hostname. Server-side and client-side validation. (#82)
snprintf buffer overflow in HA JSON — clamped buffer length before conditional appends and closing brace in handleSpoolDetected to prevent memory corruption on long payloads. (#79)
Monotonic NFC write request IDs — replaced millis()-based IDs with centralized NFCManager::generateRequestId() counter to prevent collisions across batched writes. (#80)
processMessages queue cap — capped message loop at QUEUE_SIZE iterations to prevent timer starvation from rapid message bursts. (#81)
HTTP mutex on WebServerManager — Spoolman proxy, UID registration, and spool link handlers now acquire g_httpMutex. Prevents races with background Spoolman sync that caused intermittent HTTP 400 errors. (#75)
URL-encode manufacturer — vendor names with spaces (e.g., "Bambu Lab") no longer break UID registration queries. (#76)
ApplicationManager code review fixes — shadowed variable rename, deduplicated ifdef blocks, NATIVE_TEST guard, HA field rename.
HA state retention after tag removal — spool data (material, color, weight, temps) persists in HA sensor after tag is removed, with present: false. Dashboards now show last scanned spool instead of going blank. (#60)
Tag writer: populate from Spoolman — searchable spool picker on all 3 writer pages (OpenPrintTag, TigerTag, OpenTag3D). Pick a spool from your Spoolman inventory and the form auto-fills. Shows hint when Spoolman is not configured. (#32)
[P1] Spoolman streaming UID lookup — replaced ArduinoJson bulk parse with streaming HTTP parser for spool lookups. Fixes NoMemory crash on 30+ spools that broke all tag format syncs. Uses ~600 bytes instead of ~60KB. Works with any database size. (#68)
HA publish queue — increased from 6 to 12 items and added drop logging. Previously silently dropped messages when MQTT was disconnected. (#28)
TFT null queue guards — all queue operations now check for null before access. Prevents crash if queue allocation fails.
TFT queue depth — increased from 4 to 8 with drop logging for burst traffic.
TFT showText4 — was discarding lines 1-2 on generic tag scans. Now shows "Spool: Unknown Tag" instead of just "Unknown Tag".
TFT duplicate struct removed — eliminated TFTSpoolData (duplicate of DisplaySpoolData with unused name field). Saves 48 bytes per queued message.
TFT startTask check — now verifies task creation succeeded instead of logging success unconditionally.
WiFi reconnection — automatic reconnect with exponential backoff (5s→60s) when WiFi drops. Display shows "WiFi Lost" / "WiFi OK" on state change. mDNS re-initializes on reconnect. NFC scanning continues uninterrupted. (#29)
Bambu AMS blueprint — Home Assistant blueprint pushes scanned spool data (material, color, temps) into Bambu Lab AMS trays automatically. Scan a tag, load the tray, done.
Bambu → Spoolman deduction blueprint — after a Bambu print finishes or is canceled, automatically deducts filament weight from Spoolman per tray. Requires spoolman-homeassistant integration.
Bambu filament ID map — 34 material types mapped to Bambu generic filament IDs for accurate AMS tray identification.
Link/re-assign NFC+ tags to Spoolman spools — spool picker on reader page with search. Link unlinked tags or re-assign existing ones. Proxy endpoints avoid CORS. (#54)
Tag writer auto-populate from scanned tag — place a tag on the reader, open any writer page, form fields pre-fill from the tag's data. Works cross-format (scan TigerTag, write as OpenPrintTag). (#57)
NFC+ reader shows temps — extruder and bed temps from Spoolman now displayed on the reader page for NFC+ tags. (#56)
TFT display support (ST7789 240x240) — color spool graphic with filament color fill, weight bar, tag format icons, and breathing animation for low spools (<100g). Runtime NVS toggle in web config. Mutually exclusive with LCD on WROOM (shared GPIO 22/23). Uses LovyanGFX with 8-bit color sprite for heap efficiency.
DisplayI interface — LCDManager and TFTManager both implement a shared display interface. ApplicationManager works with either display without knowing which is attached.
Spoolman color_hex parsing — nested objects (vendor.extra:{}) broke the JSON streaming parser, causing color to be empty for NFC+ UID lookups. Parser now skips unknown nested objects correctly.
NFC+ registration temps — extruder and bed temperatures now written to Spoolman filament settings. Single temp fields (averaged from material DB min/max).
NFC+ weight bar — initial_weight_g now passed through SpoolmanSyncedPayload so NFC+ tags show the weight bar on TFT and LCD.
SPI bus separation — PN5180 moved to HSPI, TFT on VSPI. Separate SPI peripherals eliminate bus contention.
AP mode fallback with captive portal — when WiFi SSID is empty or STA connection fails after 15s, the ESP32 starts an open hotspot (SpoolSense-XXXX). A captive portal DNS server auto-redirects phones and laptops to the config page at 192.168.4.1. Enter WiFi credentials, save, device reboots into normal mode. Enables zero-CLI setup for the upcoming web flasher.
Tag writer dry temp/time auto-populate — switched TigerTag and OpenTag3D writer material data from api.tigertag.io to canonical GitHub JSON source (TigerTag-RFID-Guide/database/). The API was missing dry temp, dry time, and proper nozzle/bed min/max ranges. All writer pages now auto-populate dry temp, dry time, and actual min/max values on material selection. Closes #49.
Stale auto-fill values when switching materials — writer fields now clear when switching to a material without data instead of retaining the previous material's values.
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.
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<HardwareNFCConnection*> 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).
LCD Type field blank after Spoolman sync — material_name is now carried in SpoolmanSyncedPayload instead of being re-read from NFC state after sync completes. Avoids blank Type when the tag briefly loses contact during the Spoolman HTTP request.
NFC+ Register button silent failure — fixed TypeError caused by calling .options[selectedIndex] on an <input> element (not a <select>). Registration now works correctly.
Generic tag Spoolman lookup — scanning a plain NFC tag (e.g. NTAG215) now triggers a Spoolman UID lookup via the nfc_id extra field. Material, manufacturer, color, and remaining weight are displayed on the LCD and populated in the reader page and /api/status.
Duplicate UID handling — when multiple Spoolman spools share the same nfc_id, the most recently registered spool (highest ID) is now used instead of the first match.
NVS runtime feature toggling — LCD and LED are now controlled at runtime via NVS (lcd_on, led_on) rather than compile-time #ifdef flags. A single firmware binary now correctly enables or disables hardware based on what the installer configured. ConfigurationManager::begin() moved before peripheral init so NVS values are available before hardware is initialized.
Atomic OpenPrintTag write — /api/write-tag now builds a fresh CBOR tag and writes all fields in a single NFC pass (~5s vs ~25s). Eliminates sequential write drops, scan loop race conditions, and CBOR re-encoding overflow. Existing field values are preserved when not specified.
Skip redundant Spoolman syncs — sync state cache (per UID) skips PATCH requests when filament and weight haven't changed since the last sync. 2-hour TTL with automatic invalidation on tag re-use, archive, and middleware write commands. Reduces unnecessary Spoolman API traffic, especially in AFC setups.
Buffer overflow in mifareBlockWrite16 — cmd[1] wrote past 1-byte array (undefined behavior)
Mutex-less tag_data reads — sendSpoolUpdatedMessage and processWriteQueue now hold tagMutex when reading currentSpool
ESP.restart() during NFC write — scan task is now paused before restart to prevent tag corruption
NFCScanTask stack bumped 6144→8192 bytes
Spoolman sync cache weight comparison uses epsilon (0.01g) instead of float == to prevent false misses from floating-point drift
Spoolman sync cache protected by dedicated FreeRTOS mutex — prevents races between syncSpool() task and ApplicationManager invalidation on middleware writes
spoolman_id included in sync cache hit check — prevents false cache hits when different spools share the same UID, filament, and weight within the TTL window
PrusaLink integration (experimental) — automatic print monitoring and filament weight deduction via PrusaLink API. PrinterManager FreeRTOS task with IDLE/TRACKING state machine. Pre-print validation warns on filament type mismatch and nozzle temp exceeding tag max. Per-tool XL multi-head filament tracking (up to 5 tools). Web config UI for PrusaLink enable toggle, URL, and API key. Looking for testers with Prusa printers.
LED set_color command — scanner LED now shows filament color from Spoolman when scanning UID-only tags. Receives color via MQTT spoolsense/<id>/cmd/set_color.
NFC+ Registration page at /register/uid — register plain NFC tags in Spoolman using UID as identifier. No data written to the tag.
Shared material auto-fill — selecting a material auto-fills nozzle temps (±10°C), bed temps (±5°C), and density across all writer pages
Type-to-search for material and brand fields on all writer pages (replaces dropdowns). TigerTag API expands options when reachable, hardcoded fallback for offline use.
Web-based configuration page at spoolsense.local/config — change WiFi, MQTT, Spoolman, automation mode, and hardware settings from the browser. Settings saved to NVS and persist across OTA updates. Device reboots after saving.
Spoolman filament enrichment — settings_extruder_temp and settings_bed_temp populated from tag data (averaged from min/max). Custom material_name from tag used as filament name.
Spoolman extra fields — tag_format on spool (OpenPrintTag/TigerTag), aspect, dry_temp, dry_time_hours on filament (from TigerTag data). Written opportunistically; no errors if fields don't exist.
Installer creates Spoolman extra fields (nfc_id, tag_format, aspect, dry_temp, dry_time_hours) when Spoolman is enabled.
Spoolman sync test suite (11 tests) covering spool UUID lookup with nested JSON, matching edge cases.
Filament matching uses vendor + material + color (was vendor + material only). Prevents different colored filaments from the same brand conflating into one entry.
Spoolman is source of truth — existing filament fields (name, temps) are never overwritten; only blank values are filled from tag data.
SpoolmanManager::isConfigured() now checks isSpoolmanEnabled() flag, not just URL length. Respects web config enable/disable toggle.
Spoolman spool lookup replaced streaming JSON parser with ArduinoJson for reliable nested object handling.
setupRF() no longer fails after tag reads — full PN5180 state machine reset (RF_OFF → Idle → clear IRQs → loadRFConfig → RF_ON → Transceive) between scan cycles.
Spoolman spool lookup no longer creates duplicates — ArduinoJson correctly handles nested filament/vendor id fields.
Spoolman enable/disable via web config — fixed NVS type mismatch (putBool/getBool) and isConfigured check.
LCD config flag — uses #if ENABLE_LCD (value check) instead of #ifdef ENABLE_LCD (existence check).
Native test build — ENABLE_STATUS_LED unset in native builds, pauseScanTask/resumeScanTask gated behind NATIVE_TEST, TigerTagParser/ConversionUtils linked into NFC tests.
setupRF() no longer fails after tag reads — root cause was the PN5180 state machine not being reset between scan cycles. After any tag read (ISO15693 multi-block or ISO14443A probe), the chip stayed in Transceive/Receive state. setupRF() now performs a full state machine reset: RF_OFF → Idle → clear IRQs → loadRFConfig → RF_ON → Transceive. Eliminates repeated SpoolDetected events and Spoolman API spam.
Spoolman spool lookup no longer creates duplicates — replaced the streaming JSON parser (htcw_json) with ArduinoJson for spool UUID lookups. The streaming parser could not reliably handle Spoolman's nested JSON responses (filament → vendor → id), causing UUID matching to fail and duplicate spools to be created on every scan. ArduinoJson handles nesting correctly.
[1.3.3] - 2026-03-18 — Multi-page Web UI, TigerTag, OTA Updates¶
Multi-page web UI at spoolsense.local — landing page with tool cards, shared navigation across all pages
Tag Reader page (/reader) — auto-detects tag format (OpenPrintTag, TigerTag, generic UID), displays all data read-only, stops polling on detection with "Scan Again" button
TigerTag reader support — detect and parse NTAG213/215 TigerTag binary format with embedded material/brand lookup tables
TigerTag writer page (/writer/tigertag) — write filament data to NTAG tags in TigerTag format with material, brand, color, weight, diameter, aspect, and temperature fields
writeISO14443Pages() — NTAG page write support via PN5180 mifareBlockWrite4 (command 0xA2)
WRITE_TIGERTAG NFC write type with 40-byte binary payload
OTA firmware update page (/update) — check for updates from GitHub releases with release notes display, one-click download and flash with progress tracking, manual .bin upload
Dual OTA partition table (ota_0 + ota_1, 1.88MB each) — enables automatic rollback on failed update
Async OTA download — background FreeRTOS task streams firmware from GitHub HTTPS, browser polls /api/ota-status for progress
NFC scan task pause/resume during OTA upload
GET /api/version — returns firmware version and board type
GET /api/ota-status — returns OTA download/flash state and progress
POST /api/update-from-url — ESP32 downloads and flashes firmware from a URL
POST /api/upload-firmware — multipart binary upload for manual OTA
/api/status extended with tag_kind field and nested tigertag object for TigerTag data
POST /api/write-tigertag — assemble and enqueue TigerTag binary write
OpenPrintTag and TigerTag logos served as PROGMEM PNGs at /img/openprinttag.png and /img/tigertag.png
Logos displayed in writer page card headers
Spool archive on re-tag — automatically archives old Spoolman spool when tag is re-written with different filament, or same filament with weight jump on nearly empty spool (≤100g → >500g)
FIRMWARE_VERSION build flag in platformio.ini — single source of truth for version, shown on update page and in HA discovery
Shared CSS (/css/shared.css) and JS (/js/shared.js) served as cacheable endpoints
BLE stack — BluetoothManager.cpp/.h deleted, CONFIG_BT build flags removed, BLE init removed from main.cpp. Saves ~540KB flash. Configuration moving to web UI
TigerTag reader support — detect and parse NTAG213/215 TigerTag binary format (ISO14443A) with material, brand, color, weight, and temperature data via embedded lookup tables
NVS config support — ConfigurationManager reads from NVS partition first, per-key fallback to compile-time defaults; enables pre-built binary flashing via installer without recompiling
GitHub Actions release workflow — auto-builds ESP32-WROOM and ESP32-S3-Zero firmware on tag push, attaches bootloader + partitions + firmware to GitHub release
SpoolSense Installer — interactive CLI (spoolsense-installer repo) that downloads firmware, generates NVS config, verifies chip/flash, and flashes via esptool
SpoolSense GitHub org — both repos transferred to github.com/SpoolSense
Built-in HTTP web server (port 80) — reachable at http://spoolsense.local after WiFi connects
Tag writer UI served directly from the device — write OpenPrintTag fields from any browser on the local network
REST API for tag operations: GET /api/status, POST /api/write-tag, POST /api/format-tag
Tag writer supports all OpenPrintTag fields: material type, manufacturer, custom material name,
full/remaining weight, color, density, diameter, print/bed/preheat temperatures, and Spoolman ID
CORS headers on all endpoints for local development testing
mDNS registration as spoolsense.local
NFC read retry logic (up to 3 attempts with RF reset between retries) — fixes ISO15693 read
failures that previously left the NFC stack in a stuck state
setupRF() failure recovery: clears stuck RF state immediately instead of waiting 30s for watchdog
NFC scan loop now performs a full hardware reset when setupRF() fails with a tag present,
recovering in one cycle instead of ~30 seconds
LCD is now truly optional — ENABLE_LCD 0 in UserConfig.h fully gates I2C init, LCD task,
and all LCD calls; no Wire library or LCD code compiled when disabled