-
Notifications
You must be signed in to change notification settings - Fork 6
feat: TFT display support (ST7789 240x240) with DisplayI interface #52
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 10 commits
c012584
f6b46f3
4f2b548
4234219
094f30c
c180584
f98cbc3
be8e627
b4b4477
e78bc30
8477227
6c9a702
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,167 @@ | ||
| # Overview: | ||
| 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. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. (QB_NEW_EN_HYPHEN) 🤖 Prompt for AI Agents |
||
| 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 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) | ||
|
|
@@ -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); | ||
|
|
@@ -81,7 +85,7 @@ void PN5180::begin() { | |
|
|
||
| void PN5180::end() { | ||
| digitalWrite(PN5180_NSS, HIGH); // disable | ||
| SPI.end(); | ||
| pn5180_spi.end(); | ||
| } | ||
|
|
||
| /* | ||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Return the result of This wrapper still discards the 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 |
||
|
|
||
| return true; | ||
| } | ||
|
|
@@ -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; | ||
| } | ||
|
|
@@ -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; | ||
| } | ||
|
|
@@ -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)); | ||
|
|
@@ -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; | ||
| } | ||
|
|
@@ -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: ")); | ||
|
|
@@ -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; | ||
| } | ||
|
|
@@ -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: ")); | ||
|
|
@@ -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; | ||
| } | ||
|
|
@@ -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(); | ||
|
|
@@ -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(); | ||
|
|
@@ -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. | ||
| { | ||
|
|
@@ -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. | ||
| { | ||
|
|
||
There was a problem hiding this comment.
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