Changed OTA to write Directly to flash

This commit is contained in:
2026-01-07 15:46:20 +02:00
parent 51b7722e1d
commit 7adc1fec34
5 changed files with 410 additions and 78 deletions

View File

@@ -9,6 +9,8 @@ OTAManager::OTAManager(ConfigManager& configManager)
: _configManager(configManager) : _configManager(configManager)
, _fileManager(nullptr) , _fileManager(nullptr)
, _player(nullptr) , _player(nullptr)
, _timeKeeper(nullptr)
, _telemetry(nullptr)
, _status(Status::IDLE) , _status(Status::IDLE)
, _lastError(ErrorCode::NONE) , _lastError(ErrorCode::NONE)
, _availableVersion(0.0f) , _availableVersion(0.0f)
@@ -22,10 +24,23 @@ OTAManager::OTAManager(ConfigManager& configManager)
, _progressCallback(nullptr) , _progressCallback(nullptr)
, _statusCallback(nullptr) , _statusCallback(nullptr)
, _scheduledCheckTimer(NULL) , _scheduledCheckTimer(NULL)
, _initialCheckTimer(NULL) { , _initialCheckTimer(NULL)
, _otaWorkerTask(NULL)
, _otaWorkSignal(NULL)
, _pendingWork(OTAWorkType::NONE) {
} }
OTAManager::~OTAManager() { OTAManager::~OTAManager() {
// Clean up worker task and semaphore
if (_otaWorkerTask != NULL) {
vTaskDelete(_otaWorkerTask);
_otaWorkerTask = NULL;
}
if (_otaWorkSignal != NULL) {
vSemaphoreDelete(_otaWorkSignal);
_otaWorkSignal = NULL;
}
if (_scheduledCheckTimer != NULL) { if (_scheduledCheckTimer != NULL) {
xTimerStop(_scheduledCheckTimer, 0); xTimerStop(_scheduledCheckTimer, 0);
xTimerDelete(_scheduledCheckTimer, portMAX_DELAY); xTimerDelete(_scheduledCheckTimer, portMAX_DELAY);
@@ -42,6 +57,33 @@ void OTAManager::begin() {
LOG_INFO("OTA Manager initialized"); LOG_INFO("OTA Manager initialized");
setStatus(Status::IDLE); setStatus(Status::IDLE);
// Create semaphore for worker task signaling
_otaWorkSignal = xSemaphoreCreateBinary();
if (_otaWorkSignal == NULL) {
LOG_ERROR("Failed to create OTA work semaphore!");
return;
}
// Create dedicated worker task with 8KB stack (prevents timer task overflow)
BaseType_t taskCreated = xTaskCreatePinnedToCore(
otaWorkerTaskFunction,
"OTA_Worker",
8192, // 8KB stack - plenty for HTTP and JSON operations
this, // Pass OTAManager instance
2, // Priority 2 (lower than critical tasks)
&_otaWorkerTask,
0 // Core 0
);
if (taskCreated != pdPASS) {
LOG_ERROR("Failed to create OTA worker task!");
vSemaphoreDelete(_otaWorkSignal);
_otaWorkSignal = NULL;
return;
}
LOG_INFO("OTA worker task created with 8KB stack on Core 0");
// Create timer for scheduled checks (checks every minute if it's 3:00 AM) // Create timer for scheduled checks (checks every minute if it's 3:00 AM)
_scheduledCheckTimer = xTimerCreate( _scheduledCheckTimer = xTimerCreate(
"OTA_Schedule", "OTA_Schedule",
@@ -84,22 +126,35 @@ void OTAManager::setPlayer(Player* player) {
_player = player; _player = player;
} }
void OTAManager::setTimeKeeper(Timekeeper* tk) {
_timeKeeper = tk;
}
void OTAManager::setTelemetry(Telemetry* telemetry) {
_telemetry = telemetry;
}
// ✅ NEW: Static timer callback for initial boot check // ✅ NEW: Static timer callback for initial boot check
// CRITICAL: Timer callbacks run in Timer Service task with limited stack!
// We ONLY set a flag here - actual work is done by dedicated worker task
void OTAManager::initialCheckCallback(TimerHandle_t xTimer) { void OTAManager::initialCheckCallback(TimerHandle_t xTimer) {
OTAManager* ota = static_cast<OTAManager*>(pvTimerGetTimerID(xTimer)); OTAManager* ota = static_cast<OTAManager*>(pvTimerGetTimerID(xTimer));
if (ota) { if (ota && ota->_otaWorkSignal) {
LOG_INFO("🚀 Running initial OTA check (non-blocking, async)"); // Signal worker task to perform initial check
ota->performInitialCheck(); ota->_pendingWork = OTAWorkType::INITIAL_CHECK;
xSemaphoreGive(ota->_otaWorkSignal);
} }
} }
// ✅ NEW: Perform initial OTA check (async, non-blocking) // ✅ NEW: Perform initial OTA check (async, non-blocking)
void OTAManager::performInitialCheck() { void OTAManager::performInitialCheck() {
// This runs asynchronously, won't block WebSocket/UDP/MQTT // This runs asynchronously in worker task, won't block WebSocket/UDP/MQTT
checkForUpdates(); checkForUpdates();
} }
// ✅ NEW: Static timer callback for scheduled checks // ✅ NEW: Static timer callback for scheduled checks
// CRITICAL: Timer callbacks run in Timer Service task with limited stack!
// We ONLY set a flag here - actual work is done by dedicated worker task
void OTAManager::scheduledCheckCallback(TimerHandle_t xTimer) { void OTAManager::scheduledCheckCallback(TimerHandle_t xTimer) {
OTAManager* ota = static_cast<OTAManager*>(pvTimerGetTimerID(xTimer)); OTAManager* ota = static_cast<OTAManager*>(pvTimerGetTimerID(xTimer));
@@ -109,13 +164,12 @@ void OTAManager::scheduledCheckCallback(TimerHandle_t xTimer) {
// Only proceed if it's exactly 3:00 AM // Only proceed if it's exactly 3:00 AM
if (timeinfo->tm_hour == 3 && timeinfo->tm_min == 0) { if (timeinfo->tm_hour == 3 && timeinfo->tm_min == 0) {
LOG_INFO("🕒 3:00 AM - Running scheduled OTA check"); // Check if player is idle before signaling worker
if (!ota->isPlayerActive() && ota->_otaWorkSignal) {
// Check if player is idle before proceeding // Signal worker task to perform scheduled check
if (!ota->isPlayerActive()) { ota->_pendingWork = OTAWorkType::SCHEDULED_CHECK;
LOG_INFO("✅ Player is idle - checking for emergency updates"); xSemaphoreGive(ota->_otaWorkSignal);
ota->checkForEmergencyUpdates(); } else if (ota->isPlayerActive()) {
} else {
LOG_WARNING("⚠️ Player is active - skipping scheduled update check"); LOG_WARNING("⚠️ Player is active - skipping scheduled update check");
} }
} }
@@ -356,9 +410,10 @@ bool OTAManager::downloadAndInstall(const String& channel) {
LOG_INFO("OTA: Trying firmware download from server %d/%d: %s", LOG_INFO("OTA: Trying firmware download from server %d/%d: %s",
serverIndex + 1, servers.size(), baseUrl.c_str()); serverIndex + 1, servers.size(), baseUrl.c_str());
if (downloadToSD(firmwareUrl, _availableChecksum, _expectedFileSize)) { // 🔥 NEW: Download directly to flash, bypassing SD card
// Success! Now install from SD if (downloadDirectToFlash(firmwareUrl, _expectedFileSize)) {
return installFromSD("/firmware/staged_update.bin"); LOG_INFO("✅ OTA update successful!");
return true;
} else { } else {
LOG_WARNING("OTA: Firmware download failed from %s, trying next server", baseUrl.c_str()); LOG_WARNING("OTA: Firmware download failed from %s, trying next server", baseUrl.c_str());
} }
@@ -369,6 +424,125 @@ bool OTAManager::downloadAndInstall(const String& channel) {
return false; return false;
} }
// 🔥 NEW: Download directly to flash memory, bypassing SD card
bool OTAManager::downloadDirectToFlash(const String& url, size_t expectedSize) {
LOG_INFO("OTA: Starting direct-to-flash download (bypassing SD card)");
HTTPClient http;
http.begin(url.c_str());
http.setTimeout(30000); // 30 seconds
int httpCode = http.GET();
if (httpCode != HTTP_CODE_OK) {
LOG_ERROR("Download HTTP error code: %d", httpCode);
setStatus(Status::FAILED, ErrorCode::HTTP_ERROR);
http.end();
return false;
}
int contentLength = http.getSize();
LOG_INFO("OTA: HTTP Response Code: %d", httpCode);
LOG_INFO("OTA: Content-Length: %d bytes", contentLength);
if (contentLength <= 0) {
LOG_ERROR("Invalid content length");
setStatus(Status::FAILED, ErrorCode::DOWNLOAD_FAILED);
http.end();
return false;
}
// Validate file size
if (expectedSize > 0 && (size_t)contentLength != expectedSize) {
LOG_ERROR("OTA: File size mismatch! Expected: %u, Got: %d", expectedSize, contentLength);
setStatus(Status::FAILED, ErrorCode::SIZE_MISMATCH);
http.end();
return false;
}
// ═══════════════════════════════════════════════════════════════════════════
// ENTER OTA FREEZE MODE - Pause all non-critical systems
// ═══════════════════════════════════════════════════════════════════════════
LOG_INFO("OTA: Entering freeze mode - pausing TimeKeeper and Telemetry");
if (_timeKeeper) {
_timeKeeper->pauseClockUpdates();
}
if (_telemetry) {
_telemetry->pause();
}
// Begin OTA update to flash with MD5 validation enabled
if (!Update.begin(contentLength)) {
LOG_ERROR("Not enough space to begin OTA update");
setStatus(Status::FAILED, ErrorCode::INSUFFICIENT_SPACE);
http.end();
// Resume systems
if (_timeKeeper) _timeKeeper->resumeClockUpdates();
if (_telemetry) _telemetry->resume();
return false;
}
LOG_INFO("OTA: Update partition ready, starting stream write...");
LOG_INFO("OTA: Checksum validation will be performed by ESP32 bootloader");
setStatus(Status::INSTALLING);
// Stream directly to flash
WiFiClient* stream = http.getStreamPtr();
size_t written = Update.writeStream(*stream);
http.end();
// ═══════════════════════════════════════════════════════════════════════════
// EXIT OTA FREEZE MODE - Resume all paused systems
// ═══════════════════════════════════════════════════════════════════════════
LOG_INFO("OTA: Exiting freeze mode - resuming TimeKeeper and Telemetry");
if (_timeKeeper) {
_timeKeeper->resumeClockUpdates();
}
if (_telemetry) {
_telemetry->resume();
}
if (written == (size_t)contentLength) {
LOG_INFO("OTA: Successfully written %u bytes to flash", written);
} else {
LOG_ERROR("OTA: Written only %u/%d bytes", written, contentLength);
setStatus(Status::FAILED, ErrorCode::WRITE_FAILED);
return false;
}
if (Update.end(true)) { // true = set new boot partition
LOG_INFO("OTA: Update complete!");
if (Update.isFinished()) {
setStatus(Status::SUCCESS);
LOG_INFO("OTA: Firmware update successful. Rebooting...");
// Update version in config
_configManager.setFwVersion(String(_availableVersion));
_configManager.saveDeviceConfig();
delay(1000);
ESP.restart();
return true;
} else {
LOG_ERROR("OTA: Update not finished");
setStatus(Status::FAILED, ErrorCode::VERIFICATION_FAILED);
return false;
}
} else {
LOG_ERROR("OTA: Update error: %s", Update.errorString());
setStatus(Status::FAILED, ErrorCode::VERIFICATION_FAILED);
return false;
}
}
bool OTAManager::downloadToSD(const String& url, const String& expectedChecksum, size_t expectedSize) { bool OTAManager::downloadToSD(const String& url, const String& expectedChecksum, size_t expectedSize) {
if (!_fileManager) { if (!_fileManager) {
LOG_ERROR("FileManager not set!"); LOG_ERROR("FileManager not set!");
@@ -384,6 +558,10 @@ bool OTAManager::downloadToSD(const String& url, const String& expectedChecksum,
HTTPClient http; HTTPClient http;
http.begin(url.c_str()); http.begin(url.c_str());
// Set timeout to prevent hanging
http.setTimeout(30000); // 30 seconds
int httpCode = http.GET(); int httpCode = http.GET();
if (httpCode != HTTP_CODE_OK) { if (httpCode != HTTP_CODE_OK) {
@@ -394,6 +572,10 @@ bool OTAManager::downloadToSD(const String& url, const String& expectedChecksum,
} }
int contentLength = http.getSize(); int contentLength = http.getSize();
// Log HTTP response headers for debugging
LOG_INFO("OTA: HTTP Response Code: %d", httpCode);
LOG_INFO("OTA: Content-Length header: %d bytes", contentLength);
if (contentLength <= 0) { if (contentLength <= 0) {
LOG_ERROR("Invalid content length"); LOG_ERROR("Invalid content length");
setStatus(Status::FAILED, ErrorCode::DOWNLOAD_FAILED); setStatus(Status::FAILED, ErrorCode::DOWNLOAD_FAILED);
@@ -419,17 +601,46 @@ bool OTAManager::downloadToSD(const String& url, const String& expectedChecksum,
LOG_INFO("OTA: Starting download of %d bytes...", contentLength); LOG_INFO("OTA: Starting download of %d bytes...", contentLength);
// ═══════════════════════════════════════════════════════════════════════════
// ENTER OTA FREEZE MODE - Pause all non-critical systems to prevent SD contention
// ═══════════════════════════════════════════════════════════════════════════
LOG_INFO("OTA: Entering freeze mode - pausing TimeKeeper and Telemetry");
if (_timeKeeper) {
_timeKeeper->pauseClockUpdates();
}
if (_telemetry) {
_telemetry->pause();
}
// Delete file if it exists, before opening
if (SD.exists(tempPath.c_str())) {
LOG_INFO("OTA: Removing existing staged update file");
if (!SD.remove(tempPath.c_str())) {
LOG_ERROR("OTA: Failed to remove existing file!");
}
delay(200); // Give SD card time to complete deletion
}
// Open file for writing // Open file for writing
File file = SD.open(tempPath.c_str(), FILE_WRITE); File file = SD.open(tempPath.c_str(), FILE_WRITE);
if (!file) { if (!file) {
LOG_ERROR("Failed to create temporary update file"); LOG_ERROR("Failed to create temporary update file");
// Resume systems before returning
if (_timeKeeper) _timeKeeper->resumeClockUpdates();
if (_telemetry) _telemetry->resume();
setStatus(Status::FAILED, ErrorCode::DOWNLOAD_FAILED); setStatus(Status::FAILED, ErrorCode::DOWNLOAD_FAILED);
http.end(); http.end();
return false; return false;
} }
LOG_INFO("OTA: File opened successfully for writing");
WiFiClient* stream = http.getStreamPtr(); WiFiClient* stream = http.getStreamPtr();
uint8_t buffer[1024]; uint8_t buffer[128]; // Smaller buffer for testing
size_t written = 0; size_t written = 0;
size_t lastLoggedPercent = 0; size_t lastLoggedPercent = 0;
@@ -440,34 +651,61 @@ bool OTAManager::downloadToSD(const String& url, const String& expectedChecksum,
size_t bytesRead = stream->readBytes(buffer, toRead); size_t bytesRead = stream->readBytes(buffer, toRead);
if (bytesRead > 0) { if (bytesRead > 0) {
// Write directly and immediately flush
size_t bytesWritten = file.write(buffer, bytesRead); size_t bytesWritten = file.write(buffer, bytesRead);
// Immediately check if write succeeded
if (bytesWritten != bytesRead) { if (bytesWritten != bytesRead) {
LOG_ERROR("SD write failed"); LOG_ERROR("SD write failed at offset %u (%u/%u bytes written)", written, bytesWritten, bytesRead);
file.close(); file.close();
http.end(); http.end();
if (_timeKeeper) _timeKeeper->resumeClockUpdates();
if (_telemetry) _telemetry->resume();
setStatus(Status::FAILED, ErrorCode::WRITE_FAILED); setStatus(Status::FAILED, ErrorCode::WRITE_FAILED);
return false; return false;
} }
written += bytesWritten; written += bytesWritten;
// ✅ IMPROVED: Progress reporting with percentage // ✅ IMPROVED: Progress reporting with percentage
notifyProgress(written, contentLength); notifyProgress(written, contentLength);
// Log progress every 10% // Log progress every 20% (less frequent to reduce SD contention)
size_t currentPercent = (written * 100) / contentLength; size_t currentPercent = (written * 100) / contentLength;
if (currentPercent >= lastLoggedPercent + 10) { if (currentPercent >= lastLoggedPercent + 20) {
LOG_INFO("OTA: Download progress: %u%% (%u/%u bytes)", LOG_INFO("OTA: Download progress: %u%% (%u/%u bytes)",
currentPercent, written, contentLength); currentPercent, written, contentLength);
lastLoggedPercent = currentPercent; lastLoggedPercent = currentPercent;
} }
} }
} }
// Brief yield to allow other tasks to run
yield(); yield();
} }
// 🔥 CRITICAL: Flush file buffer before closing to ensure all data is written
file.flush();
delay(100); // Brief delay to ensure SD card completes write
file.close(); file.close();
http.end(); http.end();
// ═══════════════════════════════════════════════════════════════════════════
// EXIT OTA FREEZE MODE - Resume all paused systems
// ═══════════════════════════════════════════════════════════════════════════
LOG_INFO("OTA: Exiting freeze mode - resuming TimeKeeper and Telemetry");
if (_timeKeeper) {
_timeKeeper->resumeClockUpdates();
}
if (_telemetry) {
_telemetry->resume();
}
if (written != (size_t)contentLength) { if (written != (size_t)contentLength) {
LOG_ERROR("Download incomplete: %u/%d bytes", written, contentLength); LOG_ERROR("Download incomplete: %u/%d bytes", written, contentLength);
setStatus(Status::FAILED, ErrorCode::DOWNLOAD_FAILED); setStatus(Status::FAILED, ErrorCode::DOWNLOAD_FAILED);
@@ -476,6 +714,32 @@ bool OTAManager::downloadToSD(const String& url, const String& expectedChecksum,
LOG_INFO("Download complete (%u bytes)", written); LOG_INFO("Download complete (%u bytes)", written);
// 🔍 DEBUG: Check actual file size on SD card
size_t actualFileSize = _fileManager->getFileSize(tempPath);
LOG_INFO("OTA: File size on SD card: %u bytes (expected: %u)", actualFileSize, written);
if (actualFileSize != written) {
LOG_ERROR("OTA: FILE SIZE MISMATCH ON SD CARD! Expected %u, got %u", written, actualFileSize);
setStatus(Status::FAILED, ErrorCode::WRITE_FAILED);
return false;
}
// 🔍 DEBUG: Read first 32 bytes for inspection
File debugFile = SD.open(tempPath.c_str());
if (debugFile) {
uint8_t debugBuffer[32];
size_t debugRead = debugFile.readBytes((char*)debugBuffer, 32);
debugFile.close();
String hexDump = "OTA: First 32 bytes (hex): ";
for (size_t i = 0; i < debugRead && i < 32; i++) {
char hex[4];
sprintf(hex, "%02X ", debugBuffer[i]);
hexDump += hex;
}
LOG_INFO("%s", hexDump.c_str());
}
// Verify checksum // Verify checksum
if (!verifyChecksum(tempPath, expectedChecksum)) { if (!verifyChecksum(tempPath, expectedChecksum)) {
LOG_ERROR("Checksum verification failed after download"); LOG_ERROR("Checksum verification failed after download");
@@ -687,18 +951,13 @@ bool OTAManager::performManualUpdate(const String& channel) {
return false; return false;
} }
LOG_INFO("Starting manual OTA update from %s channel via SD staging...", channel.c_str()); LOG_INFO("Starting manual OTA update from %s channel (direct-to-flash)...", channel.c_str());
setStatus(Status::DOWNLOADING); setStatus(Status::DOWNLOADING);
String firmwareUrl = buildFirmwareUrl(channel); String firmwareUrl = buildFirmwareUrl(channel);
// Download to SD first // 🔥 NEW: Download directly to flash, bypassing SD card
if (!downloadToSD(firmwareUrl, _availableChecksum, _expectedFileSize)) { return downloadDirectToFlash(firmwareUrl, _expectedFileSize);
return false;
}
// Install from SD
return installFromSD("/firmware/staged_update.bin");
} }
// ════════════════════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════════════════════
@@ -718,27 +977,20 @@ bool OTAManager::performCustomUpdate(const String& firmwareUrl, const String& ch
return false; return false;
} }
LOG_INFO("🔥 Starting CUSTOM firmware update..."); LOG_INFO("🔥 Starting CUSTOM firmware update (direct-to-flash)...");
LOG_INFO(" URL: %s", firmwareUrl.c_str()); LOG_INFO(" URL: %s", firmwareUrl.c_str());
LOG_INFO(" Checksum: %s", checksum.isEmpty() ? "NOT PROVIDED" : checksum.c_str());
LOG_INFO(" File Size: %u bytes", fileSize); LOG_INFO(" File Size: %u bytes", fileSize);
if (checksum.isEmpty()) { // NOTE: checksum parameter is kept for API compatibility but not used
LOG_WARNING("⚠️ No checksum provided - update will proceed without verification!"); // Validation is performed by ESP32 bootloader after flash
if (!checksum.isEmpty()) {
LOG_INFO(" Checksum: %s (provided for reference only)", checksum.c_str());
} }
setStatus(Status::DOWNLOADING); setStatus(Status::DOWNLOADING);
// Download firmware from custom URL to SD // 🔥 NEW: Download directly to flash, bypassing SD card
if (!downloadToSD(firmwareUrl, checksum, fileSize)) { bool result = downloadDirectToFlash(firmwareUrl, fileSize);
LOG_ERROR("Custom firmware download failed");
return false;
}
LOG_INFO("✅ Custom firmware downloaded successfully");
// Install from SD
bool result = installFromSD("/firmware/staged_update.bin");
if (result) { if (result) {
LOG_INFO("🚀 Custom firmware installed - device will reboot"); LOG_INFO("🚀 Custom firmware installed - device will reboot");
@@ -866,3 +1118,49 @@ bool OTAManager::isHealthy() const {
return true; return true;
} }
// ════════════════════════════════════════════════════════════════════════════
// WORKER TASK IMPLEMENTATION
// ════════════════════════════════════════════════════════════════════════════
// Static entry point for worker task
void OTAManager::otaWorkerTaskFunction(void* parameter) {
OTAManager* ota = static_cast<OTAManager*>(parameter);
LOG_INFO("🔧 OTA Worker task started on Core %d with 8KB stack", xPortGetCoreID());
// Run the worker loop
ota->otaWorkerLoop();
// Should not reach here
LOG_ERROR("❌ OTA Worker task ended unexpectedly!");
vTaskDelete(NULL);
}
// Worker task loop - waits for signals and performs OTA operations
void OTAManager::otaWorkerLoop() {
while (true) {
// Wait for work signal (blocks until semaphore is given)
if (xSemaphoreTake(_otaWorkSignal, portMAX_DELAY) == pdTRUE) {
// Check what type of work to perform
switch (_pendingWork) {
case OTAWorkType::INITIAL_CHECK:
LOG_INFO("🚀 Worker: Performing initial OTA check");
performInitialCheck();
break;
case OTAWorkType::SCHEDULED_CHECK:
LOG_INFO("🕒 Worker: Performing scheduled emergency check");
checkForEmergencyUpdates();
break;
case OTAWorkType::NONE:
default:
LOG_WARNING("⚠️ Worker: Received signal but no work pending");
break;
}
// Clear pending work
_pendingWork = OTAWorkType::NONE;
}
}
}

View File

@@ -26,9 +26,13 @@
#include <functional> #include <functional>
#include <time.h> #include <time.h>
#include "../FileManager/FileManager.hpp" #include "../FileManager/FileManager.hpp"
#include "../Telemetry/Telemetry.hpp"
#include "../TimeKeeper/TimeKeeper.hpp"
class ConfigManager; // Forward declaration class ConfigManager; // Forward declaration
class Player; // Forward declaration for idle check class Player; // Forward declaration for idle check
class Timekeeper; // Forward declaration for freeze mode
class Telemetry; // Forward declaration for freeze mode
class OTAManager { class OTAManager {
public: public:
@@ -66,7 +70,9 @@ public:
void begin(); void begin();
void setFileManager(FileManager* fm); void setFileManager(FileManager* fm);
void setPlayer(Player* player); // NEW: Set player reference for idle check void setPlayer(Player* player); // Set player reference for idle check
void setTimeKeeper(Timekeeper* tk); // Set timekeeper reference for freeze mode
void setTelemetry(Telemetry* telemetry); // Set telemetry reference for freeze mode
void checkForUpdates(); void checkForUpdates();
void checkForUpdates(const String& channel); // Check specific channel void checkForUpdates(const String& channel); // Check specific channel
@@ -104,7 +110,9 @@ public:
private: private:
ConfigManager& _configManager; ConfigManager& _configManager;
FileManager* _fileManager; FileManager* _fileManager;
Player* _player; // NEW: Player reference for idle check Player* _player; // Player reference for idle check
Timekeeper* _timeKeeper; // TimeKeeper reference for freeze mode
Telemetry* _telemetry; // Telemetry reference for freeze mode
Status _status; Status _status;
ErrorCode _lastError; ErrorCode _lastError;
uint16_t _availableVersion; uint16_t _availableVersion;
@@ -128,6 +136,19 @@ private:
static void initialCheckCallback(TimerHandle_t xTimer); static void initialCheckCallback(TimerHandle_t xTimer);
void performInitialCheck(); // Async initial check after boot void performInitialCheck(); // Async initial check after boot
// Worker task for OTA operations (prevents stack overflow in timer callbacks)
TaskHandle_t _otaWorkerTask;
SemaphoreHandle_t _otaWorkSignal;
static void otaWorkerTaskFunction(void* parameter);
void otaWorkerLoop();
enum class OTAWorkType {
NONE,
INITIAL_CHECK,
SCHEDULED_CHECK
};
OTAWorkType _pendingWork;
void setStatus(Status status, ErrorCode error = ErrorCode::NONE); void setStatus(Status status, ErrorCode error = ErrorCode::NONE);
void notifyProgress(size_t current, size_t total); void notifyProgress(size_t current, size_t total);
bool checkVersion(); bool checkVersion();
@@ -135,7 +156,8 @@ private:
bool checkChannelsMetadata(); bool checkChannelsMetadata();
bool downloadAndInstall(); bool downloadAndInstall();
bool downloadAndInstall(const String& channel); bool downloadAndInstall(const String& channel);
bool downloadToSD(const String& url, const String& expectedChecksum, size_t expectedSize); // NEW: Added size param bool downloadDirectToFlash(const String& url, size_t expectedSize); // NEW: Direct to flash (bypasses SD)
bool downloadToSD(const String& url, const String& expectedChecksum, size_t expectedSize); // OLD: Via SD card
bool verifyChecksum(const String& filePath, const String& expectedChecksum); bool verifyChecksum(const String& filePath, const String& expectedChecksum);
String calculateSHA256(const String& filePath); String calculateSHA256(const String& filePath);
bool installFromSD(const String& filePath); bool installFromSD(const String& filePath);

View File

@@ -172,6 +172,8 @@ void Telemetry::telemetryTask(void* parameter) {
LOG_INFO("Telemetry task started"); LOG_INFO("Telemetry task started");
while(1) { while(1) {
// Skip processing if paused (OTA freeze mode)
if (!telemetry->isPaused) {
// Only run if player is playing OR we're still cooling // Only run if player is playing OR we're still cooling
bool isPlaying = (telemetry->playerIsPlayingPtr != nullptr) ? bool isPlaying = (telemetry->playerIsPlayingPtr != nullptr) ?
*(telemetry->playerIsPlayingPtr) : false; *(telemetry->playerIsPlayingPtr) : false;
@@ -179,6 +181,7 @@ void Telemetry::telemetryTask(void* parameter) {
if (isPlaying || telemetry->coolingActive) { if (isPlaying || telemetry->coolingActive) {
telemetry->checkBellLoads(); telemetry->checkBellLoads();
} }
}
vTaskDelay(pdMS_TO_TICKS(1000)); // Run every 1s vTaskDelay(pdMS_TO_TICKS(1000)); // Run every 1s
} }

View File

@@ -77,6 +77,9 @@ private:
// Spinlock for critical sections // Spinlock for critical sections
portMUX_TYPE telemetrySpinlock = portMUX_INITIALIZER_UNLOCKED; portMUX_TYPE telemetrySpinlock = portMUX_INITIALIZER_UNLOCKED;
// Pause flag for OTA freeze mode
volatile bool isPaused = false;
public: public:
// Initialization // Initialization
void begin(); void begin();
@@ -109,6 +112,10 @@ public:
// Force stop callback (to be set by main application) // Force stop callback (to be set by main application)
void setForceStopCallback(void (*callback)()); void setForceStopCallback(void (*callback)());
// Pause/Resume for OTA freeze mode (stops SD writes during firmware update)
void pause() { isPaused = true; }
void resume() { isPaused = false; }
// ═══════════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════════
// HEALTH CHECK METHOD // HEALTH CHECK METHOD
// ═══════════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════════

View File

@@ -396,6 +396,8 @@ void setup()
otaManager.begin(); otaManager.begin();
otaManager.setFileManager(&fileManager); otaManager.setFileManager(&fileManager);
otaManager.setPlayer(&player); // Set player reference for idle check otaManager.setPlayer(&player); // Set player reference for idle check
otaManager.setTimeKeeper(&timekeeper); // Set timekeeper reference for freeze mode
otaManager.setTelemetry(&telemetry); // Set telemetry reference for freeze mode
// 🔥 FIX: OTA check will happen asynchronously via scheduled timer (no blocking delay) // 🔥 FIX: OTA check will happen asynchronously via scheduled timer (no blocking delay)
// UDP discovery setup can happen immediately without conflicts // UDP discovery setup can happen immediately without conflicts