Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
167 changes: 167 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# Overview:

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

Consider adding blank lines around headings for markdown compliance.

The markdown linter flagged that headings should be surrounded by blank lines (MD022). While this doesn't affect functionality, adding blank lines would improve readability and markdown standard compliance.

Also applies to: 8-8, 20-20, 28-28, 35-35, 144-144, 151-151

🧰 Tools
🪛 markdownlint-cli2 (0.22.0)

[warning] 1-1: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

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

In `@CLAUDE.md` at line 1, Add single blank lines before and after each top-level
heading so the file complies with MD022; specifically insert a blank line above
and below the "# Overview:" heading and do the same for the other headings
called out (the headings at the positions noted: the ones around lines showing
headings at 8, 20, 28, 35, 144, and 151) so each heading is separated by an
empty line above and below.

ESP32 / ESP32-S3 Arduino project built with PlatformIO.
Primary current hardware target is ESP32 DevKit / WROOM (typically 4MB flash).
PN5180 NFC scanner firmware with OpenPrintTag support today and planned generic NFC tag support. The firmware is intended to function as a standalone NFC scanner for spool management systems such as SpoolSense rather than direct printer integrations.
16x2 I2C LCD support is optional.
All config is compile-time via UserConfig.h. OTA firmware updates via web UI.

# Guidelines:
Be concise in responses
Run compile checks before handing off: `pio run -e esp32s3zero` (primary target) and `pio run -e esp32dev` (secondary). Do NOT flash — only compile.
Consider thread safety for all changes
Add new files to source inventory (one-line)
Run tests after changes: ./scripts/run_all_tests.sh
Avoid adding more heap allocations - the device is low on memory.
Use StaticJsonDocument document, even though it is deprecated.
Use LSP plugins when searching for C/C++ and TypeScript identifiers.
All user-editable configuration must live in include/UserConfig.h.
Do not add hardcoded credentials or environment-specific settings anywhere else in the codebase.

# Project goals:
- Rename project to `spoolsense_scanner` and remove legacy `openprinttag_scanner` naming throughout the codebase, docs, BLE strings, and UI.
- Keep OpenPrintTag support as the first fully supported tag format.
- Add support for standard NFC tags such as NTAG215 for simple UID-based Spoolman / SpoolSense workflows. These tags are not OpenPrintTag and should be handled as a separate path.
- Keep LCD support optional as a first-class build/profile feature.
- Add and stabilize ESP32-S3 support.
- Long-term stretch goal: support OpenTag3D as an additional tag format later.

# Architecture summary:
main.cpp: Initializes all managers, starts FreeRTOS tasks
ApplicationManager: Central state machine + message bus, receives events (print start, spool scan, etc.) via queue and coordinates responses.
NFC Stack: NFCManager -> Hardware NFC adapter (PN5180 today) -> tag protocol handler -> openprinttag_lib (for OpenPrintTag) or generic tag handler
Spool Sync: ApplicationManager triggers sync -> SpoolmanManager queues request -> SpoolmanManager task -> HTTP requests to SpoolSense / Spoolman style APIs.

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

Optional: hyphenate compound adjective.

The phrase "Spoolman style APIs" could be hyphenated as "Spoolman-style APIs" per grammar conventions for compound adjectives.

🧰 Tools
🪛 LanguageTool

[grammar] ~32-~32: Use a hyphen to join words.
Context: ...> HTTP requests to SpoolSense / Spoolman style APIs. Configuration: Compile-time ...

(QB_NEW_EN_HYPHEN)

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

