diff --git a/include/BoardPins.h b/include/BoardPins.h index c8fd65f..0f3ccb8 100644 --- a/include/BoardPins.h +++ b/include/BoardPins.h @@ -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 @@ -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 diff --git a/lib/PN5180/PN5180.cpp b/lib/PN5180/PN5180.cpp index e4eff54..dfd4e5d 100644 --- a/lib/PN5180/PN5180.cpp +++ b/lib/PN5180/PN5180.cpp @@ -19,9 +19,13 @@ //#define DEBUG 1 #include +#include #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(); 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(millis() - tagRemovedAtMs); if (elapsedMs >= TAG_REMOVED_STATUS_DELAY_MS) { - // Don't overwrite LCD if user is typing a tool number - if (keypadBufferLen_ == 0) { + // Don't overwrite display if user is typing a tool number. + // On TFT, keep showing spool data — screen timeout handles dimming. +#ifndef NATIVE_TEST + bool isTft = ConfigurationManager::getInstance().isTftEnabled(); +#else + bool isTft = false; +#endif + if (keypadBufferLen_ == 0 && !isTft) { showStatusOnLCD(); } pendingStatusAfterTagRemoved = false; @@ -83,7 +89,7 @@ void ApplicationManager::processMessages() { // Check for delayed Type/Remain display - if (pendingTypeRemainDisplay && lcdManager) { + if (pendingTypeRemainDisplay && display_) { uint32_t elapsedMs = static_cast(millis() - typeRemainScheduledAtMs); if (elapsedMs >= TYPE_REMAIN_DISPLAY_DELAY_MS) { char line1[17]; @@ -94,7 +100,7 @@ void ApplicationManager::processMessages() { } else { snprintf(line2, sizeof(line2), "Remain: %.0fg", delayedDisplayKgRemaining * 1000.0f); } - lcdManager->updateScreen(line1, line2); + display_->showText(line1, line2); pendingTypeRemainDisplay = false; @@ -106,7 +112,7 @@ void ApplicationManager::processMessages() { } void ApplicationManager::showStatusOnLCD() { - if (lcdManager == nullptr) { + if (display_ == nullptr) { return; } @@ -146,7 +152,7 @@ void ApplicationManager::showStatusOnLCD() { char line2[17]; snprintf(line1, sizeof(line1), "NFC+ Wifi%c", wifiInd); snprintf(line2, sizeof(line2), "SM%c MQTT%c", smInd, haInd); - lcdManager->updateScreen(line1, line2); + display_->showText(line1, line2); } void ApplicationManager::scheduleTypeRemainDisplay(const char* material_name, float kg_remaining) { @@ -230,10 +236,10 @@ void ApplicationManager::handlePrintStarted(const AppMessage& msg) { startingSpoolId[0] = '\0'; spoolChangedDuringPrint = false; - if (lcdManager) { + if (display_) { char line2[17]; snprintf(line2, sizeof(line2), "Job: %d", currentJobId); - lcdManager->updateScreen("Print Started", line2); + display_->showText("Print Started", line2); } // Publish printer state to HA @@ -332,18 +338,27 @@ void ApplicationManager::handleSpoolDetected(const AppMessage& msg) { pendingTypeRemainDisplay = false; pendingStatusAfterTagRemoved = false; - // Update LCD with spool info (dedupe by spool_id) - if (lcdManager && strcmp(lastDisplayedSpoolId, msg.payload.spoolDetected.spool_id) != 0) { + // Update display with spool info (dedupe by spool_id) + if (display_ && strcmp(lastDisplayedSpoolId, msg.payload.spoolDetected.spool_id) != 0) { strncpy(lastDisplayedSpoolId, msg.payload.spoolDetected.spool_id, sizeof(lastDisplayedSpoolId) - 1); lastDisplayedSpoolId[sizeof(lastDisplayedSpoolId) - 1] = '\0'; - lastDisplayedBlankId[0] = '\0'; // Clear so blank tag re-displays if swapped + lastDisplayedBlankId[0] = '\0'; - char line1[17]; - char line2[17]; - snprintf(line1, sizeof(line1), "Type: %.10s", msg.payload.spoolDetected.material_name); - snprintf(line2, sizeof(line2), "Remain: %.0fg", msg.payload.spoolDetected.kg_remaining * 1000.0f); - lcdManager->updateScreen("**** Spool ****", "*** Scanned ***", line1, line2); - } else if (lcdManager) { + const auto& s = msg.payload.spoolDetected; + DisplaySpoolData spool{}; + strncpy(spool.brand, s.manufacturer, sizeof(spool.brand) - 1); + strncpy(spool.material, s.material_name, sizeof(spool.material) - 1); + snprintf(spool.colorHex, sizeof(spool.colorHex), "%02X%02X%02X", + s.primary_color[0], s.primary_color[1], s.primary_color[2]); + spool.remainingWeight = s.kg_remaining * 1000.0f; + spool.totalWeight = s.initial_weight_g; + // Map tag format string to tag type constant + if (strcmp(s.tag_format, "OpenPrintTag") == 0) spool.tagType = 1; + else if (strcmp(s.tag_format, "TigerTag") == 0) spool.tagType = 2; + else if (strcmp(s.tag_format, "OpenTag3D") == 0) spool.tagType = 3; + else spool.tagType = 0; + display_->showSpool(spool); + } else if (display_) { Serial.printf("ApplicationManager: Skipping LCD update for already displayed spool %s\n", msg.payload.spoolDetected.spool_id); } @@ -430,19 +445,19 @@ void ApplicationManager::handleSpoolUpdated(const AppMessage& msg) { } #endif - if (lcdManager) { + if (display_) { if (msg.payload.spoolUpdated.success) { char line1[17]; snprintf(line1, sizeof(line1), "Updated: %.0fg", kgRemaining * 1000.0f); if (spoolmanConfigured) { - lcdManager->updateScreen(line1, "Syncing Spoolman"); + display_->showText(line1, "Syncing Spoolman"); // Type/Remain will be scheduled after SPOOLMAN_SYNCED } else { char line2[17]; snprintf(line2, sizeof(line2), "Remain: %.0fg", kgRemaining * 1000.0f); - lcdManager->updateScreen("Spool Updated!", line2); + display_->showText("Spool Updated!", line2); // Schedule Type/Remain display after 5 seconds (no Spoolman path) if (materialName[0] != '\0') { @@ -450,7 +465,7 @@ void ApplicationManager::handleSpoolUpdated(const AppMessage& msg) { } } } else { - lcdManager->updateScreen("Spool Update", "Failed!"); + display_->showText("Spool Update", "Failed!"); } } @@ -521,12 +536,12 @@ void ApplicationManager::handleBlankTagDetected(const AppMessage& msg) { pendingStatusAfterTagRemoved = false; - if (lcdManager && strcmp(lastDisplayedBlankId, msg.payload.blankTag.spool_id) != 0) { + if (display_ && strcmp(lastDisplayedBlankId, msg.payload.blankTag.spool_id) != 0) { strncpy(lastDisplayedBlankId, msg.payload.blankTag.spool_id, sizeof(lastDisplayedBlankId) - 1); lastDisplayedBlankId[sizeof(lastDisplayedBlankId) - 1] = '\0'; lastDisplayedSpoolId[0] = '\0'; // Clear so valid spool re-displays if swapped - lcdManager->updateScreen("**** Spool ****", "*** Scanned ***", "Unknown Tag", "Use app to setup"); + display_->showText4("**** Spool ****", "*** Scanned ***", "Unknown Tag", "Use app to setup"); } // Publish blank tag state to HA @@ -555,12 +570,12 @@ void ApplicationManager::handleGenericTagDetected(const AppMessage& msg) { pendingStatusAfterTagRemoved = false; - if (lcdManager && strcmp(lastDisplayedBlankId, msg.payload.genericTag.spool_id) != 0) { + if (display_ && strcmp(lastDisplayedBlankId, msg.payload.genericTag.spool_id) != 0) { strncpy(lastDisplayedBlankId, msg.payload.genericTag.spool_id, sizeof(lastDisplayedBlankId) - 1); lastDisplayedBlankId[sizeof(lastDisplayedBlankId) - 1] = '\0'; lastDisplayedSpoolId[0] = '\0'; - lcdManager->updateScreen("**** Spool ****", "*** Scanned ***", "Generic Tag", "Checking Spoolman"); + display_->showText4("**** Spool ****", "*** Scanned ***", "Generic Tag", "Checking Spoolman"); } // Publish generic tag state to HA (pre-lookup) @@ -589,16 +604,16 @@ void ApplicationManager::handleGenericTagDetected(const AppMessage& msg) { void ApplicationManager::finishPrint(float gramsUsed, bool /*canceled*/) { if (spoolChangedDuringPrint) { Serial.println("ApplicationManager: Spool changed during print - not updating weight"); - if (lcdManager) { - lcdManager->updateScreen("Spool changed!", "No update"); + if (display_) { + display_->showText("Spool changed!", "No update"); } return; } if (startingSpoolId[0] == '\0') { Serial.println("ApplicationManager: No spool detected during print - not updating weight"); - if (lcdManager) { - lcdManager->updateScreen("No spool found", "No update"); + if (display_) { + display_->showText("No spool found", "No update"); } return; } @@ -609,8 +624,8 @@ void ApplicationManager::finishPrint(float gramsUsed, bool /*canceled*/) { // Only auto-update NFC tag in SELF_DIRECTED mode if (automationMode == AutomationMode::SELF_DIRECTED) { - if (lcdManager) { - lcdManager->updateScreen("Updating spool..", ""); + if (display_) { + display_->showText("Updating spool..", ""); } // Enqueue write request with expected spool ID @@ -623,14 +638,14 @@ void ApplicationManager::finishPrint(float gramsUsed, bool /*canceled*/) { NFCManager::getInstance().enqueueWrite(request); } else { - if (lcdManager) { - lcdManager->updateScreen("Print done", "HA controlled"); + if (display_) { + display_->showText("Print done", "HA controlled"); } } } else { Serial.println("ApplicationManager: No filament used - not updating spool"); - if (lcdManager) { - lcdManager->updateScreen("Print done", "No filament used"); + if (display_) { + display_->showText("Print done", "No filament used"); } } } @@ -667,26 +682,28 @@ void ApplicationManager::handleSpoolmanSynced(const AppMessage& msg) { } #endif - if (lcdManager) { - if (msg.payload.spoolmanSynced.success) { - char line1[17]; - char line2[17]; - snprintf(line1, sizeof(line1), "Type: %.10s", materialName); - if (ConfigurationManager::getInstance().isKeypadEnabled()) { - snprintf(line2, sizeof(line2), "%.0fg Tool#? #", - kgRemaining * 1000.0f); - } else { - snprintf(line2, sizeof(line2), "Remain: %.0fg", - kgRemaining * 1000.0f); - } - lcdManager->updateScreen(line1, line2); + if (display_) { + if (msg.payload.spoolmanSynced.success && msg.payload.spoolmanSynced.is_uid_lookup) { + // UID lookup — show spool graphic with Spoolman data (tag had no data) + DisplaySpoolData spool{}; + strncpy(spool.brand, msg.payload.spoolmanSynced.manufacturer, sizeof(spool.brand) - 1); + strncpy(spool.material, materialName, sizeof(spool.material) - 1); + const char* colorSrc = msg.payload.spoolmanSynced.color_hex; + if (colorSrc[0] == '#') colorSrc++; + strncpy(spool.colorHex, colorSrc, sizeof(spool.colorHex) - 1); + spool.remainingWeight = kgRemaining * 1000.0f; + spool.totalWeight = msg.payload.spoolmanSynced.initial_weight_g; + spool.tagType = 5; + display_->showSpool(spool); + } else if (msg.payload.spoolmanSynced.success) { + // Smart tag — don't overwrite, handleSpoolDetected already showed correct data } else if (msg.payload.spoolmanSynced.is_uid_lookup) { - lcdManager->updateScreen("Generic Tag", "Not in Spoolman"); + display_->showText("Generic Tag", "Not in Spoolman"); } else { char line1[17]; snprintf(line1, sizeof(line1), "Updated: %.0fg", kgRemaining * 1000.0f); - lcdManager->updateScreen(line1, "Spoolman Error"); + display_->showText(line1, "Spoolman Error"); // Schedule Type/Remain display after 5 seconds even on error if (materialName[0] != '\0') { @@ -878,15 +895,15 @@ void ApplicationManager::handlePrinterWarning(const AppMessage& msg) { w.warning_type, w.expected, w.actual); // Show on LCD - if (lcdManager) { + if (display_) { if (strcmp(w.warning_type, "filament_mismatch") == 0) { char line2[17]; snprintf(line2, sizeof(line2), "%.3s!=%.3s WRONG!", w.expected, w.actual); - lcdManager->updateScreen("WRONG FILAMENT!", line2); + display_->showText("WRONG FILAMENT!", line2); } else if (strcmp(w.warning_type, "temp_exceeds_max") == 0) { char line2[17]; snprintf(line2, sizeof(line2), "%.0fC>%dC max", w.gcode_temp, w.tag_max_temp); - lcdManager->updateScreen("TEMP WARNING!", line2); + display_->showText("TEMP WARNING!", line2); } } @@ -944,10 +961,8 @@ void ApplicationManager::handleKeypadDigit(const AppMessage& msg) { keypadBuffer_[keypadBufferLen_] = '\0'; } - if (lcdManager) { - char line[17]; - snprintf(line, sizeof(line), "Assign to: T%s", keypadBuffer_); - lcdManager->updateScreen(line, "# Confirm * Clr", "", ""); + if (display_) { + display_->showKeypad(keypadBuffer_); } } @@ -955,7 +970,7 @@ void ApplicationManager::handleKeypadConfirm() { Serial.println("EVENT: KeypadConfirm"); if (keypadBufferLen_ == 0) { - if (lcdManager) lcdManager->updateScreen("No tool entered", "Type number + #"); + if (display_) display_->showText("No tool entered", "Type number + #"); return; } @@ -965,7 +980,7 @@ void ApplicationManager::handleKeypadConfirm() { bool hasScannedSpool = NFCManager::getInstance().getCurrentSpoolState(state) && (state.present || state.spool_id[0] != '\0'); if (!hasScannedSpool) { - if (lcdManager) lcdManager->updateScreen("No spool scanned", "Scan tag first"); + if (display_) display_->showText("No spool scanned", "Scan tag first"); keypadBuffer_[0] = '\0'; keypadBufferLen_ = 0; return; @@ -973,10 +988,10 @@ void ApplicationManager::handleKeypadConfirm() { #endif if (sendAssignSpool(keypadBuffer_)) { - if (lcdManager) { + if (display_) { char line[17]; snprintf(line, sizeof(line), "Assigned T%s", keypadBuffer_); - lcdManager->updateScreen(line, "OK"); + display_->showText(line, "OK"); } } @@ -989,8 +1004,8 @@ void ApplicationManager::handleKeypadCancel() { keypadBuffer_[0] = '\0'; keypadBufferLen_ = 0; - if (lcdManager) { - lcdManager->updateScreen("Tool entry", "Cleared"); + if (display_) { + display_->showText("Tool entry", "Cleared"); } } @@ -1007,14 +1022,14 @@ bool ApplicationManager::sendAssignSpool(const char* toolNumber) { const char* moonrakerUrl = ConfigurationManager::getInstance().getMoonrakerURL(); if (!moonrakerUrl || moonrakerUrl[0] == '\0') { Serial.println("ApplicationManager: Moonraker URL not configured — cannot assign spool"); - if (lcdManager) lcdManager->updateScreen("Moonraker URL", "Not configured"); + if (display_) display_->showText("Moonraker URL", "Not configured"); return false; } extern SemaphoreHandle_t g_httpMutex; if (g_httpMutex && xSemaphoreTake(g_httpMutex, pdMS_TO_TICKS(3000)) != pdTRUE) { Serial.println("ApplicationManager: Could not acquire HTTP mutex for ASSIGN_SPOOL"); - if (lcdManager) lcdManager->updateScreen("Assign failed", "HTTP busy"); + if (display_) display_->showText("Assign failed", "HTTP busy"); return false; } @@ -1041,7 +1056,7 @@ bool ApplicationManager::sendAssignSpool(const char* toolNumber) { Serial.printf("ApplicationManager: ASSIGN_SPOOL T%s — HTTP %d\n", toolNumber, code); if (code != 200) { - if (lcdManager) lcdManager->updateScreen("Assign failed", "Check Moonraker"); + if (display_) display_->showText("Assign failed", "Check Moonraker"); return false; } return true; diff --git a/src/ApplicationManager.h b/src/ApplicationManager.h index 91db18a..9f1ddf5 100644 --- a/src/ApplicationManager.h +++ b/src/ApplicationManager.h @@ -11,7 +11,7 @@ #include #endif -class LCDManager; +class DisplayI; enum class AppMessageType { PRINT_STARTED, @@ -82,6 +82,7 @@ struct SpoolmanSyncedPayload { char spool_id[17]; bool success; float kg_remaining; // Remaining weight for LCD display + float initial_weight_g; // Initial spool weight (for weight bar display) int32_t spoolman_id; // Resolved Spoolman spool ID (-1 if unknown) char material_name[32]; // Carried from sync request — avoids re-reading NFC state char manufacturer[64]; // Vendor name (populated for UID lookups) @@ -151,7 +152,7 @@ class ApplicationManager { public: static ApplicationManager& getInstance(); - bool begin(LCDManager* lcd = nullptr); + bool begin(DisplayI* display = nullptr); bool sendMessage(const AppMessage& msg, uint32_t waitMs = 0); void processMessages(); void showStatusOnLCD(); @@ -167,7 +168,7 @@ class ApplicationManager { #ifdef NATIVE_TEST void resetForTest() { if (messageQueue) { vQueueDelete(messageQueue); messageQueue = nullptr; } - lcdManager = nullptr; + display_ = nullptr; currentState = AppState::IDLE; startingSpoolId[0] = '\0'; currentJobId = 0; @@ -194,8 +195,8 @@ class ApplicationManager { QueueHandle_t messageQueue = nullptr; static constexpr size_t QUEUE_SIZE = 12; - // LCD reference - LCDManager* lcdManager = nullptr; + // Display reference (LCD or TFT) + DisplayI* display_ = nullptr; // State machine AppState currentState = AppState::IDLE; diff --git a/src/ConfigHTML.h b/src/ConfigHTML.h index a88975a..25a389f 100644 --- a/src/ConfigHTML.h +++ b/src/ConfigHTML.h @@ -192,6 +192,13 @@ const char CONFIG_HTML[] PROGMEM = R"rawliteral( +
+ TFT Display (ST7789 240x240) + +
NFC Reader + +
- - -
- -
- - -
- -
- - + +
@@ -213,13 +203,25 @@ const char UID_REGISTRATION_HTML[] PROGMEM = R"rawliteral( // Auto-fill temps and density from material selection var nfcFieldMap = { - minPrintTemp: 'min_print_temp', maxPrintTemp: 'max_print_temp', - minBedTemp: 'min_bed_temp', maxBedTemp: 'max_bed_temp', density: 'density' }; - trackAutoFill(['min_print_temp','max_print_temp','min_bed_temp','max_bed_temp','density']); + trackAutoFill(['extruder_temp','bed_temp','density']); materialTypeEl.addEventListener('input', function() { autoFillMaterialData(materialTypeEl.value, nfcFieldMap); + // Average min/max temps into single fields + var m = lookupMaterial(materialTypeEl.value); + if (m) { + var et = document.getElementById('extruder_temp'); + if (et && et.dataset.autoFilled !== 'false' && m.minPrintTemp && m.maxPrintTemp) { + et.value = Math.round((m.minPrintTemp + m.maxPrintTemp) / 2); + et.dataset.autoFilled = 'true'; + } + var bt = document.getElementById('bed_temp'); + if (bt && bt.dataset.autoFilled !== 'false' && m.minBedTemp && m.maxBedTemp) { + bt.value = Math.round((m.minBedTemp + m.maxBedTemp) / 2); + bt.dataset.autoFilled = 'true'; + } + } }); loadMaterialDb().then(function(db) { var dl = document.getElementById('material-list'); @@ -352,10 +354,8 @@ const char UID_REGISTRATION_HTML[] PROGMEM = R"rawliteral( remaining_g: remainingWeight, density: readPositiveNumber('density') || 0, diameter_mm: readPositiveNumber('diameter_mm') || 1.75, - min_print_temp: readPositiveNumber('min_print_temp') || 0, - max_print_temp: readPositiveNumber('max_print_temp') || 0, - min_bed_temp: readPositiveNumber('min_bed_temp') || 0, - max_bed_temp: readPositiveNumber('max_bed_temp') || 0 + extruder_temp: readPositiveNumber('extruder_temp') || 0, + bed_temp: readPositiveNumber('bed_temp') || 0 }; setResult('Registering in Spoolman\u2026', ''); diff --git a/src/WebServerManager.cpp b/src/WebServerManager.cpp index 2532e8b..57db953 100644 --- a/src/WebServerManager.cpp +++ b/src/WebServerManager.cpp @@ -248,10 +248,8 @@ void WebServerManager::handleApiRegisterUid() { 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; + int extruderTemp = doc["extruder_temp"] | 0; + int bedTemp = doc["bed_temp"] | 0; if (strlen(uid) == 0) { sendError(400, "UID is required"); @@ -323,6 +321,8 @@ void WebServerManager::handleApiRegisterUid() { filBody["density"] = density > 0 ? density : 1.24f; filBody["diameter"] = diameter > 0 ? diameter : 1.75f; if (strlen(color) > 0) filBody["color_hex"] = color; + if (extruderTemp > 0) filBody["settings_extruder_temp"] = extruderTemp; + if (bedTemp > 0) filBody["settings_bed_temp"] = bedTemp; String filJson; serializeJson(filBody, filJson); @@ -518,6 +518,7 @@ void WebServerManager::handleApiGetConfig() { doc["prusalink_url"] = cfg.prusalink_url; doc["prusalink_key_set"] = (cfg.prusalink_api_key[0] != '\0'); doc["nfc_reader"] = cfg.nfc_reader; + doc["tft_enabled"] = cfg.tft_enabled; doc["ap_mode"] = _apMode; if (_apMode) { extern char g_apSSID[]; @@ -554,6 +555,11 @@ void WebServerManager::handleApiPostConfig() { update.lcd_enabled = doc["lcd_enabled"] | (uint8_t)0; update.led_enabled = doc["led_enabled"] | (uint8_t)0; update.keypad_enabled = doc["keypad_enabled"] | (uint8_t)0; + update.tft_enabled = doc["tft_enabled"] | (uint8_t)0; + // TFT and LCD share GPIO 22/23 on WROOM — auto-disable LCD when TFT enabled + if (update.tft_enabled && update.lcd_enabled) { + update.lcd_enabled = 0; + } strncpy(update.moonraker_url, doc["moonraker_url"] | "", sizeof(update.moonraker_url) - 1); update.prusalink_on = doc["prusalink_on"] | (uint8_t)0; strncpy(update.prusalink_url, doc["prusalink_url"] | "", sizeof(update.prusalink_url) - 1); diff --git a/src/main.cpp b/src/main.cpp index 8da59a8..8e125ac 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -8,7 +8,9 @@ #include "NFCManager.h" #include "SpoolmanManager.h" #include "HomeAssistantManager.h" +#include "DisplayI.h" #include "LCDManager.h" +#include "TFTManager.h" #include "LEDManager.h" #include "WebServerManager.h" #include "PrinterManager.h" @@ -32,6 +34,9 @@ static PrusaLinkStrategy prusaLinkStrategy; // Always declared; only initialized if isLcdEnabled() at runtime LCDManager lcdManager(0x27, 16, 2); +// TFT display — replaces LCD when enabled via NVS +TFTManager tftManager; + // Always declared; only initialized if isLedEnabled() at runtime LEDManager ledManager; @@ -55,7 +60,9 @@ void startAPMode() { Serial.printf("AP started: %s @ 192.168.4.1\n", g_apSSID); auto& config = ConfigurationManager::getInstance(); - if (config.isLcdEnabled()) { + if (config.isTftEnabled()) { + tftManager.showError(g_apSSID); + } else if (config.isLcdEnabled()) { lcdManager.updateScreen(g_apSSID, "Go to 192.168.4.1"); } if (config.isLedEnabled()) { @@ -75,7 +82,9 @@ void initWiFi() { Serial.print("Connecting to WiFi: "); Serial.println(config.getWiFiSSID()); - if (config.isLcdEnabled()) { + if (config.isTftEnabled()) { + tftManager.showWifiConnecting(); + } else if (config.isLcdEnabled()) { lcdManager.updateScreen("Connecting WiFi", ""); } @@ -93,7 +102,9 @@ void initWiFi() { Serial.print("WiFi connected! IP: "); Serial.println(WiFi.localIP()); - if (config.isLcdEnabled()) { + if (config.isTftEnabled()) { + tftManager.showWifiConnected(WiFi.localIP().toString().c_str()); + } else if (config.isLcdEnabled()) { lcdManager.updateScreen("WiFi OK", WiFi.localIP().toString().c_str()); } @@ -141,7 +152,13 @@ void setup() { ledManager.showBooting(); } - if (config.isLcdEnabled()) { + if (config.isTftEnabled()) { + // TFT display — mutually exclusive with LCD on WROOM + tftManager.begin(); + tftManager.startTask(); + tftManager.showBoot(FIRMWARE_VERSION); + Serial.println("TFT initialized"); + } else if (config.isLcdEnabled()) { // Initialize I2C with custom pins for LCD Wire.begin(PIN_LCD_SDA, PIN_LCD_SCL); Serial.println("I2C initialized"); @@ -158,8 +175,14 @@ void setup() { InputManager::getInstance().begin(); } - // Initialize ApplicationManager (message queue) with LCD reference - if (!ApplicationManager::getInstance().begin(config.isLcdEnabled() ? &lcdManager : nullptr)) { + // Initialize ApplicationManager (message queue) with display reference + DisplayI* activeDisplay = nullptr; + if (config.isTftEnabled()) { + activeDisplay = &tftManager; + } else if (config.isLcdEnabled()) { + activeDisplay = &lcdManager; + } + if (!ApplicationManager::getInstance().begin(activeDisplay)) { Serial.println("ApplicationManager init failed - halting"); while (1) { delay(1000); } } @@ -243,7 +266,9 @@ void setup() { } } - if (config.isLcdEnabled()) { + if (config.isTftEnabled()) { + tftManager.showReady(); + } else if (config.isLcdEnabled()) { ApplicationManager::getInstance().showStatusOnLCD(); }