Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions src/DisplayI.h
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
};
101 changes: 101 additions & 0 deletions src/TFTManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Comment on lines +747 to +770
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

TFT task is not restarted after OTA failure—is this intentional?

After freeForOTA() deletes the TFT task, if OTA fails the task is never restarted. This means subsequent display calls (e.g., showReady(), showSpool()) will queue messages to _messageQueue with no consumer, leaving the TFT stuck on the error screen.

This is likely acceptable since OTA failure typically requires user intervention (power cycle or retry), but consider documenting this behavior or adding a note that manual reboot is expected after OTA failure.

🧰 Tools
🪛 Clang (14.0.6)

[warning] 746-746: variable 'cx' is not initialized

(cppcoreguidelines-init-variables)


[warning] 746-746: variable name 'cx' is too short, expected at least 3 characters

(readability-identifier-length)


[warning] 764-764: implicit conversion 'const char *' -> bool

(readability-implicit-bool-conversion)


[warning] 764-764: implicit conversion 'char' -> bool

(readability-implicit-bool-conversion)

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

In `@src/TFTManager.cpp` around lines 745 - 768, TFT task is never restarted after
OTA failure so queued messages on _messageQueue have no consumer; update
TFTManager::showOTAError (or the OTA failure handling path after freeForOTA())
to either restart the TFT task (the same routine that creates the TFT task,
e.g., the start/create task method used elsewhere) or explicitly document that a
manual reboot is required—ensure the fix reinitializes whatever freeForOTA()
torn down so subsequent calls like showReady() and showSpool() are served, and
reference _messageQueue so the consumer is restored.

5 changes: 5 additions & 0 deletions src/TFTManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 23 additions & 1 deletion src/WebServerManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
#include "ConversionUtils.h"
#include "TigerTagParser.h"
#include "HomeAssistantManager.h"
#include "DisplayI.h"

extern "C" {
#include "openprinttag_lib.h"
Expand Down Expand Up @@ -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();

Expand All @@ -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;
}
Expand All @@ -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;
}
Expand All @@ -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);
}
}
}
}
}
Expand All @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions src/WebServerManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
#include <WebServer.h>
#endif

class DisplayI;

class WebServerManager {
public:
static WebServerManager& getInstance();
Expand All @@ -15,6 +17,7 @@ class WebServerManager {

// Call from loop() — processes pending HTTP requests.
void handleClient();
void setDisplay(DisplayI* display) { _display = display; }

private:
WebServerManager() = default;
Expand Down Expand Up @@ -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
};
Expand Down
1 change: 1 addition & 0 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 ===");
Expand Down