From a3ca85b4d4d97ba731c706af9074f4eb98fa3e3b Mon Sep 17 00:00:00 2001 From: Shane Jordan Date: Mon, 30 Mar 2026 20:08:43 -0500 Subject: [PATCH 1/4] feat: add freeForOTA/updateOTAProgress to DisplayI interface --- src/DisplayI.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/DisplayI.h b/src/DisplayI.h index a9bb027..0d8ffb6 100644 --- a/src/DisplayI.h +++ b/src/DisplayI.h @@ -34,4 +34,8 @@ class DisplayI { // Screen timeout virtual void setScreenTimeoutMs(uint32_t timeoutMs) = 0; + + // OTA support — TFT frees sprite to reclaim heap for SSL + virtual void freeForOTA() {} + virtual void updateOTAProgress(uint8_t percent) { (void)percent; } }; From b83b4c680d3f0496125ce88f331c7e9dc8282f1a Mon Sep 17 00:00:00 2001 From: Shane Jordan Date: Mon, 30 Mar 2026 20:10:55 -0500 Subject: [PATCH 2/4] feat: implement freeForOTA/updateOTAProgress in TFTManager --- src/TFTManager.cpp | 74 ++++++++++++++++++++++++++++++++++++++++++++++ src/TFTManager.h | 4 +++ 2 files changed, 78 insertions(+) diff --git a/src/TFTManager.cpp b/src/TFTManager.cpp index 908aff9..3385456 100644 --- a/src/TFTManager.cpp +++ b/src/TFTManager.cpp @@ -667,3 +667,77 @@ void TFTManager::showSpool(const DisplaySpoolData& spool) { tftSpool.tagType = spool.tagType; showSpoolScanned(tftSpool); } + +// --------------------------------------------------------------------------- +// OTA support — free sprite for SSL heap, render directly to panel +// --------------------------------------------------------------------------- +void TFTManager::freeForOTA() { + // Stop the TFT task so no queue processing conflicts with direct writes + if (_taskHandle != nullptr) { + vTaskDelete(_taskHandle); + _taskHandle = nullptr; + Serial.println("TFTManager: Task stopped for OTA"); + } + + // Delete sprite to free 57.6KB heap + _sprite.deleteSprite(); + Serial.printf("TFTManager: Sprite freed, heap now %u\n", ESP.getFreeHeap()); + + // Wake screen if it was off + _tft.setBrightness(255); + + // Draw OTA screen directly to panel (no sprite) + _tft.fillScreen(COLOR_BG); + + // Header bar + _tft.fillRect(0, 0, _tft.width(), 28, COLOR_HEADER_BG); + _tft.setTextColor(COLOR_ACCENT); + _tft.setTextSize(1); + _tft.setTextDatum(MC_DATUM); + _tft.drawString("SpoolSense", _tft.width() / 2, 14); + + // "Updating..." text + int cx = _tft.width() / 2; + _tft.setTextColor(COLOR_TEXT); + _tft.setTextSize(2); + _tft.setTextDatum(MC_DATUM); + _tft.drawString("Updating...", cx, 80); + + // Progress bar outline (same position updateOTAProgress will fill) + int barX = 20; + int barY = 120; + int barW = _tft.width() - 40; // 200px on 240px display + int barH = 20; + _tft.drawRoundRect(barX, barY, barW, barH, barH / 2, 0x555555); + + // "0%" text + _tft.setTextColor(COLOR_SUBTEXT); + _tft.setTextSize(1); + _tft.setTextDatum(MC_DATUM); + _tft.drawString("0%", cx, 155); +} + +void TFTManager::updateOTAProgress(uint8_t percent) { + if (percent > 100) percent = 100; + + int barX = 20; + int barY = 120; + int barW = _tft.width() - 40; // 200px + int barH = 20; + int cx = _tft.width() / 2; + + // Fill progress bar + int filled = (barW * percent) / 100; + if (filled > 0) { + _tft.fillRoundRect(barX, barY, filled, barH, barH / 2, COLOR_ACCENT); + } + + // Clear and redraw percentage text + _tft.fillRect(cx - 30, 148, 60, 16, COLOR_BG); + char pctStr[8]; + snprintf(pctStr, sizeof(pctStr), "%u%%", percent); + _tft.setTextColor(COLOR_SUBTEXT); + _tft.setTextSize(1); + _tft.setTextDatum(MC_DATUM); + _tft.drawString(pctStr, cx, 155); +} diff --git a/src/TFTManager.h b/src/TFTManager.h index 18eaa41..920a584 100644 --- a/src/TFTManager.h +++ b/src/TFTManager.h @@ -73,6 +73,10 @@ class TFTManager : public DisplayI { void showKeypadEntry(const char* toolNumber); void showError(const char* msg); + // OTA support — free sprite heap, render progress directly to panel + void freeForOTA() override; + void updateOTAProgress(uint8_t percent) override; + void setScreenTimeoutMs(uint32_t timeoutMs) override; // DisplayI interface From 877f5cb42c5d9fff1adb3794cf22beac5f4118ea Mon Sep 17 00:00:00 2001 From: Shane Jordan Date: Mon, 30 Mar 2026 20:23:22 -0500 Subject: [PATCH 3/4] feat: wire TFT OTA progress into WebServerManager download task --- src/DisplayI.h | 1 + src/TFTManager.cpp | 25 +++++++++++++++++++++++++ src/TFTManager.h | 1 + src/WebServerManager.cpp | 24 +++++++++++++++++++++++- src/WebServerManager.h | 5 +++++ src/main.cpp | 1 + 6 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/DisplayI.h b/src/DisplayI.h index 0d8ffb6..b8dbdbd 100644 --- a/src/DisplayI.h +++ b/src/DisplayI.h @@ -38,4 +38,5 @@ class DisplayI { // OTA support — TFT frees sprite to reclaim heap for SSL virtual void freeForOTA() {} virtual void updateOTAProgress(uint8_t percent) { (void)percent; } + virtual void showOTAError(const char* error) { (void)error; } }; diff --git a/src/TFTManager.cpp b/src/TFTManager.cpp index 3385456..14bf7f1 100644 --- a/src/TFTManager.cpp +++ b/src/TFTManager.cpp @@ -741,3 +741,28 @@ void TFTManager::updateOTAProgress(uint8_t percent) { _tft.setTextDatum(MC_DATUM); _tft.drawString(pctStr, cx, 155); } + +void TFTManager::showOTAError(const char* error) { + int cx = _tft.width() / 2; + + // Clear progress area + _tft.fillRect(0, 60, _tft.width(), _tft.height() - 60, COLOR_BG); + + // Error icon + _tft.fillCircle(cx, 100, 22, 0xFF4444); + _tft.setTextColor(COLOR_BG); + _tft.setTextSize(3); + _tft.setTextDatum(MC_DATUM); + _tft.drawString("X", cx, 100); + + // "Update Failed" text + _tft.setTextColor(COLOR_TEXT); + _tft.setTextSize(1); + _tft.drawString("Update Failed", cx, 135); + + // Error detail + if (error && error[0]) { + _tft.setTextColor(COLOR_SUBTEXT); + _tft.drawString(error, cx, 155); + } +} diff --git a/src/TFTManager.h b/src/TFTManager.h index 920a584..4281b49 100644 --- a/src/TFTManager.h +++ b/src/TFTManager.h @@ -76,6 +76,7 @@ class TFTManager : public DisplayI { // OTA support — free sprite heap, render progress directly to panel void freeForOTA() override; void updateOTAProgress(uint8_t percent) override; + void showOTAError(const char* error) override; void setScreenTimeoutMs(uint32_t timeoutMs) override; diff --git a/src/WebServerManager.cpp b/src/WebServerManager.cpp index e3fd8a9..c21a6c1 100644 --- a/src/WebServerManager.cpp +++ b/src/WebServerManager.cpp @@ -31,6 +31,7 @@ #include "ConversionUtils.h" #include "TigerTagParser.h" #include "HomeAssistantManager.h" +#include "DisplayI.h" extern "C" { #include "openprinttag_lib.h" @@ -823,6 +824,12 @@ void WebServerManager::otaDownloadTask(void* param) { // Pause NFC during OTA NFCManager::getInstance().pauseScanTask(); + // Free TFT sprite to reclaim ~57KB heap for SSL + if (self->_display) { + self->_display->freeForOTA(); + Serial.printf("OTA: Free heap after sprite release: %u\n", ESP.getFreeHeap()); + } + WiFiClientSecure secureClient; secureClient.setInsecure(); @@ -839,6 +846,9 @@ void WebServerManager::otaDownloadTask(void* param) { NFCManager::getInstance().resumeScanTask(); snprintf(self->_otaError, sizeof(self->_otaError), "Download failed: HTTP %d", httpCode); self->_otaState = OtaState::FAILED; + if (self->_display) { + self->_display->showOTAError(self->_otaError); + } vTaskDelete(nullptr); return; } @@ -856,6 +866,9 @@ void WebServerManager::otaDownloadTask(void* param) { NFCManager::getInstance().resumeScanTask(); strncpy(self->_otaError, "Update.begin failed", sizeof(self->_otaError)); self->_otaState = OtaState::FAILED; + if (self->_display) { + self->_display->showOTAError(self->_otaError); + } vTaskDelete(nullptr); return; } @@ -873,7 +886,13 @@ void WebServerManager::otaDownloadTask(void* param) { Update.write(buf, bytesRead); written += bytesRead; if (contentLength > 0) { - self->_otaProgress = (uint8_t)((written * 100) / contentLength); + uint8_t newPct = (uint8_t)((written * 100) / contentLength); + if (newPct != self->_otaProgress) { + self->_otaProgress = newPct; + if (self->_display) { + self->_display->updateOTAProgress(newPct); + } + } } } } @@ -894,6 +913,9 @@ void WebServerManager::otaDownloadTask(void* param) { NFCManager::getInstance().resumeScanTask(); strncpy(self->_otaError, "Update verification failed", sizeof(self->_otaError)); self->_otaState = OtaState::FAILED; + if (self->_display) { + self->_display->showOTAError(self->_otaError); + } } vTaskDelete(nullptr); diff --git a/src/WebServerManager.h b/src/WebServerManager.h index 0e87c85..1d26857 100644 --- a/src/WebServerManager.h +++ b/src/WebServerManager.h @@ -5,6 +5,8 @@ #include #endif +class DisplayI; + class WebServerManager { public: static WebServerManager& getInstance(); @@ -15,6 +17,7 @@ class WebServerManager { // Call from loop() — processes pending HTTP requests. void handleClient(); + void setDisplay(DisplayI* display) { _display = display; } private: WebServerManager() = default; @@ -68,6 +71,8 @@ class WebServerManager { char _otaError[64] = {0}; volatile uint8_t _otaProgress = 0; + DisplayI* _display = nullptr; + void sendError(int code, const char* msg); #endif }; diff --git a/src/main.cpp b/src/main.cpp index 50a9c04..c2d57e2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -282,6 +282,7 @@ void setup() { // Start HTTP server (both STA and AP mode) if (WiFi.status() == WL_CONNECTED || g_apModeActive) { WebServerManager::getInstance().begin(g_apModeActive); + WebServerManager::getInstance().setDisplay(activeDisplay); } Serial.println("=== Setup complete ==="); From f93ae20c596d83a307502064412efb5fc01ae9d9 Mon Sep 17 00:00:00 2001 From: Shane Jordan Date: Mon, 30 Mar 2026 20:37:01 -0500 Subject: [PATCH 4/4] fix: document intentional TFT task non-recovery after OTA failure (code review) --- src/TFTManager.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/TFTManager.cpp b/src/TFTManager.cpp index 14bf7f1..f6c4666 100644 --- a/src/TFTManager.cpp +++ b/src/TFTManager.cpp @@ -672,7 +672,9 @@ void TFTManager::showSpool(const DisplaySpoolData& spool) { // OTA support — free sprite for SSL heap, render directly to panel // --------------------------------------------------------------------------- void TFTManager::freeForOTA() { - // Stop the TFT task so no queue processing conflicts with direct writes + // Stop the TFT task so no queue processing conflicts with direct writes. + // NOTE: The task and sprite are NOT restored after OTA failure — the device + // is expected to reboot (success) or require manual reboot (failure). if (_taskHandle != nullptr) { vTaskDelete(_taskHandle); _taskHandle = nullptr;