diff --git a/src/DisplayI.h b/src/DisplayI.h index a9bb027..b8dbdbd 100644 --- a/src/DisplayI.h +++ b/src/DisplayI.h @@ -34,4 +34,9 @@ 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; } + virtual void showOTAError(const char* error) { (void)error; } }; diff --git a/src/TFTManager.cpp b/src/TFTManager.cpp index 908aff9..f6c4666 100644 --- a/src/TFTManager.cpp +++ b/src/TFTManager.cpp @@ -667,3 +667,104 @@ 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. + // 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; + 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); +} + +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 18eaa41..4281b49 100644 --- a/src/TFTManager.h +++ b/src/TFTManager.h @@ -73,6 +73,11 @@ 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 showOTAError(const char* error) override; + void setScreenTimeoutMs(uint32_t timeoutMs) override; // DisplayI interface 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 ===");