Changed OTA to write Directly to flash
This commit is contained in:
@@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -348,27 +402,147 @@ bool OTAManager::checkVersion(const String& channel) {
|
|||||||
// ✅ ENHANCED: Download and install with size validation
|
// ✅ ENHANCED: Download and install with size validation
|
||||||
bool OTAManager::downloadAndInstall(const String& channel) {
|
bool OTAManager::downloadAndInstall(const String& channel) {
|
||||||
std::vector<String> servers = _configManager.getUpdateServers();
|
std::vector<String> servers = _configManager.getUpdateServers();
|
||||||
|
|
||||||
for (size_t serverIndex = 0; serverIndex < servers.size(); serverIndex++) {
|
for (size_t serverIndex = 0; serverIndex < servers.size(); serverIndex++) {
|
||||||
String baseUrl = servers[serverIndex];
|
String baseUrl = servers[serverIndex];
|
||||||
String firmwareUrl = baseUrl + "/ota/" + _configManager.getHardwareVariant() + "/" + channel + "/firmware.bin";
|
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());
|
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// All servers failed
|
// All servers failed
|
||||||
LOG_ERROR("OTA: All %d servers failed to provide firmware", servers.size());
|
LOG_ERROR("OTA: All %d servers failed to provide firmware", servers.size());
|
||||||
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,16 +558,24 @@ 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) {
|
||||||
LOG_ERROR("Download HTTP error code: %d", httpCode);
|
LOG_ERROR("Download HTTP error code: %d", httpCode);
|
||||||
setStatus(Status::FAILED, ErrorCode::HTTP_ERROR);
|
setStatus(Status::FAILED, ErrorCode::HTTP_ERROR);
|
||||||
http.end();
|
http.end();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
@@ -418,64 +600,146 @@ 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;
|
||||||
|
|
||||||
while (http.connected() && written < (size_t)contentLength) {
|
while (http.connected() && written < (size_t)contentLength) {
|
||||||
size_t available = stream->available();
|
size_t available = stream->available();
|
||||||
if (available) {
|
if (available) {
|
||||||
size_t toRead = min(available, sizeof(buffer));
|
size_t toRead = min(available, sizeof(buffer));
|
||||||
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);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
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");
|
||||||
@@ -483,7 +747,7 @@ bool OTAManager::downloadToSD(const String& url, const String& expectedChecksum,
|
|||||||
setStatus(Status::FAILED, ErrorCode::CHECKSUM_MISMATCH);
|
setStatus(Status::FAILED, ErrorCode::CHECKSUM_MISMATCH);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_INFO("Download and checksum verification successful");
|
LOG_INFO("Download and checksum verification successful");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -678,27 +942,22 @@ bool OTAManager::performManualUpdate(const String& channel) {
|
|||||||
LOG_WARNING("OTA update already in progress");
|
LOG_WARNING("OTA update already in progress");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for updates in the specified channel first
|
// Check for updates in the specified channel first
|
||||||
checkForUpdates(channel);
|
checkForUpdates(channel);
|
||||||
|
|
||||||
if (!_updateAvailable) {
|
if (!_updateAvailable) {
|
||||||
LOG_WARNING("No update available in %s channel", channel.c_str());
|
LOG_WARNING("No update available in %s channel", channel.c_str());
|
||||||
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");
|
||||||
@@ -863,6 +1115,52 @@ bool OTAManager::isHealthy() const {
|
|||||||
LOG_DEBUG("OTAManager: Unhealthy - Scheduled check timer not running");
|
LOG_DEBUG("OTAManager: Unhealthy - Scheduled check timer not running");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -127,6 +135,19 @@ private:
|
|||||||
TimerHandle_t _initialCheckTimer;
|
TimerHandle_t _initialCheckTimer;
|
||||||
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);
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -172,14 +172,17 @@ void Telemetry::telemetryTask(void* parameter) {
|
|||||||
LOG_INFO("Telemetry task started");
|
LOG_INFO("Telemetry task started");
|
||||||
|
|
||||||
while(1) {
|
while(1) {
|
||||||
// Only run if player is playing OR we're still cooling
|
// Skip processing if paused (OTA freeze mode)
|
||||||
bool isPlaying = (telemetry->playerIsPlayingPtr != nullptr) ?
|
if (!telemetry->isPaused) {
|
||||||
*(telemetry->playerIsPlayingPtr) : false;
|
// Only run if player is playing OR we're still cooling
|
||||||
|
bool isPlaying = (telemetry->playerIsPlayingPtr != nullptr) ?
|
||||||
|
*(telemetry->playerIsPlayingPtr) : false;
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
@@ -108,6 +111,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
|
||||||
|
|||||||
@@ -395,7 +395,9 @@ void setup()
|
|||||||
// Initialize OTA Manager
|
// Initialize OTA Manager
|
||||||
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
|
||||||
|
|||||||
Reference in New Issue
Block a user