In `@CLAUDE.md` at line 32, Update the phrase in CLAUDE.md to hyphenate the
compound adjective: change "Spoolman style APIs" to "Spoolman-style APIs" so the
compound adjective is grammatically correct (look for the line containing "Spool
Sync: ApplicationManager triggers sync -> SpoolmanManager queues request ->
SpoolmanManager task -> HTTP requests to SpoolSense / Spoolman style APIs.").

Configuration: Compile-time via UserConfig.h -> DeviceConfig -> ConfigurationManager. BLE (BluetoothManager) handles only spool tag operations at runtime. Long-term direction is a broader multi-format scanner architecture for SpoolSense.

# Source Inventory
OpenPrintTag Library
lib/openprinttag/cbor.h / cbor_native.c — Minimal CBOR implementation used by the OpenPrintTag encoder/decoder and native tests
lib/openprinttag/openprinttag_lib.c / .h — Encode/decode filament data (CBOR, NDEF)
lib/openprinttag/openprinttag_pn532.h — Example HAL adapter for PN532-style 4‑byte page NFC implementations (pn532-esp-idf)
lib/openprinttag/openprinttag_adafruit_pn532.h — HAL adapter for Adafruit_PN532 Arduino library

PN5180 Driver
lib/PN5180/Debug.cpp / .h — Hex/debug helpers
lib/PN5180/PN5180.cpp / .h — Core driver, SPI + register control
lib/PN5180/PN5180ISO15693.cpp / .h — ISO15693 protocol implementation
lib/PN5180/PN5180ISO14443.cpp / .h — ISO14443A detection (Type A activate + anticollision, NTAG215 UID) — Copyright 2019 Dirk Carstensen, LGPL-2.1

Board / Config
include/BoardPins.h — Board-conditional pin definitions (#define), auto-selected via BOARD_ESP32_S3 from UserConfig.h

Application Core
src/main.cpp — Entry point, task startup
src/ApplicationManager.cpp / .h — Central state machine + event queue
src/ConfigurationManager.cpp / .h — Device config (loaded from UserConfig.h at boot via DeviceConfig)
src/DeviceConfig.cpp / .h — Compile-time config struct populated from UserConfig.h defines

NFC
src/NFCManager.cpp / .h — NFC scan/read/write task and primary tag detection/handling entry point
src/HardwareNFCConnection.cpp / .h — PN5180 hardware adapter (ISO15693 + ISO14443A page read/write)
src/HardwareNFCConnectionPN532.cpp / .h — PN532 hardware adapter (ISO14443A only, Adafruit_PN532)
src/NFCConnectionI.h — NFC hardware interface
src/NFCTypes.h — Detected spool state structs (TagKind: OpenPrintTag, GenericUidTag, OpenTag3D, TigerTag, BlankTag)
src/NFCWriteTypes.h — Write queue types/enums (includes WRITE_TIGERTAG with 40-byte payload)
src/TigerTagParser.cpp / .h — TigerTag NTAG213 binary parser with embedded material/brand lookup tables


Spool Sync
src/SpoolmanManager.cpp / .h — Spoolman API sync + queue worker

Home Assistant
src/HomeAssistantManager.cpp / .h — MQTT client task, publish/subscribe, HA discovery

UI / UX
src/DisplayI.h — Display interface (showText, showSpool, showKeypad, showWriteResult) — implemented by LCDManager and TFTManager
src/LCDManager.cpp / .h — I2C LCD task + status updates, implements DisplayI
src/LCDDisplayLogic.h — Shared LCD message merge/timing rules
src/TFTManager.cpp / .h — ST7789 240x240 TFT display via LovyanGFX, implements DisplayI, 8-bit color sprite rendering
src/TFTConfig.h — LovyanGFX hardware config per board (SPI bus, pins, panel settings)
src/WebServerManager.cpp / .h — HTTP server (port 80, mDNS spoolsense.local); multi-page UI + API endpoints + OTA upload
src/LandingHTML.h — Landing page PROGMEM served at GET /
src/ReaderHTML.h — Tag reader page PROGMEM served at GET /reader
src/TagWriterHTML.h — OpenPrintTag writer page PROGMEM served at GET /writer/openprinttag
src/TigerTagWriterHTML.h — TigerTag writer page PROGMEM served at GET /writer/tigertag
src/UpdateHTML.h — Firmware update page PROGMEM served at GET /update (auto-check GitHub + manual upload)
src/ConfigHTML.h — Device configuration page PROGMEM served at GET /config (WiFi, MQTT, Spoolman, hardware)
src/SharedCSS.h — Shared CSS PROGMEM served at GET /css/shared.css
src/SharedJS.h — Shared JS PROGMEM served at GET /js/shared.js
src/OpenPrintTagLogo.h — OpenPrintTag logo PNG served at GET /img/openprinttag.png
src/TigerTagLogo.h — TigerTag logo PNG served at GET /img/tigertag.png
docs/writer-ui-plan.md — Tag writer UI redesign plan

Utilities
src/ConversionUtils.cpp / .h — Shared data format conversion utilities (material types, colors, density defaults)
src/InputManager.cpp / .h — Optional 3x4 matrix keypad driver; polls for key presses and enqueues KEYPAD_DIGIT/CONFIRM/CANCEL messages (compiled only when ENABLE_KEYPAD=1)

Tests
OpenPrintTag
test/test_openprinttag.c — CBOR + NDEF unit tests (mock HAL)

Native Fakes / Stubs
test/native/FakeLCDManager.h — In-memory LCD
test/native/StubApplicationManager.h — Message capture stub
test/native/StubNFCConnection.h — Simulated NFC tags
test/native/NativePlatform.cpp — Stub Serial

Native Tests
test/native/test_app_flow.cpp — App state transitions
test/native/test_lcd_manager.cpp — LCD message merge timing behavior
test/native/test_nfc_read.cpp — NFC read behavior
test/native/test_raw_write.cpp — Raw binary write to NFC tag
test/native/TestableApplicationManager.h — Queue bypass harness
test/native/TestNFCManager.h — Write queue tracker
test/native/test_helpers.h — Factories + assertions

Integration Tests
test/integration/ha.cpp — Native standalone MQTT/HA connectivity + discovery/state publisher
test/integration/Makefile — Build/run helper for local HA integration probe

Integration / HIL Test Harness
test/integration/http_server.py — Test orchestrator + mock spool management APIs + SSE server
test/integration/mock_spoolman.py — Mock Spoolman API state controller
test/integration/scenarios/base.py — BaseTestScenario with BLE bridge helpers
test/integration/scenarios/test_format_spool.py — Format spool test
test/integration/scenarios/test_set_filament.py — Set filament weight test
test/integration/scenarios/test_set_filament_profile.py — Set filament type/manufacturer test
test/integration/scenarios/test_print_e2e.py — End-to-end print simulation test
test/integration/scenarios/test_print_30_percent.py — Canceled print at 30% integration test
test/integration/scenarios/test_print_100x.py — 100x print endurance test (excluded from run-all)
test/integration/scenarios/test_recent_spools.py — Swap spool A/B and verify recently seen spool history
test/integration/scenarios/test_spoolman_sync.py — Spoolman sync verification test
test/integration/scenarios/test_color_update.py — Color field update and verification test
test/integration/scenarios/test_spool_swap_during_print.py — Mid-print spool swap edge case test
test/integration/scenarios/test_zero_weight_handling.py — Zero weight boundary and clamping test
test/integration/scenarios/test_printer_api_errors.py — PrusaLink API error resilience test
test/integration/scenarios/test_print_progress_edge_cases.py — Print progress edge cases (0%, 100%, dwell)
test/integration/scenarios/test_automation_mode_controlled.py — HA-controlled mode (no auto-deduction) test
test/integration/scenarios/test_job_disappeared_deduction.py — Job disappeared (204) bgcode fallback deduction test
test/integration/scenarios/test_real_tag.py — Raw binary write from fixture and 100g deduction verification
test/integration/scenarios/test_write_spoolman_spool.py — Write Spoolman spool test (Mode A API fetch, Mode B direct data)
test/integration/site/index.html — Web Bluetooth test runner UI
test/integration/requirements.txt — Python dependencies for integration tests (paho-mqtt)
test/integration/mqtt_config.json — MQTT broker configuration for event-driven test waits

# Notes for future direction
- Treat OpenPrintTag, generic UID tags, and future formats such as OpenTag3D as separate handler paths.
- Prefer protocol/tag detection first, then route to the correct parser/handler.
- Reuse `lib/openprinttag/*` as the OpenPrintTag engine, while evolving the surrounding scanner firmware under the `spoolsense_scanner` identity.

- Direct printer integrations (PrusaLink, OctoPrint) are intentionally out of scope for spoolsense_scanner and should not be expanded further in this project.

# gstack
Use the /browse skill from gstack for all web browsing. Never use mcp__claude-in-chrome__* tools.

Available skills:
- /plan-ceo-review
- /plan-eng-review
- /plan-design-review
- /design-consultation
- /review
- /ship
- /browse
- /qa
- /qa-only
- /qa-design-review
- /setup-browser-cookies
- /retro
- /document-release
17 changes: 17 additions & 0 deletions include/BoardPins.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@
#define PIN_KEYPAD_COL1 17
#define PIN_KEYPAD_COL2 18
#define PIN_KEYPAD_COL3 42
// TFT SPI display (ENABLE_TFT — S3 pins, not yet validated on hardware)
#define PIN_TFT_MOSI 13
#define PIN_TFT_SCLK 15
#define PIN_TFT_MISO -1
#define PIN_TFT_CS 16
#define PIN_TFT_DC 3
#define PIN_TFT_RST -1
#define PIN_TFT_BL -1
#else
// --- ESP32-WROOM-32 pin mapping (default) ---
// PN5180 SPI
Expand Down Expand Up @@ -70,4 +78,13 @@
#define PIN_KEYPAD_COL1 19
#define PIN_KEYPAD_COL2 21
#define PIN_KEYPAD_COL3 5
// TFT SPI display (ENABLE_TFT — mutually exclusive with LCD I2C)
// Uses VSPI. Pins 23/22 freed from LCD when TFT replaces it.
#define PIN_TFT_MOSI 23 // VSPI MOSI (shared with LCD SDA when LCD enabled)
#define PIN_TFT_SCLK 22 // VSPI SCLK (shared with LCD SCL when LCD enabled)
#define PIN_TFT_MISO -1 // Not needed for write-only TFT
#define PIN_TFT_CS 2 // Free GPIO
#define PIN_TFT_DC 4 // Freed from LED when TFT is enabled
#define PIN_TFT_RST -1 // Software reset via LovyanGFX
#define PIN_TFT_BL -1 // No backlight control (always on), or pick a free pin
#endif
58 changes: 31 additions & 27 deletions lib/PN5180/PN5180.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,13 @@
//#define DEBUG 1

#include <Arduino.h>
#include <SPI.h>
#include "PN5180.h"
#include "Debug.h"

// Use HSPI for PN5180 so VSPI is free for TFT display
static SPIClass pn5180_spi(HSPI);

// PN5180 1-Byte Direct Commands
// see 11.4.3.3 Host Interface Command List
#define PN5180_WRITE_REGISTER (0x00)
Expand Down Expand Up @@ -67,9 +71,9 @@ void PN5180::begin() {
digitalWrite(PN5180_RST, HIGH); // no reset

if (PN5180_SCK >= 0 && PN5180_MISO >= 0 && PN5180_MOSI >= 0) {
SPI.begin(PN5180_SCK, PN5180_MISO, PN5180_MOSI);
pn5180_spi.begin(PN5180_SCK, PN5180_MISO, PN5180_MOSI);
} else {
SPI.begin();
pn5180_spi.begin();
}
PN5180DEBUG(F("SPI pinout: "));
PN5180DEBUG(F("SS=")); PN5180DEBUG(PN5180_NSS);
Expand All @@ -81,7 +85,7 @@ void PN5180::begin() {

void PN5180::end() {
digitalWrite(PN5180_NSS, HIGH); // disable
SPI.end();
pn5180_spi.end();
}

/*
Expand Down Expand Up @@ -109,9 +113,9 @@ bool PN5180::writeRegister(uint8_t reg, uint32_t value) {
*/
uint8_t buf[6] = { PN5180_WRITE_REGISTER, reg, p[0], p[1], p[2], p[3] };

SPI.beginTransaction(PN5180_SPI_SETTINGS);
pn5180_spi.beginTransaction(PN5180_SPI_SETTINGS);
transceiveCommand(buf, 6);
SPI.endTransaction();
pn5180_spi.endTransaction();
Comment on lines +116 to +118

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

Return the result of transceiveCommand() instead of always succeeding.

This wrapper still discards the bool from transceiveCommand() and returns true, so BUSY/SPI timeouts are silently reported as success. The same pattern is repeated in the other transaction wrappers below.

Possible fix
-  pn5180_spi.beginTransaction(PN5180_SPI_SETTINGS);
-  transceiveCommand(buf, 6);
-  pn5180_spi.endTransaction();
-
-  return true;
+  pn5180_spi.beginTransaction(PN5180_SPI_SETTINGS);
+  const bool ok = transceiveCommand(buf, 6);
+  pn5180_spi.endTransaction();
+  return ok;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/PN5180/PN5180.cpp` around lines 116 - 118, The wrapper that begins/ends
SPI transactions (calls pn5180_spi.beginTransaction, transceiveCommand(buf, 6),
pn5180_spi.endTransaction()) should propagate the boolean result of
transceiveCommand instead of always returning true; modify the wrapper function
(and the other similar transaction wrappers in PN5180.cpp) to capture the return
value of transceiveCommand(...) and return that value (ensuring endTransaction()
still runs), so BUSY/SPI timeouts are reported correctly.


return true;
}
Expand Down Expand Up @@ -139,9 +143,9 @@ bool PN5180::writeRegisterWithOrMask(uint8_t reg, uint32_t mask) {

uint8_t buf[6] = { PN5180_WRITE_REGISTER_OR_MASK, reg, p[0], p[1], p[2], p[3] };

SPI.beginTransaction(PN5180_SPI_SETTINGS);
pn5180_spi.beginTransaction(PN5180_SPI_SETTINGS);
transceiveCommand(buf, 6);
SPI.endTransaction();
pn5180_spi.endTransaction();

return true;
}
Expand Down Expand Up @@ -169,9 +173,9 @@ bool PN5180::writeRegisterWithAndMask(uint8_t reg, uint32_t mask) {

uint8_t buf[6] = { PN5180_WRITE_REGISTER_AND_MASK, reg, p[0], p[1], p[2], p[3] };

SPI.beginTransaction(PN5180_SPI_SETTINGS);
pn5180_spi.beginTransaction(PN5180_SPI_SETTINGS);
transceiveCommand(buf, 6);
SPI.endTransaction();
pn5180_spi.endTransaction();

return true;
}
Expand All @@ -190,9 +194,9 @@ bool PN5180::readRegister(uint8_t reg, uint32_t *value) {

uint8_t cmd[2] = { PN5180_READ_REGISTER, reg };

SPI.beginTransaction(PN5180_SPI_SETTINGS);
pn5180_spi.beginTransaction(PN5180_SPI_SETTINGS);
transceiveCommand(cmd, 2, (uint8_t*)value, 4);
SPI.endTransaction();
pn5180_spi.endTransaction();

PN5180DEBUG(F("Register value=0x"));
PN5180DEBUG(formatHex(*value));
Expand Down Expand Up @@ -223,9 +227,9 @@ bool PN5180::readRegister(uint8_t reg, uint32_t *value) {
buffer[2+i] = data[i];
}

SPI.beginTransaction(PN5180_SPI_SETTINGS);
pn5180_spi.beginTransaction(PN5180_SPI_SETTINGS);
transceiveCommand(buffer, len+2);
SPI.endTransaction();
pn5180_spi.endTransaction();

return true;
}
Expand Down Expand Up @@ -255,9 +259,9 @@ bool PN5180::readEEprom(uint8_t addr, uint8_t *buffer, int len) {

uint8_t cmd[3] = { PN5180_READ_EEPROM, addr, (uint8_t)len };

SPI.beginTransaction(PN5180_SPI_SETTINGS);
pn5180_spi.beginTransaction(PN5180_SPI_SETTINGS);
transceiveCommand(cmd, 3, buffer, len);
SPI.endTransaction();
pn5180_spi.endTransaction();

#ifdef DEBUG
PN5180DEBUG(F("EEPROM values: "));
Expand Down Expand Up @@ -328,9 +332,9 @@ bool PN5180::sendData(uint8_t *data, int len, uint8_t validBits) {
return false;
}

SPI.beginTransaction(PN5180_SPI_SETTINGS);
pn5180_spi.beginTransaction(PN5180_SPI_SETTINGS);
transceiveCommand(buffer, len+2);
SPI.endTransaction();
pn5180_spi.endTransaction();

return true;
}
Expand Down Expand Up @@ -360,9 +364,9 @@ uint8_t * PN5180::readData(int len, uint8_t *buffer /* = NULL */) {

uint8_t cmd[2] = { PN5180_READ_DATA, 0x00 };

SPI.beginTransaction(PN5180_SPI_SETTINGS);
pn5180_spi.beginTransaction(PN5180_SPI_SETTINGS);
transceiveCommand(cmd, 2, buffer, len);
SPI.endTransaction();
pn5180_spi.endTransaction();

#ifdef DEBUG
PN5180DEBUG(F("Data read: "));
Expand Down Expand Up @@ -403,9 +407,9 @@ bool PN5180::loadRFConfig(uint8_t txConf, uint8_t rxConf) {

uint8_t cmd[3] = { PN5180_LOAD_RF_CONFIG, txConf, rxConf };

SPI.beginTransaction(PN5180_SPI_SETTINGS);
pn5180_spi.beginTransaction(PN5180_SPI_SETTINGS);
transceiveCommand(cmd, 3);
SPI.endTransaction();
pn5180_spi.endTransaction();

return true;
}
Expand All @@ -420,9 +424,9 @@ bool PN5180::setRF_on() {

uint8_t cmd[2] = { PN5180_RF_ON, 0x00 };

SPI.beginTransaction(PN5180_SPI_SETTINGS);
pn5180_spi.beginTransaction(PN5180_SPI_SETTINGS);
transceiveCommand(cmd, 2);
SPI.endTransaction();
pn5180_spi.endTransaction();

{
unsigned long t = millis();
Expand All @@ -447,9 +451,9 @@ bool PN5180::setRF_off() {

uint8_t cmd[2] { PN5180_RF_OFF, 0x00 };

SPI.beginTransaction(PN5180_SPI_SETTINGS);
pn5180_spi.beginTransaction(PN5180_SPI_SETTINGS);
transceiveCommand(cmd, 2);
SPI.endTransaction();
pn5180_spi.endTransaction();

{
unsigned long t = millis();
Expand Down Expand Up @@ -526,7 +530,7 @@ bool PN5180::transceiveCommand(uint8_t *sendBuffer, size_t sendBufferLen, uint8_
digitalWrite(PN5180_NSS, LOW); delay(2);
// 2.
for (uint8_t i=0; i<sendBufferLen; i++) {
SPI.transfer(sendBuffer[i]);
pn5180_spi.transfer(sendBuffer[i]);
}
// 3.
{
Expand Down Expand Up @@ -561,7 +565,7 @@ bool PN5180::transceiveCommand(uint8_t *sendBuffer, size_t sendBufferLen, uint8_
digitalWrite(PN5180_NSS, LOW); delay(2);
// 2.
for (uint8_t i=0; i<recvBufferLen; i++) {
recvBuffer[i] = SPI.transfer(0xff);
recvBuffer[i] = pn5180_spi.transfer(0xff);
}
// 3.
{
Expand Down
1 change: 1 addition & 0 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ lib_deps =
adafruit/Adafruit NeoPixel
adafruit/Adafruit PN532@1.3.4
chris--a/Keypad@^3.1.1
lovyan03/LovyanGFX@^1.1.16

[env:esp32dev]
board = esp32dev
Expand Down
Loading