diff --git a/vesper/src/OTAManager/OTAManager.cpp b/vesper/src/OTAManager/OTAManager.cpp index a99f4d1..f7fc032 100644 --- a/vesper/src/OTAManager/OTAManager.cpp +++ b/vesper/src/OTAManager/OTAManager.cpp @@ -9,6 +9,8 @@ OTAManager::OTAManager(ConfigManager& configManager) : _configManager(configManager) , _fileManager(nullptr) , _player(nullptr) + , _timeKeeper(nullptr) + , _telemetry(nullptr) , _status(Status::IDLE) , _lastError(ErrorCode::NONE) , _availableVersion(0.0f) @@ -22,10 +24,23 @@ OTAManager::OTAManager(ConfigManager& configManager) , _progressCallback(nullptr) , _statusCallback(nullptr) , _scheduledCheckTimer(NULL) - , _initialCheckTimer(NULL) { + , _initialCheckTimer(NULL) + , _otaWorkerTask(NULL) + , _otaWorkSignal(NULL) + , _pendingWork(OTAWorkType::NONE) { } 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) { xTimerStop(_scheduledCheckTimer, 0); xTimerDelete(_scheduledCheckTimer, portMAX_DELAY); @@ -42,6 +57,33 @@ void OTAManager::begin() { LOG_INFO("OTA Manager initialized"); 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) _scheduledCheckTimer = xTimerCreate( "OTA_Schedule", @@ -84,22 +126,35 @@ void OTAManager::setPlayer(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 +// 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) { OTAManager* ota = static_cast(pvTimerGetTimerID(xTimer)); - if (ota) { - LOG_INFO("🚀 Running initial OTA check (non-blocking, async)"); - ota->performInitialCheck(); + if (ota && ota->_otaWorkSignal) { + // Signal worker task to perform initial check + ota->_pendingWork = OTAWorkType::INITIAL_CHECK; + xSemaphoreGive(ota->_otaWorkSignal); } } // ✅ NEW: Perform initial OTA check (async, non-blocking) 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(); } // ✅ 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) { OTAManager* ota = static_cast(pvTimerGetTimerID(xTimer)); @@ -109,13 +164,12 @@ void OTAManager::scheduledCheckCallback(TimerHandle_t xTimer) { // Only proceed if it's exactly 3:00 AM if (timeinfo->tm_hour == 3 && timeinfo->tm_min == 0) { - LOG_INFO("🕒 3:00 AM - Running scheduled OTA check"); - - // Check if player is idle before proceeding - if (!ota->isPlayerActive()) { - LOG_INFO("✅ Player is idle - checking for emergency updates"); - ota->checkForEmergencyUpdates(); - } else { + // Check if player is idle before signaling worker + if (!ota->isPlayerActive() && ota->_otaWorkSignal) { + // Signal worker task to perform scheduled check + ota->_pendingWork = OTAWorkType::SCHEDULED_CHECK; + xSemaphoreGive(ota->_otaWorkSignal); + } else if (ota->isPlayerActive()) { LOG_WARNING("⚠️ Player is active - skipping scheduled update check"); } } @@ -348,27 +402,147 @@ bool OTAManager::checkVersion(const String& channel) { // ✅ ENHANCED: Download and install with size validation bool OTAManager::downloadAndInstall(const String& channel) { std::vector servers = _configManager.getUpdateServers(); - + for (size_t serverIndex = 0; serverIndex < servers.size(); serverIndex++) { String baseUrl = servers[serverIndex]; String firmwareUrl = baseUrl + "/ota/" + _configManager.getHardwareVariant() + "/" + channel + "/firmware.bin"; - - 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()); - - if (downloadToSD(firmwareUrl, _availableChecksum, _expectedFileSize)) { - // Success! Now install from SD - return installFromSD("/firmware/staged_update.bin"); + + // 🔥 NEW: Download directly to flash, bypassing SD card + if (downloadDirectToFlash(firmwareUrl, _expectedFileSize)) { + LOG_INFO("✅ OTA update successful!"); + return true; } else { LOG_WARNING("OTA: Firmware download failed from %s, trying next server", baseUrl.c_str()); } } - + // All servers failed LOG_ERROR("OTA: All %d servers failed to provide firmware", servers.size()); 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) { if (!_fileManager) { LOG_ERROR("FileManager not set!"); @@ -384,16 +558,24 @@ bool OTAManager::downloadToSD(const String& url, const String& expectedChecksum, HTTPClient http; http.begin(url.c_str()); + + // Set timeout to prevent hanging + 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 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) { LOG_ERROR("Invalid content length"); setStatus(Status::FAILED, ErrorCode::DOWNLOAD_FAILED); @@ -418,64 +600,146 @@ bool OTAManager::downloadToSD(const String& url, const String& expectedChecksum, } 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 File file = SD.open(tempPath.c_str(), FILE_WRITE); if (!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); http.end(); return false; } - + + LOG_INFO("OTA: File opened successfully for writing"); + WiFiClient* stream = http.getStreamPtr(); - uint8_t buffer[1024]; + uint8_t buffer[128]; // Smaller buffer for testing size_t written = 0; size_t lastLoggedPercent = 0; - + while (http.connected() && written < (size_t)contentLength) { size_t available = stream->available(); if (available) { size_t toRead = min(available, sizeof(buffer)); size_t bytesRead = stream->readBytes(buffer, toRead); - + if (bytesRead > 0) { + // Write directly and immediately flush size_t bytesWritten = file.write(buffer, bytesRead); + + // Immediately check if write succeeded 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(); http.end(); + + if (_timeKeeper) _timeKeeper->resumeClockUpdates(); + if (_telemetry) _telemetry->resume(); + setStatus(Status::FAILED, ErrorCode::WRITE_FAILED); return false; } + written += bytesWritten; - + // ✅ IMPROVED: Progress reporting with percentage notifyProgress(written, contentLength); - - // Log progress every 10% + + // Log progress every 20% (less frequent to reduce SD contention) size_t currentPercent = (written * 100) / contentLength; - if (currentPercent >= lastLoggedPercent + 10) { - LOG_INFO("OTA: Download progress: %u%% (%u/%u bytes)", + if (currentPercent >= lastLoggedPercent + 20) { + LOG_INFO("OTA: Download progress: %u%% (%u/%u bytes)", currentPercent, written, contentLength); lastLoggedPercent = currentPercent; } } } + + // Brief yield to allow other tasks to run 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(); + 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_ERROR("Download incomplete: %u/%d bytes", written, contentLength); setStatus(Status::FAILED, ErrorCode::DOWNLOAD_FAILED); return false; } - + 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 if (!verifyChecksum(tempPath, expectedChecksum)) { LOG_ERROR("Checksum verification failed after download"); @@ -483,7 +747,7 @@ bool OTAManager::downloadToSD(const String& url, const String& expectedChecksum, setStatus(Status::FAILED, ErrorCode::CHECKSUM_MISMATCH); return false; } - + LOG_INFO("Download and checksum verification successful"); return true; } @@ -678,27 +942,22 @@ bool OTAManager::performManualUpdate(const String& channel) { LOG_WARNING("OTA update already in progress"); return false; } - + // Check for updates in the specified channel first checkForUpdates(channel); - + if (!_updateAvailable) { LOG_WARNING("No update available in %s channel", channel.c_str()); 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); - + String firmwareUrl = buildFirmwareUrl(channel); - - // Download to SD first - if (!downloadToSD(firmwareUrl, _availableChecksum, _expectedFileSize)) { - return false; - } - - // Install from SD - return installFromSD("/firmware/staged_update.bin"); + + // 🔥 NEW: Download directly to flash, bypassing SD card + return downloadDirectToFlash(firmwareUrl, _expectedFileSize); } // ════════════════════════════════════════════════════════════════════════════ @@ -718,27 +977,20 @@ bool OTAManager::performCustomUpdate(const String& firmwareUrl, const String& ch 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(" Checksum: %s", checksum.isEmpty() ? "NOT PROVIDED" : checksum.c_str()); LOG_INFO(" File Size: %u bytes", fileSize); - if (checksum.isEmpty()) { - LOG_WARNING("⚠️ No checksum provided - update will proceed without verification!"); + // NOTE: checksum parameter is kept for API compatibility but not used + // 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); - // Download firmware from custom URL to SD - if (!downloadToSD(firmwareUrl, checksum, 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"); + // 🔥 NEW: Download directly to flash, bypassing SD card + bool result = downloadDirectToFlash(firmwareUrl, fileSize); if (result) { LOG_INFO("🚀 Custom firmware installed - device will reboot"); @@ -863,6 +1115,52 @@ bool OTAManager::isHealthy() const { LOG_DEBUG("OTAManager: Unhealthy - Scheduled check timer not running"); return false; } - + return true; } + +// ════════════════════════════════════════════════════════════════════════════ +// WORKER TASK IMPLEMENTATION +// ════════════════════════════════════════════════════════════════════════════ + +// Static entry point for worker task +void OTAManager::otaWorkerTaskFunction(void* parameter) { + OTAManager* ota = static_cast(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; + } + } +} diff --git a/vesper/src/OTAManager/OTAManager.hpp b/vesper/src/OTAManager/OTAManager.hpp index f9f2dd8..fd11e59 100644 --- a/vesper/src/OTAManager/OTAManager.hpp +++ b/vesper/src/OTAManager/OTAManager.hpp @@ -26,9 +26,13 @@ #include #include #include "../FileManager/FileManager.hpp" +#include "../Telemetry/Telemetry.hpp" +#include "../TimeKeeper/TimeKeeper.hpp" -class ConfigManager; // Forward declaration -class Player; // Forward declaration for idle check +class ConfigManager; // Forward declaration +class Player; // Forward declaration for idle check +class Timekeeper; // Forward declaration for freeze mode +class Telemetry; // Forward declaration for freeze mode class OTAManager { public: @@ -66,7 +70,9 @@ public: void begin(); 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(const String& channel); // Check specific channel @@ -104,7 +110,9 @@ public: private: ConfigManager& _configManager; 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; ErrorCode _lastError; uint16_t _availableVersion; @@ -127,6 +135,19 @@ private: TimerHandle_t _initialCheckTimer; static void initialCheckCallback(TimerHandle_t xTimer); 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 notifyProgress(size_t current, size_t total); @@ -135,7 +156,8 @@ private: bool checkChannelsMetadata(); bool downloadAndInstall(); 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); String calculateSHA256(const String& filePath); bool installFromSD(const String& filePath); diff --git a/vesper/src/Telemetry/Telemetry.cpp b/vesper/src/Telemetry/Telemetry.cpp index 80edebf..9df1558 100644 --- a/vesper/src/Telemetry/Telemetry.cpp +++ b/vesper/src/Telemetry/Telemetry.cpp @@ -172,14 +172,17 @@ void Telemetry::telemetryTask(void* parameter) { LOG_INFO("Telemetry task started"); while(1) { - // Only run if player is playing OR we're still cooling - bool isPlaying = (telemetry->playerIsPlayingPtr != nullptr) ? - *(telemetry->playerIsPlayingPtr) : false; + // Skip processing if paused (OTA freeze mode) + if (!telemetry->isPaused) { + // Only run if player is playing OR we're still cooling + bool isPlaying = (telemetry->playerIsPlayingPtr != nullptr) ? + *(telemetry->playerIsPlayingPtr) : false; - if (isPlaying || telemetry->coolingActive) { - telemetry->checkBellLoads(); + if (isPlaying || telemetry->coolingActive) { + telemetry->checkBellLoads(); + } } - + vTaskDelay(pdMS_TO_TICKS(1000)); // Run every 1s } } diff --git a/vesper/src/Telemetry/Telemetry.hpp b/vesper/src/Telemetry/Telemetry.hpp index ca98bab..52677f9 100644 --- a/vesper/src/Telemetry/Telemetry.hpp +++ b/vesper/src/Telemetry/Telemetry.hpp @@ -77,6 +77,9 @@ private: // Spinlock for critical sections portMUX_TYPE telemetrySpinlock = portMUX_INITIALIZER_UNLOCKED; + // Pause flag for OTA freeze mode + volatile bool isPaused = false; + public: // Initialization void begin(); @@ -108,6 +111,10 @@ public: // Force stop callback (to be set by main application) 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 diff --git a/vesper/vesper.ino b/vesper/vesper.ino index 9275ecf..13355d0 100644 --- a/vesper/vesper.ino +++ b/vesper/vesper.ino @@ -395,7 +395,9 @@ void setup() // Initialize OTA Manager otaManager.begin(); 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) // UDP discovery setup can happen immediately without conflicts