From 93585f94ecc1d33b0526ec23b49fbbf44e616152 Mon Sep 17 00:00:00 2001 From: hhvrc Date: Thu, 10 Oct 2024 16:08:36 +0200 Subject: [PATCH 1/9] Refactor OTA Update Manager --- include/Common.h | 10 + include/config/OtaUpdateConfig.h | 4 +- include/http/FirmwareCDN.h | 28 + include/ota/FirmwareBinaryHash.h | 13 + include/ota/FirmwareReleaseInfo.h | 14 + include/{ => ota}/OtaUpdateChannel.h | 0 include/ota/OtaUpdateClient.h | 20 + include/{ => ota}/OtaUpdateManager.h | 12 +- include/{ => ota}/OtaUpdateStep.h | 0 src/GatewayClient.cpp | 2 +- src/OtaUpdateManager.cpp | 688 ------------------ .../websocket/gateway/OtaInstall.cpp | 2 +- src/http/FirmwareCDN.cpp | 157 ++++ src/main.cpp | 2 +- src/ota/OtaUpdateClient.cpp | 275 +++++++ src/ota/OtaUpdateManager.cpp | 326 +++++++++ 16 files changed, 850 insertions(+), 703 deletions(-) create mode 100644 include/http/FirmwareCDN.h create mode 100644 include/ota/FirmwareBinaryHash.h create mode 100644 include/ota/FirmwareReleaseInfo.h rename include/{ => ota}/OtaUpdateChannel.h (100%) create mode 100644 include/ota/OtaUpdateClient.h rename include/{ => ota}/OtaUpdateManager.h (59%) rename include/{ => ota}/OtaUpdateStep.h (100%) delete mode 100644 src/OtaUpdateManager.cpp create mode 100644 src/http/FirmwareCDN.cpp create mode 100644 src/ota/OtaUpdateClient.cpp create mode 100644 src/ota/OtaUpdateManager.cpp diff --git a/include/Common.h b/include/Common.h index 23c4d0e8..ca754ebb 100644 --- a/include/Common.h +++ b/include/Common.h @@ -21,6 +21,16 @@ #endif #define OPENSHOCK_FW_CDN_URL(path) "https://" OPENSHOCK_FW_CDN_DOMAIN path +#define OPENSHOCK_FW_CDN_CHANNEL_URL(ch) OPENSHOCK_FW_CDN_URL("/version-" ch ".txt") +#define OPENSHOCK_FW_CDN_STABLE_URL OPENSHOCK_FW_CDN_CHANNEL_URL("stable") +#define OPENSHOCK_FW_CDN_BETA_URL OPENSHOCK_FW_CDN_CHANNEL_URL("beta") +#define OPENSHOCK_FW_CDN_DEVELOP_URL OPENSHOCK_FW_CDN_CHANNEL_URL("develop") +#define OPENSHOCK_FW_CDN_BOARDS_BASE_URL_FORMAT OPENSHOCK_FW_CDN_URL("/%s") +#define OPENSHOCK_FW_CDN_BOARDS_INDEX_URL_FORMAT OPENSHOCK_FW_CDN_BOARDS_BASE_URL_FORMAT "/boards.txt" +#define OPENSHOCK_FW_CDN_VERSION_BASE_URL_FORMAT OPENSHOCK_FW_CDN_BOARDS_BASE_URL_FORMAT "/" OPENSHOCK_FW_BOARD +#define OPENSHOCK_FW_CDN_APP_URL_FORMAT OPENSHOCK_FW_CDN_VERSION_BASE_URL_FORMAT "/app.bin" +#define OPENSHOCK_FW_CDN_FILESYSTEM_URL_FORMAT OPENSHOCK_FW_CDN_VERSION_BASE_URL_FORMAT "/staticfs.bin" +#define OPENSHOCK_FW_CDN_SHA256_HASHES_URL_FORMAT OPENSHOCK_FW_CDN_VERSION_BASE_URL_FORMAT "/hashes.sha256.txt" #define OPENSHOCK_GPIO_INVALID -1 diff --git a/include/config/OtaUpdateConfig.h b/include/config/OtaUpdateConfig.h index 3bcec5ef..6323586c 100644 --- a/include/config/OtaUpdateConfig.h +++ b/include/config/OtaUpdateConfig.h @@ -2,8 +2,8 @@ #include "config/ConfigBase.h" #include "FirmwareBootType.h" -#include "OtaUpdateChannel.h" -#include "OtaUpdateStep.h" +#include "ota/OtaUpdateChannel.h" +#include "ota/OtaUpdateStep.h" #include diff --git a/include/http/FirmwareCDN.h b/include/http/FirmwareCDN.h new file mode 100644 index 00000000..7134a9d3 --- /dev/null +++ b/include/http/FirmwareCDN.h @@ -0,0 +1,28 @@ +#pragma once + +#include "http/HTTPRequestManager.h" +#include "ota/FirmwareBinaryHash.h" +#include "ota/OtaUpdateChannel.h" +#include "SemVer.h" + +#include + +namespace OpenShock::HTTP::FirmwareCDN { + /// @brief Fetches the firmware version for the given channel from the firmware CDN. + /// Valid response codes: 200, 304 + /// @param channel The channel to fetch the firmware version for. + /// @return The firmware version or an error response. + HTTP::Response GetFirmwareVersion(OtaUpdateChannel channel); + + /// @brief Fetches the list of available boards for the given firmware version from the firmware CDN. + /// Valid response codes: 200, 304 + /// @param version The firmware version to fetch the boards for. + /// @return The list of available boards or an error response. + HTTP::Response> GetFirmwareBoards(const OpenShock::SemVer& version); + + /// @brief Fetches the binary hashes for the given firmware version from the firmware CDN. + /// Valid response codes: 200, 304 + /// @param version The firmware version to fetch the binary hashes for. + /// @return The binary hashes or an error response. + HTTP::Response> GetFirmwareBinaryHashes(const OpenShock::SemVer& version); +} // namespace OpenShock::HTTP::FirmwareCDN diff --git a/include/ota/FirmwareBinaryHash.h b/include/ota/FirmwareBinaryHash.h new file mode 100644 index 00000000..72442c52 --- /dev/null +++ b/include/ota/FirmwareBinaryHash.h @@ -0,0 +1,13 @@ +#pragma once + +#include +#include + +namespace OpenShock +{ + struct FirmwareBinaryHash + { + std::string name; + uint8_t hash[32]; + }; +} // namespace OpenShock diff --git a/include/ota/FirmwareReleaseInfo.h b/include/ota/FirmwareReleaseInfo.h new file mode 100644 index 00000000..63a59e7e --- /dev/null +++ b/include/ota/FirmwareReleaseInfo.h @@ -0,0 +1,14 @@ +#pragma once + +#include +#include + +namespace OpenShock +{ + struct FirmwareReleaseInfo { + std::string appBinaryUrl; + uint8_t appBinaryHash[32]; + std::string filesystemBinaryUrl; + uint8_t filesystemBinaryHash[32]; + }; +} // namespace OpenShock diff --git a/include/OtaUpdateChannel.h b/include/ota/OtaUpdateChannel.h similarity index 100% rename from include/OtaUpdateChannel.h rename to include/ota/OtaUpdateChannel.h diff --git a/include/ota/OtaUpdateClient.h b/include/ota/OtaUpdateClient.h new file mode 100644 index 00000000..37505ebb --- /dev/null +++ b/include/ota/OtaUpdateClient.h @@ -0,0 +1,20 @@ +#pragma once + +#include "SemVer.h" + +#include + +namespace OpenShock { + class OtaUpdateClient { + public: + OtaUpdateClient(const OpenShock::SemVer& version); + ~OtaUpdateClient(); + + bool Start(); + private: + void _task(); + + OpenShock::SemVer m_version; + TaskHandle_t m_taskHandle; + }; +} diff --git a/include/OtaUpdateManager.h b/include/ota/OtaUpdateManager.h similarity index 59% rename from include/OtaUpdateManager.h rename to include/ota/OtaUpdateManager.h index fc2b6b6a..2816817a 100644 --- a/include/OtaUpdateManager.h +++ b/include/ota/OtaUpdateManager.h @@ -1,6 +1,7 @@ #pragma once #include "FirmwareBootType.h" +#include "FirmwareReleaseInfo.h" #include "OtaUpdateChannel.h" #include "SemVer.h" @@ -12,16 +13,7 @@ namespace OpenShock::OtaUpdateManager { [[nodiscard]] bool Init(); - struct FirmwareRelease { - std::string appBinaryUrl; - uint8_t appBinaryHash[32]; - std::string filesystemBinaryUrl; - uint8_t filesystemBinaryHash[32]; - }; - - bool TryGetFirmwareVersion(OtaUpdateChannel channel, OpenShock::SemVer& version); - bool TryGetFirmwareBoards(const OpenShock::SemVer& version, std::vector& boards); - bool TryGetFirmwareRelease(const OpenShock::SemVer& version, FirmwareRelease& release); + bool TryGetFirmwareRelease(const OpenShock::SemVer& version, FirmwareReleaseInfo& release); bool TryStartFirmwareInstallation(const OpenShock::SemVer& version); diff --git a/include/OtaUpdateStep.h b/include/ota/OtaUpdateStep.h similarity index 100% rename from include/OtaUpdateStep.h rename to include/ota/OtaUpdateStep.h diff --git a/src/GatewayClient.cpp b/src/GatewayClient.cpp index 3bd2d0b0..d55708c8 100644 --- a/src/GatewayClient.cpp +++ b/src/GatewayClient.cpp @@ -6,7 +6,7 @@ const char* const TAG = "GatewayClient"; #include "config/Config.h" #include "event_handlers/WebSocket.h" #include "Logging.h" -#include "OtaUpdateManager.h" +#include "ota/OtaUpdateManager.h" #include "serialization/WSGateway.h" #include "Time.h" #include "util/CertificateUtils.h" diff --git a/src/OtaUpdateManager.cpp b/src/OtaUpdateManager.cpp deleted file mode 100644 index 907bda9a..00000000 --- a/src/OtaUpdateManager.cpp +++ /dev/null @@ -1,688 +0,0 @@ -#include "OtaUpdateManager.h" - -const char* const TAG = "OtaUpdateManager"; - -#include "CaptivePortal.h" -#include "Common.h" -#include "config/Config.h" -#include "GatewayConnectionManager.h" -#include "Hashing.h" -#include "http/HTTPRequestManager.h" -#include "Logging.h" -#include "SemVer.h" -#include "serialization/WSGateway.h" -#include "Time.h" -#include "util/HexUtils.h" -#include "util/PartitionUtils.h" -#include "util/StringUtils.h" -#include "util/TaskUtils.h" -#include "wifi/WiFiManager.h" - -#include -#include - -#include -#include - -#include -#include - -using namespace std::string_view_literals; - -#define OPENSHOCK_FW_CDN_CHANNEL_URL(ch) OPENSHOCK_FW_CDN_URL("/version-" ch ".txt") - -#define OPENSHOCK_FW_CDN_STABLE_URL OPENSHOCK_FW_CDN_CHANNEL_URL("stable") -#define OPENSHOCK_FW_CDN_BETA_URL OPENSHOCK_FW_CDN_CHANNEL_URL("beta") -#define OPENSHOCK_FW_CDN_DEVELOP_URL OPENSHOCK_FW_CDN_CHANNEL_URL("develop") - -#define OPENSHOCK_FW_CDN_BOARDS_BASE_URL_FORMAT OPENSHOCK_FW_CDN_URL("/%s") -#define OPENSHOCK_FW_CDN_BOARDS_INDEX_URL_FORMAT OPENSHOCK_FW_CDN_BOARDS_BASE_URL_FORMAT "/boards.txt" - -#define OPENSHOCK_FW_CDN_VERSION_BASE_URL_FORMAT OPENSHOCK_FW_CDN_BOARDS_BASE_URL_FORMAT "/" OPENSHOCK_FW_BOARD - -#define OPENSHOCK_FW_CDN_APP_URL_FORMAT OPENSHOCK_FW_CDN_VERSION_BASE_URL_FORMAT "/app.bin" -#define OPENSHOCK_FW_CDN_FILESYSTEM_URL_FORMAT OPENSHOCK_FW_CDN_VERSION_BASE_URL_FORMAT "/staticfs.bin" -#define OPENSHOCK_FW_CDN_SHA256_HASHES_URL_FORMAT OPENSHOCK_FW_CDN_VERSION_BASE_URL_FORMAT "/hashes.sha256.txt" - -/// @brief Stops initArduino() from handling OTA rollbacks -/// @todo Get rid of Arduino entirely. >:( -/// -/// @see .platformio/packages/framework-arduinoespressif32/cores/esp32/esp32-hal-misc.c -/// @return true -bool verifyRollbackLater() { - return true; -} - -using namespace OpenShock; - -enum OtaTaskEventFlag : uint32_t { - OTA_TASK_EVENT_UPDATE_REQUESTED = 1 << 0, - OTA_TASK_EVENT_WIFI_DISCONNECTED = 1 << 1, // If both connected and disconnected are set, disconnected takes priority. - OTA_TASK_EVENT_WIFI_CONNECTED = 1 << 2, -}; - -static esp_ota_img_states_t _otaImageState; -static OpenShock::FirmwareBootType _bootType; -static TaskHandle_t _taskHandle; -static OpenShock::SemVer _requestedVersion; -static SemaphoreHandle_t _requestedVersionMutex = xSemaphoreCreateMutex(); - -bool _tryQueueUpdateRequest(const OpenShock::SemVer& version) { - if (xSemaphoreTake(_requestedVersionMutex, pdMS_TO_TICKS(1000)) != pdTRUE) { - OS_LOGE(TAG, "Failed to take requested version mutex"); - return false; - } - - _requestedVersion = version; - - xSemaphoreGive(_requestedVersionMutex); - - xTaskNotify(_taskHandle, OTA_TASK_EVENT_UPDATE_REQUESTED, eSetBits); - - return true; -} - -bool _tryGetRequestedVersion(OpenShock::SemVer& version) { - if (xSemaphoreTake(_requestedVersionMutex, pdMS_TO_TICKS(1000)) != pdTRUE) { - OS_LOGE(TAG, "Failed to take requested version mutex"); - return false; - } - - version = _requestedVersion; - - xSemaphoreGive(_requestedVersionMutex); - - return true; -} - -void _otaEvGotIPHandler(arduino_event_t* event) { - (void)event; - xTaskNotify(_taskHandle, OTA_TASK_EVENT_WIFI_CONNECTED, eSetBits); -} -void _otaEvWiFiDisconnectedHandler(arduino_event_t* event) { - (void)event; - xTaskNotify(_taskHandle, OTA_TASK_EVENT_WIFI_DISCONNECTED, eSetBits); -} - -bool _sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask task, float progress) { - int32_t updateId; - if (!Config::GetOtaUpdateId(updateId)) { - OS_LOGE(TAG, "Failed to get OTA update ID"); - return false; - } - - if (!Serialization::Gateway::SerializeOtaInstallProgressMessage(updateId, task, progress, GatewayConnectionManager::SendMessageBIN)) { - OS_LOGE(TAG, "Failed to send OTA install progress message"); - return false; - } - - return true; -} -bool _sendFailureMessage(std::string_view message, bool fatal = false) { - int32_t updateId; - if (!Config::GetOtaUpdateId(updateId)) { - OS_LOGE(TAG, "Failed to get OTA update ID"); - return false; - } - - if (!Serialization::Gateway::SerializeOtaInstallFailedMessage(updateId, message, fatal, GatewayConnectionManager::SendMessageBIN)) { - OS_LOGE(TAG, "Failed to send OTA install failed message"); - return false; - } - - return true; -} - -bool _flashAppPartition(const esp_partition_t* partition, std::string_view remoteUrl, const uint8_t (&remoteHash)[32]) { - OS_LOGD(TAG, "Flashing app partition"); - - if (!_sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask::FlashingApplication, 0.0f)) { - return false; - } - - auto onProgress = [](std::size_t current, std::size_t total, float progress) -> bool { - OS_LOGD(TAG, "Flashing app partition: %u / %u (%.2f%%)", current, total, progress * 100.0f); - - _sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask::FlashingApplication, progress); - - return true; - }; - - if (!OpenShock::FlashPartitionFromUrl(partition, remoteUrl, remoteHash, onProgress)) { - OS_LOGE(TAG, "Failed to flash app partition"); - _sendFailureMessage("Failed to flash app partition"sv); - return false; - } - - if (!_sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask::MarkingApplicationBootable, 0.0f)) { - return false; - } - - // Set app partition bootable. - if (esp_ota_set_boot_partition(partition) != ESP_OK) { - OS_LOGE(TAG, "Failed to set app partition bootable"); - _sendFailureMessage("Failed to set app partition bootable"sv); - return false; - } - - return true; -} - -bool _flashFilesystemPartition(const esp_partition_t* parition, std::string_view remoteUrl, const uint8_t (&remoteHash)[32]) { - if (!_sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask::PreparingForInstall, 0.0f)) { - return false; - } - - // Make sure captive portal is stopped, timeout after 5 seconds. - if (!CaptivePortal::ForceClose(5000U)) { - OS_LOGE(TAG, "Failed to force close captive portal (timed out)"); - _sendFailureMessage("Failed to force close captive portal (timed out)"sv); - return false; - } - - OS_LOGD(TAG, "Flashing filesystem partition"); - - if (!_sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask::FlashingFilesystem, 0.0f)) { - return false; - } - - auto onProgress = [](std::size_t current, std::size_t total, float progress) -> bool { - OS_LOGD(TAG, "Flashing filesystem partition: %u / %u (%.2f%%)", current, total, progress * 100.0f); - - _sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask::FlashingFilesystem, progress); - - return true; - }; - - if (!OpenShock::FlashPartitionFromUrl(parition, remoteUrl, remoteHash, onProgress)) { - OS_LOGE(TAG, "Failed to flash filesystem partition"); - _sendFailureMessage("Failed to flash filesystem partition"sv); - return false; - } - - if (!_sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask::VerifyingFilesystem, 0.0f)) { - return false; - } - - // Attempt to mount filesystem. - fs::LittleFSFS test; - if (!test.begin(false, "/static", 10, "static0")) { - OS_LOGE(TAG, "Failed to mount filesystem"); - _sendFailureMessage("Failed to mount filesystem"sv); - return false; - } - test.end(); - - OpenShock::CaptivePortal::ForceClose(false); - - return true; -} - -void _otaUpdateTask(void* arg) { - (void)arg; - - OS_LOGD(TAG, "OTA update task started"); - - bool connected = false; - bool updateRequested = false; - int64_t lastUpdateCheck = 0; - - // Update task loop. - while (true) { - // Wait for event. - uint32_t eventBits = 0; - xTaskNotifyWait(0, UINT32_MAX, &eventBits, pdMS_TO_TICKS(5000)); // TODO: wait for rest time - - updateRequested |= (eventBits & OTA_TASK_EVENT_UPDATE_REQUESTED) != 0; - - if ((eventBits & OTA_TASK_EVENT_WIFI_DISCONNECTED) != 0) { - OS_LOGD(TAG, "WiFi disconnected"); - connected = false; - continue; // No further processing needed. - } - - if ((eventBits & OTA_TASK_EVENT_WIFI_CONNECTED) != 0 && !connected) { - OS_LOGD(TAG, "WiFi connected"); - connected = true; - } - - // If we're not connected, continue. - if (!connected) { - continue; - } - - int64_t now = OpenShock::millis(); - - Config::OtaUpdateConfig config; - if (!Config::GetOtaUpdateConfig(config)) { - OS_LOGE(TAG, "Failed to get OTA update config"); - continue; - } - - if (!config.isEnabled) { - OS_LOGD(TAG, "OTA updates are disabled, skipping update check"); - continue; - } - - bool firstCheck = lastUpdateCheck == 0; - int64_t diff = now - lastUpdateCheck; - int64_t diffMins = diff / 60'000LL; - - bool check = false; - check |= config.checkOnStartup && firstCheck; // On startup - check |= config.checkPeriodically && diffMins >= config.checkInterval; // Periodically - check |= updateRequested && (firstCheck || diffMins >= 1); // Update requested - - if (!check) { - continue; - } - - lastUpdateCheck = now; - - if (config.requireManualApproval) { - OS_LOGD(TAG, "Manual approval required, skipping update check"); - // TODO: IMPLEMENT - continue; - } - - OpenShock::SemVer version; - if (updateRequested) { - updateRequested = false; - - if (!_tryGetRequestedVersion(version)) { - OS_LOGE(TAG, "Failed to get requested version"); - continue; - } - - OS_LOGD(TAG, "Update requested for version %s", version.toString().c_str()); // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this - } else { - OS_LOGD(TAG, "Checking for updates"); - - // Fetch current version. - if (!OtaUpdateManager::TryGetFirmwareVersion(config.updateChannel, version)) { - OS_LOGE(TAG, "Failed to fetch firmware version"); - continue; - } - - OS_LOGD(TAG, "Remote version: %s", version.toString().c_str()); // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this - } - - if (version.toString() == OPENSHOCK_FW_VERSION) { // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this - OS_LOGI(TAG, "Requested version is already installed"); - continue; - } - - // Generate random int32_t for this update. - int32_t updateId = static_cast(esp_random()); - if (!Config::SetOtaUpdateId(updateId)) { - OS_LOGE(TAG, "Failed to set OTA update ID"); - continue; - } - if (!Config::SetOtaUpdateStep(OpenShock::OtaUpdateStep::Updating)) { - OS_LOGE(TAG, "Failed to set OTA update step"); - continue; - } - - if (!Serialization::Gateway::SerializeOtaInstallStartedMessage(updateId, version, GatewayConnectionManager::SendMessageBIN)) { - OS_LOGE(TAG, "Failed to serialize OTA install started message"); - continue; - } - - if (!_sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask::FetchingMetadata, 0.0f)) { - continue; - } - - // Fetch current release. - OtaUpdateManager::FirmwareRelease release; - if (!OtaUpdateManager::TryGetFirmwareRelease(version, release)) { - OS_LOGE(TAG, "Failed to fetch firmware release"); // TODO: Send error message to server - _sendFailureMessage("Failed to fetch firmware release"sv); - continue; - } - - // Print release. - OS_LOGD(TAG, "Firmware release:"); - OS_LOGD(TAG, " Version: %s", version.toString().c_str()); // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this - OS_LOGD(TAG, " App binary URL: %s", release.appBinaryUrl.c_str()); - OS_LOGD(TAG, " App binary hash: %s", HexUtils::ToHex<32>(release.appBinaryHash).data()); - OS_LOGD(TAG, " Filesystem binary URL: %s", release.filesystemBinaryUrl.c_str()); - OS_LOGD(TAG, " Filesystem binary hash: %s", HexUtils::ToHex<32>(release.filesystemBinaryHash).data()); - - // Get available app update partition. - const esp_partition_t* appPartition = esp_ota_get_next_update_partition(nullptr); - if (appPartition == nullptr) { - OS_LOGE(TAG, "Failed to get app update partition"); // TODO: Send error message to server - _sendFailureMessage("Failed to get app update partition"sv); - continue; - } - - // Get filesystem partition. - const esp_partition_t* filesystemPartition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, "static0"); - if (filesystemPartition == nullptr) { - OS_LOGE(TAG, "Failed to find filesystem partition"); // TODO: Send error message to server - _sendFailureMessage("Failed to find filesystem partition"sv); - continue; - } - - // Increase task watchdog timeout. - // Prevents panics on some ESP32s when clearing large partitions. - esp_task_wdt_init(15, true); - - // Flash app and filesystem partitions. - if (!_flashFilesystemPartition(filesystemPartition, release.filesystemBinaryUrl, release.filesystemBinaryHash)) continue; - if (!_flashAppPartition(appPartition, release.appBinaryUrl, release.appBinaryHash)) continue; - - // Set OTA boot type in config. - if (!Config::SetOtaUpdateStep(OpenShock::OtaUpdateStep::Updated)) { - OS_LOGE(TAG, "Failed to set OTA update step"); - _sendFailureMessage("Failed to set OTA update step"sv); - continue; - } - - // Set task watchdog timeout back to default. - esp_task_wdt_init(5, true); - - // Send reboot message. - _sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask::Rebooting, 0.0f); - - // Reboot into new firmware. - OS_LOGI(TAG, "Restarting into new firmware..."); - vTaskDelay(pdMS_TO_TICKS(200)); - break; - } - - // Restart. - esp_restart(); -} - -bool _tryGetStringList(std::string_view url, std::vector& list) { - auto response = OpenShock::HTTP::GetString( - url, - { - {"Accept", "text/plain"} - }, - {200, 304} - ); - if (response.result != OpenShock::HTTP::RequestResult::Success) { - OS_LOGE(TAG, "Failed to fetch list: [%u] %s", response.code, response.data.c_str()); - return false; - } - - list.clear(); - - std::string_view data = response.data; - - auto lines = OpenShock::StringSplitNewLines(data); - list.reserve(lines.size()); - - for (auto line : lines) { - line = OpenShock::StringTrim(line); - - if (line.empty()) { - continue; - } - - list.push_back(std::string(line)); - } - - return true; -} - -bool OtaUpdateManager::Init() { - OS_LOGN(TAG, "Fetching current partition"); - - // Fetch current partition info. - const esp_partition_t* partition = esp_ota_get_running_partition(); - if (partition == nullptr) { - OS_PANIC(TAG, "Failed to get currently running partition"); - return false; // This will never be reached, but the compiler doesn't know that. - } - - OS_LOGD(TAG, "Fetching partition state"); - - // Get OTA state for said partition. - esp_err_t err = esp_ota_get_state_partition(partition, &_otaImageState); - if (err != ESP_OK) { - OS_PANIC(TAG, "Failed to get partition state: %s", esp_err_to_name(err)); - return false; // This will never be reached, but the compiler doesn't know that. - } - - OS_LOGD(TAG, "Fetching previous update step"); - OtaUpdateStep updateStep; - if (!Config::GetOtaUpdateStep(updateStep)) { - OS_LOGE(TAG, "Failed to get OTA update step"); - return false; - } - - // Infer boot type from update step. - switch (updateStep) { - case OtaUpdateStep::Updated: - _bootType = FirmwareBootType::NewFirmware; - break; - case OtaUpdateStep::Validating: // If the update step is validating, we have failed in the middle of validating the new firmware, meaning this is a rollback. - case OtaUpdateStep::RollingBack: - _bootType = FirmwareBootType::Rollback; - break; - default: - _bootType = FirmwareBootType::Normal; - break; - } - - if (updateStep == OtaUpdateStep::Updated) { - if (!Config::SetOtaUpdateStep(OtaUpdateStep::Validating)) { - OS_PANIC(TAG, "Failed to set OTA update step in critical section"); // TODO: THIS IS A CRITICAL SECTION, WHAT DO WE DO? - } - } - - WiFi.onEvent(_otaEvGotIPHandler, ARDUINO_EVENT_WIFI_STA_GOT_IP); - WiFi.onEvent(_otaEvGotIPHandler, ARDUINO_EVENT_WIFI_STA_GOT_IP6); - WiFi.onEvent(_otaEvWiFiDisconnectedHandler, ARDUINO_EVENT_WIFI_STA_DISCONNECTED); - - // Start OTA update task. - TaskUtils::TaskCreateExpensive(_otaUpdateTask, "OTA Update", 8192, nullptr, 1, &_taskHandle); // PROFILED: 6.2KB stack usage - - return true; -} - -bool OtaUpdateManager::TryGetFirmwareVersion(OtaUpdateChannel channel, OpenShock::SemVer& version) { - std::string_view channelIndexUrl; - switch (channel) { - case OtaUpdateChannel::Stable: - channelIndexUrl = std::string_view(OPENSHOCK_FW_CDN_STABLE_URL); - break; - case OtaUpdateChannel::Beta: - channelIndexUrl = std::string_view(OPENSHOCK_FW_CDN_BETA_URL); - break; - case OtaUpdateChannel::Develop: - channelIndexUrl = std::string_view(OPENSHOCK_FW_CDN_DEVELOP_URL); - break; - default: - OS_LOGE(TAG, "Unknown channel: %u", channel); - return false; - } - - OS_LOGD(TAG, "Fetching firmware version from %s", channelIndexUrl); - - auto response = OpenShock::HTTP::GetString( - channelIndexUrl, - { - {"Accept", "text/plain"} - }, - {200, 304} - ); - if (response.result != OpenShock::HTTP::RequestResult::Success) { - OS_LOGE(TAG, "Failed to fetch firmware version: [%u] %s", response.code, response.data.c_str()); - return false; - } - - if (!OpenShock::TryParseSemVer(response.data, version)) { - OS_LOGE(TAG, "Failed to parse firmware version: %.*s", response.data.size(), response.data.data()); - return false; - } - - return true; -} - -bool OtaUpdateManager::TryGetFirmwareBoards(const OpenShock::SemVer& version, std::vector& boards) { - std::string channelIndexUrl; - if (!FormatToString(channelIndexUrl, OPENSHOCK_FW_CDN_BOARDS_INDEX_URL_FORMAT, version.toString().c_str())) { // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this - OS_LOGE(TAG, "Failed to format URL"); - return false; - } - - OS_LOGD(TAG, "Fetching firmware boards from %s", channelIndexUrl.c_str()); - - if (!_tryGetStringList(channelIndexUrl, boards)) { - OS_LOGE(TAG, "Failed to fetch firmware boards"); - return false; - } - - return true; -} - -bool _tryParseIntoHash(std::string_view hash, uint8_t (&hashBytes)[32]) { - if (!HexUtils::TryParseHex(hash.data(), hash.size(), hashBytes, 32)) { - OS_LOGE(TAG, "Failed to parse hash: %.*s", hash.size(), hash.data()); - return false; - } - - return true; -} - -bool OtaUpdateManager::TryGetFirmwareRelease(const OpenShock::SemVer& version, FirmwareRelease& release) { - auto versionStr = version.toString(); // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this - - if (!FormatToString(release.appBinaryUrl, OPENSHOCK_FW_CDN_APP_URL_FORMAT, versionStr.c_str())) { - OS_LOGE(TAG, "Failed to format URL"); - return false; - } - - if (!FormatToString(release.filesystemBinaryUrl, OPENSHOCK_FW_CDN_FILESYSTEM_URL_FORMAT, versionStr.c_str())) { - OS_LOGE(TAG, "Failed to format URL"); - return false; - } - - // Construct hash URLs. - std::string sha256HashesUrl; - if (!FormatToString(sha256HashesUrl, OPENSHOCK_FW_CDN_SHA256_HASHES_URL_FORMAT, versionStr.c_str())) { - OS_LOGE(TAG, "Failed to format URL"); - return false; - } - - // Fetch hashes. - auto sha256HashesResponse = OpenShock::HTTP::GetString( - sha256HashesUrl, - { - {"Accept", "text/plain"} - }, - {200, 304} - ); - if (sha256HashesResponse.result != OpenShock::HTTP::RequestResult::Success) { - OS_LOGE(TAG, "Failed to fetch hashes: [%u] %s", sha256HashesResponse.code, sha256HashesResponse.data.c_str()); - return false; - } - - auto hashesLines = OpenShock::StringSplitNewLines(sha256HashesResponse.data); - - // Parse hashes. - bool foundAppHash = false, foundFilesystemHash = false; - for (std::string_view line : hashesLines) { - auto parts = OpenShock::StringSplitWhiteSpace(line); - if (parts.size() != 2) { - OS_LOGE(TAG, "Invalid hashes entry: %.*s", line.size(), line.data()); - return false; - } - - auto hash = OpenShock::StringTrim(parts[0]); - auto file = OpenShock::StringTrim(parts[1]); - - if (OpenShock::StringStartsWith(file, "./"sv)) { - file = file.substr(2); - } - - if (hash.size() != 64) { - OS_LOGE(TAG, "Invalid hash: %.*s", hash.size(), hash.data()); - return false; - } - - if (file == "app.bin") { - if (foundAppHash) { - OS_LOGE(TAG, "Duplicate hash for app.bin"); - return false; - } - - if (!_tryParseIntoHash(hash, release.appBinaryHash)) { - return false; - } - - foundAppHash = true; - } else if (file == "staticfs.bin") { - if (foundFilesystemHash) { - OS_LOGE(TAG, "Duplicate hash for staticfs.bin"); - return false; - } - - if (!_tryParseIntoHash(hash, release.filesystemBinaryHash)) { - return false; - } - - foundFilesystemHash = true; - } - } - - return true; -} - -bool OtaUpdateManager::TryStartFirmwareInstallation(const OpenShock::SemVer& version) { - OS_LOGD(TAG, "Requesting firmware version %s", version.toString().c_str()); // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this - - return _tryQueueUpdateRequest(version); -} - -FirmwareBootType OtaUpdateManager::GetFirmwareBootType() { - return _bootType; -} - -bool OtaUpdateManager::IsValidatingApp() { - return _otaImageState == ESP_OTA_IMG_PENDING_VERIFY; -} - -void OtaUpdateManager::InvalidateAndRollback() { - // Set OTA boot type in config. - if (!Config::SetOtaUpdateStep(OpenShock::OtaUpdateStep::RollingBack)) { - OS_PANIC(TAG, "Failed to set OTA firmware boot type in critical section"); // TODO: THIS IS A CRITICAL SECTION, WHAT DO WE DO? - return; - } - - switch (esp_ota_mark_app_invalid_rollback_and_reboot()) { - case ESP_FAIL: - OS_LOGE(TAG, "Rollback failed (ESP_FAIL)"); - break; - case ESP_ERR_OTA_ROLLBACK_FAILED: - OS_LOGE(TAG, "Rollback failed (ESP_ERR_OTA_ROLLBACK_FAILED)"); - break; - default: - OS_LOGE(TAG, "Rollback failed (Unknown)"); - break; - } - - // Set OTA boot type in config. - if (!Config::SetOtaUpdateStep(OpenShock::OtaUpdateStep::None)) { - OS_LOGE(TAG, "Failed to set OTA firmware boot type"); - } - - esp_restart(); -} - -void OtaUpdateManager::ValidateApp() { - if (esp_ota_mark_app_valid_cancel_rollback() != ESP_OK) { - OS_PANIC(TAG, "Unable to mark app as valid, WTF?"); // TODO: Wtf do we do here? - } - - // Set OTA boot type in config. - if (!Config::SetOtaUpdateStep(OpenShock::OtaUpdateStep::Validated)) { - OS_PANIC(TAG, "Failed to set OTA firmware boot type in critical section"); // TODO: THIS IS A CRITICAL SECTION, WHAT DO WE DO? - } - - _otaImageState = ESP_OTA_IMG_VALID; -} diff --git a/src/event_handlers/websocket/gateway/OtaInstall.cpp b/src/event_handlers/websocket/gateway/OtaInstall.cpp index 55812ce1..f79849df 100644 --- a/src/event_handlers/websocket/gateway/OtaInstall.cpp +++ b/src/event_handlers/websocket/gateway/OtaInstall.cpp @@ -4,7 +4,7 @@ const char* const TAG = "ServerMessageHandlers"; #include "CaptivePortal.h" #include "Logging.h" -#include "OtaUpdateManager.h" +#include "ota/OtaUpdateManager.h" #include diff --git a/src/http/FirmwareCDN.cpp b/src/http/FirmwareCDN.cpp new file mode 100644 index 00000000..35ed5670 --- /dev/null +++ b/src/http/FirmwareCDN.cpp @@ -0,0 +1,157 @@ +#include + +#include "http/FirmwareCDN.h" + +#include "Common.h" +#include "Logging.h" +#include "util/StringUtils.h" +#include "util/HexUtils.h" + +const char* const TAG = "FirmwareCDN"; + +using namespace std::string_view_literals; + + +using namespace OpenShock; + +HTTP::Response HTTP::FirmwareCDN::GetFirmwareVersion(OtaUpdateChannel channel) { + std::string_view channelIndexUrl; + switch (channel) { + case OtaUpdateChannel::Stable: + channelIndexUrl = OPENSHOCK_FW_CDN_STABLE_URL""sv; + break; + case OtaUpdateChannel::Beta: + channelIndexUrl = OPENSHOCK_FW_CDN_BETA_URL""sv; + break; + case OtaUpdateChannel::Develop: + channelIndexUrl = OPENSHOCK_FW_CDN_DEVELOP_URL""sv; + break; + default: + OS_LOGE(TAG, "Unknown channel: %u", channel); + return {RequestResult::InternalError, 0, {}}; + } + + OS_LOGD(TAG, "Fetching firmware version from %.*s", channelIndexUrl.size(), channelIndexUrl.data()); + + auto response = OpenShock::HTTP::GetString( + channelIndexUrl, + { + {"Accept", "text/plain"} + }, + {200, 304} + ); + + if (response.result != OpenShock::HTTP::RequestResult::Success) { + OS_LOGE(TAG, "Failed to fetch firmware version: [%u] %s", response.code, response.data.c_str()); + return {RequestResult::InternalError, 0, {}}; + } + + OpenShock::SemVer version; + if (!OpenShock::TryParseSemVer(response.data, version)) { + OS_LOGE(TAG, "Failed to parse firmware version: %.*s", response.data.size(), response.data.data()); + return {RequestResult::ParseFailed, response.code, {}}; + } + + return {response.result, response.code, version}; +} + +HTTP::Response> HTTP::FirmwareCDN::GetFirmwareBoards(const OpenShock::SemVer& version) { + std::string channelIndexUrl; + if (!FormatToString(channelIndexUrl, OPENSHOCK_FW_CDN_BOARDS_INDEX_URL_FORMAT, version.toString().c_str())) { // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this + OS_LOGE(TAG, "Failed to format URL"); + return {RequestResult::InternalError, 0, {}}; + } + + OS_LOGD(TAG, "Fetching firmware boards from %s", channelIndexUrl.c_str()); + + auto response = OpenShock::HTTP::GetString( + channelIndexUrl, + { + {"Accept", "text/plain"} + }, + {200, 304} + ); + + if (response.result != OpenShock::HTTP::RequestResult::Success) { + OS_LOGE(TAG, "Failed to fetch firmware boards: [%u] %s", response.code, response.data.c_str()); + return {RequestResult::InternalError, 0, {}}; + } + + auto lines = OpenShock::StringSplitNewLines(response.data); + + std::vector boards; + boards.reserve(lines.size()); + + for (auto line : lines) { + line = OpenShock::StringTrim(line); + + if (line.empty()) { + continue; + } + + boards.push_back(std::string(line)); + } + + return {response.result, response.code, std::move(boards)}; +} + +HTTP::Response> HTTP::FirmwareCDN::GetFirmwareBinaryHashes(const OpenShock::SemVer& version) { + auto versionStr = version.toString(); // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this + + // Construct hash URLs. + std::string sha256HashesUrl; + if (!FormatToString(sha256HashesUrl, OPENSHOCK_FW_CDN_SHA256_HASHES_URL_FORMAT, versionStr.c_str())) { + OS_LOGE(TAG, "Failed to format URL"); + return {RequestResult::InternalError, 0, {}}; + } + + // Fetch hashes. + auto sha256HashesResponse = OpenShock::HTTP::GetString( + sha256HashesUrl, + { + {"Accept", "text/plain"} + }, + {200, 304} + ); + if (sha256HashesResponse.result != OpenShock::HTTP::RequestResult::Success) { + OS_LOGE(TAG, "Failed to fetch hashes: [%u] %s", sha256HashesResponse.code, sha256HashesResponse.data.c_str()); + return {RequestResult::InternalError, 0, {}}; + } + + auto hashesLines = OpenShock::StringSplitNewLines(sha256HashesResponse.data); + + // Parse hashes. + std::vector hashes; + for (std::string_view line : hashesLines) { + auto parts = OpenShock::StringSplitWhiteSpace(line); + if (parts.size() != 2) { + OS_LOGE(TAG, "Invalid hashes entry: %.*s", line.size(), line.data()); + return {RequestResult::InternalError, 0, {}}; + } + + auto hash = OpenShock::StringTrim(parts[0]); + auto file = OpenShock::StringTrim(parts[1]); + + if (OpenShock::StringStartsWith(file, "./"sv)) { + file = file.substr(2); + } + + if (hash.size() != 64) { + OS_LOGE(TAG, "Invalid hash: %.*s", hash.size(), hash.data()); + return {RequestResult::InternalError, 0, {}}; + } + + FirmwareBinaryHash binaryHash; + + if (!HexUtils::TryParseHex(hash.data(), hash.size(), binaryHash.hash, sizeof(binaryHash.hash))) { + OS_LOGE(TAG, "Failed to parse hash: %.*s", hash.size(), hash.data()); + return {RequestResult::InternalError, 0, {}}; + } + + binaryHash.name = std::string(file); + + hashes.push_back(std::move(binaryHash)); + } + + return {RequestResult::Success, 200, std::move(hashes)}; +} diff --git a/src/main.cpp b/src/main.cpp index 42fa8cb7..6028d91f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -9,7 +9,7 @@ const char* const TAG = "main"; #include "EStopManager.h" #include "GatewayConnectionManager.h" #include "Logging.h" -#include "OtaUpdateManager.h" +#include "ota/OtaUpdateManager.h" #include "serial/SerialInputHandler.h" #include "util/TaskUtils.h" #include "VisualStateManager.h" diff --git a/src/ota/OtaUpdateClient.cpp b/src/ota/OtaUpdateClient.cpp new file mode 100644 index 00000000..b84db1d2 --- /dev/null +++ b/src/ota/OtaUpdateClient.cpp @@ -0,0 +1,275 @@ +#include + +#include "Logging.h" +#include "ota/OtaUpdateClient.h" +#include "ota/OtaUpdateStep.h" +#include "serialization/WSGateway.h" + +#include "util/FnProxy.h" +#include "util/HextUtils.h" +#include "util/PartitionUtils.h" +#include "util/TaskUtils.h" + +#include +#include +#include + +#include + +const char* const TAG = "OtaUpdateClient"; + +using namespace OpenShock; + +bool _tryStartUpdate(const OpenShock::SemVer& version) { + if (xSemaphoreTake(_requestedVersionMutex, pdMS_TO_TICKS(1000)) != pdTRUE) { + OS_LOGE(TAG, "Failed to take requested version mutex"); + return false; + } + + _requestedVersion = version; + + xSemaphoreGive(_requestedVersionMutex); + + xTaskNotify(_taskHandle, OTA_TASK_EVENT_UPDATE_REQUESTED, eSetBits); + + return true; +} + +bool _tryGetRequestedVersion(OpenShock::SemVer& version) { + if (xSemaphoreTake(_requestedVersionMutex, pdMS_TO_TICKS(1000)) != pdTRUE) { + OS_LOGE(TAG, "Failed to take requested version mutex"); + return false; + } + + version = _requestedVersion; + + xSemaphoreGive(_requestedVersionMutex); + + return true; +} + +bool _sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask task, float progress) { + int32_t updateId; + if (!Config::GetOtaUpdateId(updateId)) { + OS_LOGE(TAG, "Failed to get OTA update ID"); + return false; + } + + if (!Serialization::Gateway::SerializeOtaInstallProgressMessage(updateId, task, progress, GatewayConnectionManager::SendMessageBIN)) { + OS_LOGE(TAG, "Failed to send OTA install progress message"); + return false; + } + + return true; +} +bool _sendFailureMessage(std::string_view message, bool fatal = false) { + int32_t updateId; + if (!Config::GetOtaUpdateId(updateId)) { + OS_LOGE(TAG, "Failed to get OTA update ID"); + return false; + } + + if (!Serialization::Gateway::SerializeOtaInstallFailedMessage(updateId, message, fatal, GatewayConnectionManager::SendMessageBIN)) { + OS_LOGE(TAG, "Failed to send OTA install failed message"); + return false; + } + + return true; +} + +bool _flashAppPartition(const esp_partition_t* partition, std::string_view remoteUrl, const uint8_t (&remoteHash)[32]) { + OS_LOGD(TAG, "Flashing app partition"); + + if (!_sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask::FlashingApplication, 0.0f)) { + return false; + } + + auto onProgress = [](std::size_t current, std::size_t total, float progress) -> bool { + OS_LOGD(TAG, "Flashing app partition: %u / %u (%.2f%%)", current, total, progress * 100.0f); + + _sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask::FlashingApplication, progress); + + return true; + }; + + if (!OpenShock::FlashPartitionFromUrl(partition, remoteUrl, remoteHash, onProgress)) { + OS_LOGE(TAG, "Failed to flash app partition"); + _sendFailureMessage("Failed to flash app partition"sv); + return false; + } + + if (!_sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask::MarkingApplicationBootable, 0.0f)) { + return false; + } + + // Set app partition bootable. + if (esp_ota_set_boot_partition(partition) != ESP_OK) { + OS_LOGE(TAG, "Failed to set app partition bootable"); + _sendFailureMessage("Failed to set app partition bootable"sv); + return false; + } + + return true; +} + +bool _flashFilesystemPartition(const esp_partition_t* parition, std::string_view remoteUrl, const uint8_t (&remoteHash)[32]) { + if (!_sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask::PreparingForInstall, 0.0f)) { + return false; + } + + // Make sure captive portal is stopped, timeout after 5 seconds. + if (!CaptivePortal::ForceClose(5000U)) { + OS_LOGE(TAG, "Failed to force close captive portal (timed out)"); + _sendFailureMessage("Failed to force close captive portal (timed out)"sv); + return false; + } + + OS_LOGD(TAG, "Flashing filesystem partition"); + + if (!_sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask::FlashingFilesystem, 0.0f)) { + return false; + } + + auto onProgress = [](std::size_t current, std::size_t total, float progress) -> bool { + OS_LOGD(TAG, "Flashing filesystem partition: %u / %u (%.2f%%)", current, total, progress * 100.0f); + + _sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask::FlashingFilesystem, progress); + + return true; + }; + + if (!OpenShock::FlashPartitionFromUrl(parition, remoteUrl, remoteHash, onProgress)) { + OS_LOGE(TAG, "Failed to flash filesystem partition"); + _sendFailureMessage("Failed to flash filesystem partition"sv); + return false; + } + + if (!_sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask::VerifyingFilesystem, 0.0f)) { + return false; + } + + // Attempt to mount filesystem. + fs::LittleFSFS test; + if (!test.begin(false, "/static", 10, "static0")) { + OS_LOGE(TAG, "Failed to mount filesystem"); + _sendFailureMessage("Failed to mount filesystem"sv); + return false; + } + test.end(); + + OpenShock::CaptivePortal::ForceClose(false); + + return true; +} + +OtaUpdateClient::OtaUpdateClient(const OpenShock::SemVer& version) + : m_version(version) + , m_taskHandle(nullptr) +{ +} + +OtaUpdateClient::~OtaUpdateClient() +{ + if (m_taskHandle != nullptr) { + vTaskDelete(m_taskHandle); + } +} + +bool OtaUpdateClient::Start() +{ + if (m_taskHandle != nullptr) { + OS_LOGE(TAG, "Task already started"); + return false; + } + + if (TaskUtils::TaskCreateExpensive(&Util::FnProxy<&OtaUpdateClient::_task>, TAG, 8192, this, 1, &m_taskHandle) != pdPASS) { + OS_LOGE(TAG, "Failed to create OTA update task"); + return false; + } + + return true; +} + +void OtaUpdateClient::_task() +{ + // Generate random int32_t for this update. + int32_t updateId = static_cast(esp_random()); + if (!Config::SetOtaUpdateId(updateId)) { + OS_LOGE(TAG, "Failed to set OTA update ID"); + continue; + } + if (!Config::SetOtaUpdateStep(OpenShock::OtaUpdateStep::Updating)) { + OS_LOGE(TAG, "Failed to set OTA update step"); + continue; + } + + if (!Serialization::Gateway::SerializeOtaInstallStartedMessage(updateId, version, GatewayConnectionManager::SendMessageBIN)) { + OS_LOGE(TAG, "Failed to serialize OTA install started message"); + continue; + } + + if (!_sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask::FetchingMetadata, 0.0f)) { + continue; + } + + // Fetch current release. + OtaUpdateManager::FirmwareRelease release; + if (!OtaUpdateManager::TryGetFirmwareRelease(version, release)) { + OS_LOGE(TAG, "Failed to fetch firmware release"); // TODO: Send error message to server + _sendFailureMessage("Failed to fetch firmware release"sv); + continue; + } + + // Print release. + OS_LOGD(TAG, "Firmware release:"); + OS_LOGD(TAG, " Version: %s", version.toString().c_str()); // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this + OS_LOGD(TAG, " App binary URL: %s", release.appBinaryUrl.c_str()); + OS_LOGD(TAG, " App binary hash: %s", HexUtils::ToHex<32>(release.appBinaryHash).data()); + OS_LOGD(TAG, " Filesystem binary URL: %s", release.filesystemBinaryUrl.c_str()); + OS_LOGD(TAG, " Filesystem binary hash: %s", HexUtils::ToHex<32>(release.filesystemBinaryHash).data()); + + // Get available app update partition. + const esp_partition_t* appPartition = esp_ota_get_next_update_partition(nullptr); + if (appPartition == nullptr) { + OS_LOGE(TAG, "Failed to get app update partition"); // TODO: Send error message to server + _sendFailureMessage("Failed to get app update partition"sv); + continue; + } + + // Get filesystem partition. + const esp_partition_t* filesystemPartition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, "static0"); + if (filesystemPartition == nullptr) { + OS_LOGE(TAG, "Failed to find filesystem partition"); // TODO: Send error message to server + _sendFailureMessage("Failed to find filesystem partition"sv); + continue; + } + + // Increase task watchdog timeout. + // Prevents panics on some ESP32s when clearing large partitions. + esp_task_wdt_init(15, true); + + // Flash app and filesystem partitions. + if (!_flashFilesystemPartition(filesystemPartition, release.filesystemBinaryUrl, release.filesystemBinaryHash)) continue; + if (!_flashAppPartition(appPartition, release.appBinaryUrl, release.appBinaryHash)) continue; + + // Set OTA boot type in config. + if (!Config::SetOtaUpdateStep(OpenShock::OtaUpdateStep::Updated)) { + OS_LOGE(TAG, "Failed to set OTA update step"); + _sendFailureMessage("Failed to set OTA update step"sv); + continue; + } + + // Set task watchdog timeout back to default. + esp_task_wdt_init(5, true); + + // Send reboot message. + _sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask::Rebooting, 0.0f); + + // Reboot into new firmware. + OS_LOGI(TAG, "Restarting into new firmware..."); + vTaskDelay(pdMS_TO_TICKS(200)); + break; + + // Restart. + esp_restart(); +} diff --git a/src/ota/OtaUpdateManager.cpp b/src/ota/OtaUpdateManager.cpp new file mode 100644 index 00000000..e8158d09 --- /dev/null +++ b/src/ota/OtaUpdateManager.cpp @@ -0,0 +1,326 @@ +#include + +#include "ota/OtaUpdateManager.h" + +const char* const TAG = "OtaUpdateManager"; + +#include "Common.h" +#include "config/Config.h" +#include "http/FirmwareCDN.h" +#include "ota/OtaUpdateClient.h" +#include "ota/OtaUpdateStep.h" +#include "util/StringUtils.h" +#include "util/TaskUtils.h" +#include "Logging.h" +#include "SemVer.h" + +#include // TODO: Get rid of Arduino entirely. >:( + +#include +#include + +#include +#include +#include + +using namespace std::string_view_literals; + +/// @brief Stops initArduino() from handling OTA rollbacks +/// @todo Get rid of Arduino entirely. >:( +/// +/// @see .platformio/packages/framework-arduinoespressif32/cores/esp32/esp32-hal-misc.c +/// @return true +bool verifyRollbackLater() { + return true; +} + +using namespace OpenShock; + +enum OtaTaskEventFlag : uint32_t { + OTA_TASK_EVENT_UPDATE_REQUESTED = 1 << 0, + OTA_TASK_EVENT_WIFI_DISCONNECTED = 1 << 1, // If both connected and disconnected are set, disconnected takes priority. + OTA_TASK_EVENT_WIFI_CONNECTED = 1 << 2, +}; + +static esp_ota_img_states_t _otaImageState; +static OpenShock::FirmwareBootType _bootType; +static TaskHandle_t _watcherTaskHandle = nullptr; +static SemaphoreHandle_t _updateClientMutex = xSemaphoreCreateMutex(); +static std::unique_ptr _updateClient = nullptr; + +bool _tryStartUpdate(const OpenShock::SemVer& version) { + if (xSemaphoreTake(_updateClientMutex, pdMS_TO_TICKS(1000)) != pdTRUE) { + OS_LOGE(TAG, "Failed to take requested version mutex"); + return false; + } + + if (_updateClient != nullptr) { + xSemaphoreGive(_updateClientMutex); + OS_LOGE(TAG, "Update client already started"); + return false; + } + + _updateClient = std::make_unique(version); + + if (!_updateClient->Start()) { + _updateClient.reset(); + xSemaphoreGive(_updateClientMutex); + OS_LOGE(TAG, "Failed to start update client"); + return false; + } + + xSemaphoreGive(_updateClientMutex); + + OS_LOGD(TAG, "Update client started"); + + return true; +} + +void _otaEvGotIPHandler(arduino_event_t*) { + xTaskNotify(_watcherTaskHandle, OTA_TASK_EVENT_WIFI_CONNECTED, eSetBits); +} +void _otaEvWiFiDisconnectedHandler(arduino_event_t*) { + xTaskNotify(_watcherTaskHandle, OTA_TASK_EVENT_WIFI_DISCONNECTED, eSetBits); +} + +void _otaWatcherTask(void*) { + + OS_LOGD(TAG, "OTA update task started"); + + bool connected = false; + bool updateRequested = false; + int64_t lastUpdateCheck = 0; + + // Update task loop. + while (true) { + // Wait for event. + uint32_t eventBits = 0; + xTaskNotifyWait(0, UINT32_MAX, &eventBits, pdMS_TO_TICKS(5000)); // TODO: wait for rest time + + updateRequested |= (eventBits & OTA_TASK_EVENT_UPDATE_REQUESTED) != 0; + + if ((eventBits & OTA_TASK_EVENT_WIFI_DISCONNECTED) != 0) { + OS_LOGD(TAG, "WiFi disconnected"); + connected = false; + continue; // No further processing needed. + } + + if ((eventBits & OTA_TASK_EVENT_WIFI_CONNECTED) != 0 && !connected) { + OS_LOGD(TAG, "WiFi connected"); + connected = true; + } + + // If we're not connected, continue. + if (!connected) { + continue; + } + + int64_t now = OpenShock::millis(); + + Config::OtaUpdateConfig config; + if (!Config::GetOtaUpdateConfig(config)) { + OS_LOGE(TAG, "Failed to get OTA update config"); + continue; + } + + if (!config.isEnabled) { + OS_LOGD(TAG, "OTA updates are disabled, skipping update check"); + continue; + } + + bool firstCheck = lastUpdateCheck == 0; + int64_t diff = now - lastUpdateCheck; + int64_t diffMins = diff / 60'000LL; + + bool check = false; + check |= config.checkOnStartup && firstCheck; // On startup + check |= config.checkPeriodically && diffMins >= config.checkInterval; // Periodically + check |= updateRequested && (firstCheck || diffMins >= 1); // Update requested + + if (!check) { + continue; + } + + lastUpdateCheck = now; + + if (config.requireManualApproval) { + OS_LOGD(TAG, "Manual approval required, skipping update check"); + // TODO: IMPLEMENT + continue; + } + + OpenShock::SemVer version; + if (updateRequested) { + updateRequested = false; + + if (!_tryGetRequestedVersion(version)) { + OS_LOGE(TAG, "Failed to get requested version"); + continue; + } + + OS_LOGD(TAG, "Update requested for version %s", version.toString().c_str()); // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this + } else { + OS_LOGD(TAG, "Checking for updates"); + + // Fetch current version. + if (!OtaUpdateManager::TryGetFirmwareVersion(config.updateChannel, version)) { + OS_LOGE(TAG, "Failed to fetch firmware version"); + continue; + } + + OS_LOGD(TAG, "Remote version: %s", version.toString().c_str()); // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this + } + + if (version.toString() == OPENSHOCK_FW_VERSION) { // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this + OS_LOGI(TAG, "Requested version is already installed"); + continue; + } + + + } +} + +bool OtaUpdateManager::Init() { + OS_LOGN(TAG, "Fetching current partition"); + + // Fetch current partition info. + const esp_partition_t* partition = esp_ota_get_running_partition(); + if (partition == nullptr) { + OS_PANIC(TAG, "Failed to get currently running partition"); + return false; // This will never be reached, but the compiler doesn't know that. + } + + OS_LOGD(TAG, "Fetching partition state"); + + // Get OTA state for said partition. + esp_err_t err = esp_ota_get_state_partition(partition, &_otaImageState); + if (err != ESP_OK) { + OS_PANIC(TAG, "Failed to get partition state: %s", esp_err_to_name(err)); + return false; // This will never be reached, but the compiler doesn't know that. + } + + OS_LOGD(TAG, "Fetching previous update step"); + OtaUpdateStep updateStep; + if (!Config::GetOtaUpdateStep(updateStep)) { + OS_LOGE(TAG, "Failed to get OTA update step"); + return false; + } + + // Infer boot type from update step. + switch (updateStep) { + case OtaUpdateStep::Updated: + _bootType = FirmwareBootType::NewFirmware; + break; + case OtaUpdateStep::Validating: // If the update step is validating, we have failed in the middle of validating the new firmware, meaning this is a rollback. + case OtaUpdateStep::RollingBack: + _bootType = FirmwareBootType::Rollback; + break; + default: + _bootType = FirmwareBootType::Normal; + break; + } + + if (updateStep == OtaUpdateStep::Updated) { + if (!Config::SetOtaUpdateStep(OtaUpdateStep::Validating)) { + OS_PANIC(TAG, "Failed to set OTA update step in critical section"); // TODO: THIS IS A CRITICAL SECTION, WHAT DO WE DO? + } + } + + WiFi.onEvent(_otaEvGotIPHandler, ARDUINO_EVENT_WIFI_STA_GOT_IP); + WiFi.onEvent(_otaEvGotIPHandler, ARDUINO_EVENT_WIFI_STA_GOT_IP6); + WiFi.onEvent(_otaEvWiFiDisconnectedHandler, ARDUINO_EVENT_WIFI_STA_DISCONNECTED); + + if (TaskUtils::TaskCreateExpensive(_otaWatcherTask, "OtaWatcherTask", 8192, nullptr, 1, &_watcherTaskHandle) != pdPASS) { + OS_LOGE(TAG, "Failed to create OTA watcher task"); + return false; + } + + return true; +} + +bool OtaUpdateManager::TryGetFirmwareRelease(const OpenShock::SemVer& version, FirmwareReleaseInfo& release) { + auto versionStr = version.toString(); // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this + + if (!FormatToString(release.appBinaryUrl, OPENSHOCK_FW_CDN_APP_URL_FORMAT, versionStr.c_str())) { + OS_LOGE(TAG, "Failed to format URL"); + return false; + } + + if (!FormatToString(release.filesystemBinaryUrl, OPENSHOCK_FW_CDN_FILESYSTEM_URL_FORMAT, versionStr.c_str())) { + OS_LOGE(TAG, "Failed to format URL"); + return false; + } + + // Fetch hashes. + auto response = HTTP::FirmwareCDN::GetFirmwareBinaryHashes(version); + if (response.result != HTTP::RequestResult::Success) { + OS_LOGE(TAG, "Failed to fetch hashes: [%u]", response.code); + return false; + } + + for (auto binaryHash : response.data) { + if (binaryHash.name == "app.bin") { + static_assert(sizeof(release.appBinaryHash) == sizeof(binaryHash.hash), "Hash size mismatch"); + memcpy(release.appBinaryHash, binaryHash.hash, sizeof(release.appBinaryHash)); + } else if (binaryHash.name == "staticfs.bin") { + static_assert(sizeof(release.filesystemBinaryHash) == sizeof(binaryHash.hash), "Hash size mismatch"); + memcpy(release.filesystemBinaryHash, binaryHash.hash, sizeof(release.filesystemBinaryHash)); + } + } + + return true; +} + +bool OtaUpdateManager::TryStartFirmwareInstallation(const OpenShock::SemVer& version) { + OS_LOGD(TAG, "Requesting firmware version %s", version.toString().c_str()); // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this + + return _tryStartUpdate(version); +} + +FirmwareBootType OtaUpdateManager::GetFirmwareBootType() { + return _bootType; +} + +bool OtaUpdateManager::IsValidatingApp() { + return _otaImageState == ESP_OTA_IMG_PENDING_VERIFY; +} + +void OtaUpdateManager::InvalidateAndRollback() { + // Set OTA boot type in config. + if (!Config::SetOtaUpdateStep(OpenShock::OtaUpdateStep::RollingBack)) { + OS_PANIC(TAG, "Failed to set OTA firmware boot type in critical section"); // TODO: THIS IS A CRITICAL SECTION, WHAT DO WE DO? + return; + } + + switch (esp_ota_mark_app_invalid_rollback_and_reboot()) { + case ESP_FAIL: + OS_LOGE(TAG, "Rollback failed (ESP_FAIL)"); + break; + case ESP_ERR_OTA_ROLLBACK_FAILED: + OS_LOGE(TAG, "Rollback failed (ESP_ERR_OTA_ROLLBACK_FAILED)"); + break; + default: + OS_LOGE(TAG, "Rollback failed (Unknown)"); + break; + } + + // Set OTA boot type in config. + if (!Config::SetOtaUpdateStep(OpenShock::OtaUpdateStep::None)) { + OS_LOGE(TAG, "Failed to set OTA firmware boot type"); + } + + esp_restart(); +} + +void OtaUpdateManager::ValidateApp() { + if (esp_ota_mark_app_valid_cancel_rollback() != ESP_OK) { + OS_PANIC(TAG, "Unable to mark app as valid, WTF?"); // TODO: Wtf do we do here? + } + + // Set OTA boot type in config. + if (!Config::SetOtaUpdateStep(OpenShock::OtaUpdateStep::Validated)) { + OS_PANIC(TAG, "Failed to set OTA firmware boot type in critical section"); // TODO: THIS IS A CRITICAL SECTION, WHAT DO WE DO? + } + + _otaImageState = ESP_OTA_IMG_VALID; +} From f6e2c2cfb4cc7252a6f386294ca28c1e7bda088a Mon Sep 17 00:00:00 2001 From: hhvrc Date: Fri, 11 Oct 2024 00:34:06 +0200 Subject: [PATCH 2/9] Formatting --- include/Common.h | 27 +++++++-------- include/config/OtaUpdateConfig.h | 11 +------ include/ota/FirmwareBinaryHash.h | 8 ++--- include/ota/FirmwareReleaseInfo.h | 5 ++- include/ota/OtaUpdateChannel.h | 5 +-- include/ota/OtaUpdateClient.h | 3 +- include/ota/OtaUpdateStep.h | 3 +- src/ota/OtaUpdateClient.cpp | 18 ++++++---- src/ota/OtaUpdateManager.cpp | 55 ++++++++++++++++++------------- 9 files changed, 71 insertions(+), 64 deletions(-) diff --git a/include/Common.h b/include/Common.h index ca754ebb..35423fa1 100644 --- a/include/Common.h +++ b/include/Common.h @@ -3,11 +3,11 @@ #include #include -#define DISABLE_COPY(TypeName) \ - TypeName(const TypeName&) = delete; \ +#define DISABLE_COPY(TypeName) \ + TypeName(const TypeName&) = delete; \ void operator=(const TypeName&) = delete -#define DISABLE_MOVE(TypeName) \ - TypeName(TypeName&&) = delete; \ +#define DISABLE_MOVE(TypeName) \ + TypeName(TypeName&&) = delete; \ void operator=(TypeName&&) = delete #ifndef OPENSHOCK_API_DOMAIN @@ -20,14 +20,14 @@ #error "OPENSHOCK_FW_VERSION must be defined" #endif -#define OPENSHOCK_FW_CDN_URL(path) "https://" OPENSHOCK_FW_CDN_DOMAIN path -#define OPENSHOCK_FW_CDN_CHANNEL_URL(ch) OPENSHOCK_FW_CDN_URL("/version-" ch ".txt") -#define OPENSHOCK_FW_CDN_STABLE_URL OPENSHOCK_FW_CDN_CHANNEL_URL("stable") -#define OPENSHOCK_FW_CDN_BETA_URL OPENSHOCK_FW_CDN_CHANNEL_URL("beta") -#define OPENSHOCK_FW_CDN_DEVELOP_URL OPENSHOCK_FW_CDN_CHANNEL_URL("develop") -#define OPENSHOCK_FW_CDN_BOARDS_BASE_URL_FORMAT OPENSHOCK_FW_CDN_URL("/%s") -#define OPENSHOCK_FW_CDN_BOARDS_INDEX_URL_FORMAT OPENSHOCK_FW_CDN_BOARDS_BASE_URL_FORMAT "/boards.txt" -#define OPENSHOCK_FW_CDN_VERSION_BASE_URL_FORMAT OPENSHOCK_FW_CDN_BOARDS_BASE_URL_FORMAT "/" OPENSHOCK_FW_BOARD +#define OPENSHOCK_FW_CDN_URL(path) "https://" OPENSHOCK_FW_CDN_DOMAIN path +#define OPENSHOCK_FW_CDN_CHANNEL_URL(ch) OPENSHOCK_FW_CDN_URL("/version-" ch ".txt") +#define OPENSHOCK_FW_CDN_STABLE_URL OPENSHOCK_FW_CDN_CHANNEL_URL("stable") +#define OPENSHOCK_FW_CDN_BETA_URL OPENSHOCK_FW_CDN_CHANNEL_URL("beta") +#define OPENSHOCK_FW_CDN_DEVELOP_URL OPENSHOCK_FW_CDN_CHANNEL_URL("develop") +#define OPENSHOCK_FW_CDN_BOARDS_BASE_URL_FORMAT OPENSHOCK_FW_CDN_URL("/%s") +#define OPENSHOCK_FW_CDN_BOARDS_INDEX_URL_FORMAT OPENSHOCK_FW_CDN_BOARDS_BASE_URL_FORMAT "/boards.txt" +#define OPENSHOCK_FW_CDN_VERSION_BASE_URL_FORMAT OPENSHOCK_FW_CDN_BOARDS_BASE_URL_FORMAT "/" OPENSHOCK_FW_BOARD #define OPENSHOCK_FW_CDN_APP_URL_FORMAT OPENSHOCK_FW_CDN_VERSION_BASE_URL_FORMAT "/app.bin" #define OPENSHOCK_FW_CDN_FILESYSTEM_URL_FORMAT OPENSHOCK_FW_CDN_VERSION_BASE_URL_FORMAT "/staticfs.bin" #define OPENSHOCK_FW_CDN_SHA256_HASHES_URL_FORMAT OPENSHOCK_FW_CDN_VERSION_BASE_URL_FORMAT "/hashes.sha256.txt" @@ -50,7 +50,8 @@ // Check if Arduino.h exists, if not instruct the developer to remove "arduino-esp32" from the useragent and replace it with "ESP-IDF", after which the developer may remove this warning. #if defined(__has_include) && !__has_include("Arduino.h") -#warning "Let it be known that Arduino hath finally been cast aside in favor of the noble ESP-IDF! I beseech thee, kind sir or madam, wouldst thou kindly partake in the honors of expunging 'arduino-esp32' from yonder useragent aloft, and in its stead, bestow the illustrious 'ESP-IDF'?" +#warning \ + "Let it be known that Arduino hath finally been cast aside in favor of the noble ESP-IDF! I beseech thee, kind sir or madam, wouldst thou kindly partake in the honors of expunging 'arduino-esp32' from yonder useragent aloft, and in its stead, bestow the illustrious 'ESP-IDF'?" #endif #if __cplusplus >= 202'302L diff --git a/include/config/OtaUpdateConfig.h b/include/config/OtaUpdateConfig.h index 6323586c..1f67bcb1 100644 --- a/include/config/OtaUpdateConfig.h +++ b/include/config/OtaUpdateConfig.h @@ -11,16 +11,7 @@ namespace OpenShock::Config { struct OtaUpdateConfig : public ConfigBase { OtaUpdateConfig(); OtaUpdateConfig( - bool isEnabled, - std::string cdnDomain, - OtaUpdateChannel updateChannel, - bool checkOnStartup, - bool checkPeriodically, - uint16_t checkInterval, - bool allowBackendManagement, - bool requireManualApproval, - int32_t updateId, - OtaUpdateStep updateStep + bool isEnabled, std::string cdnDomain, OtaUpdateChannel updateChannel, bool checkOnStartup, bool checkPeriodically, uint16_t checkInterval, bool allowBackendManagement, bool requireManualApproval, int32_t updateId, OtaUpdateStep updateStep ); bool isEnabled; diff --git a/include/ota/FirmwareBinaryHash.h b/include/ota/FirmwareBinaryHash.h index 72442c52..1b4b642f 100644 --- a/include/ota/FirmwareBinaryHash.h +++ b/include/ota/FirmwareBinaryHash.h @@ -3,11 +3,9 @@ #include #include -namespace OpenShock -{ - struct FirmwareBinaryHash - { +namespace OpenShock { + struct FirmwareBinaryHash { std::string name; uint8_t hash[32]; }; -} // namespace OpenShock +} // namespace OpenShock diff --git a/include/ota/FirmwareReleaseInfo.h b/include/ota/FirmwareReleaseInfo.h index 63a59e7e..febd8039 100644 --- a/include/ota/FirmwareReleaseInfo.h +++ b/include/ota/FirmwareReleaseInfo.h @@ -3,12 +3,11 @@ #include #include -namespace OpenShock -{ +namespace OpenShock { struct FirmwareReleaseInfo { std::string appBinaryUrl; uint8_t appBinaryHash[32]; std::string filesystemBinaryUrl; uint8_t filesystemBinaryHash[32]; }; -} // namespace OpenShock +} // namespace OpenShock diff --git a/include/ota/OtaUpdateChannel.h b/include/ota/OtaUpdateChannel.h index e0bbf2fa..3ab942b3 100644 --- a/include/ota/OtaUpdateChannel.h +++ b/include/ota/OtaUpdateChannel.h @@ -2,13 +2,14 @@ #include "serialization/_fbs/HubConfig_generated.h" -#include #include +#include namespace OpenShock { typedef OpenShock::Serialization::Configuration::OtaUpdateChannel OtaUpdateChannel; - inline bool TryParseOtaUpdateChannel(OtaUpdateChannel& channel, const char* str) { + inline bool TryParseOtaUpdateChannel(OtaUpdateChannel& channel, const char* str) + { if (strcasecmp(str, "stable") == 0) { channel = OtaUpdateChannel::Stable; return true; diff --git a/include/ota/OtaUpdateClient.h b/include/ota/OtaUpdateClient.h index 37505ebb..ed985b40 100644 --- a/include/ota/OtaUpdateClient.h +++ b/include/ota/OtaUpdateClient.h @@ -11,10 +11,11 @@ namespace OpenShock { ~OtaUpdateClient(); bool Start(); + private: void _task(); OpenShock::SemVer m_version; TaskHandle_t m_taskHandle; }; -} +} // namespace OpenShock diff --git a/include/ota/OtaUpdateStep.h b/include/ota/OtaUpdateStep.h index 28a9dae7..8fae996f 100644 --- a/include/ota/OtaUpdateStep.h +++ b/include/ota/OtaUpdateStep.h @@ -8,7 +8,8 @@ namespace OpenShock { typedef OpenShock::Serialization::Configuration::OtaUpdateStep OtaUpdateStep; - inline bool TryParseOtaUpdateStep(OtaUpdateStep& channel, const char* str) { + inline bool TryParseOtaUpdateStep(OtaUpdateStep& channel, const char* str) + { if (strcasecmp(str, "none") == 0) { channel = OtaUpdateStep::None; return true; diff --git a/src/ota/OtaUpdateClient.cpp b/src/ota/OtaUpdateClient.cpp index b84db1d2..391ef36d 100644 --- a/src/ota/OtaUpdateClient.cpp +++ b/src/ota/OtaUpdateClient.cpp @@ -20,7 +20,8 @@ const char* const TAG = "OtaUpdateClient"; using namespace OpenShock; -bool _tryStartUpdate(const OpenShock::SemVer& version) { +bool _tryStartUpdate(const OpenShock::SemVer& version) +{ if (xSemaphoreTake(_requestedVersionMutex, pdMS_TO_TICKS(1000)) != pdTRUE) { OS_LOGE(TAG, "Failed to take requested version mutex"); return false; @@ -35,7 +36,8 @@ bool _tryStartUpdate(const OpenShock::SemVer& version) { return true; } -bool _tryGetRequestedVersion(OpenShock::SemVer& version) { +bool _tryGetRequestedVersion(OpenShock::SemVer& version) +{ if (xSemaphoreTake(_requestedVersionMutex, pdMS_TO_TICKS(1000)) != pdTRUE) { OS_LOGE(TAG, "Failed to take requested version mutex"); return false; @@ -48,7 +50,8 @@ bool _tryGetRequestedVersion(OpenShock::SemVer& version) { return true; } -bool _sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask task, float progress) { +bool _sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask task, float progress) +{ int32_t updateId; if (!Config::GetOtaUpdateId(updateId)) { OS_LOGE(TAG, "Failed to get OTA update ID"); @@ -62,7 +65,8 @@ bool _sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask task, f return true; } -bool _sendFailureMessage(std::string_view message, bool fatal = false) { +bool _sendFailureMessage(std::string_view message, bool fatal = false) +{ int32_t updateId; if (!Config::GetOtaUpdateId(updateId)) { OS_LOGE(TAG, "Failed to get OTA update ID"); @@ -77,7 +81,8 @@ bool _sendFailureMessage(std::string_view message, bool fatal = false) { return true; } -bool _flashAppPartition(const esp_partition_t* partition, std::string_view remoteUrl, const uint8_t (&remoteHash)[32]) { +bool _flashAppPartition(const esp_partition_t* partition, std::string_view remoteUrl, const uint8_t (&remoteHash)[32]) +{ OS_LOGD(TAG, "Flashing app partition"); if (!_sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask::FlashingApplication, 0.0f)) { @@ -112,7 +117,8 @@ bool _flashAppPartition(const esp_partition_t* partition, std::string_view remot return true; } -bool _flashFilesystemPartition(const esp_partition_t* parition, std::string_view remoteUrl, const uint8_t (&remoteHash)[32]) { +bool _flashFilesystemPartition(const esp_partition_t* parition, std::string_view remoteUrl, const uint8_t (&remoteHash)[32]) +{ if (!_sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask::PreparingForInstall, 0.0f)) { return false; } diff --git a/src/ota/OtaUpdateManager.cpp b/src/ota/OtaUpdateManager.cpp index e8158d09..f2463a8d 100644 --- a/src/ota/OtaUpdateManager.cpp +++ b/src/ota/OtaUpdateManager.cpp @@ -7,14 +7,14 @@ const char* const TAG = "OtaUpdateManager"; #include "Common.h" #include "config/Config.h" #include "http/FirmwareCDN.h" +#include "Logging.h" #include "ota/OtaUpdateClient.h" #include "ota/OtaUpdateStep.h" +#include "SemVer.h" #include "util/StringUtils.h" #include "util/TaskUtils.h" -#include "Logging.h" -#include "SemVer.h" -#include // TODO: Get rid of Arduino entirely. >:( +#include // TODO: Get rid of Arduino entirely. >:( #include #include @@ -30,7 +30,8 @@ using namespace std::string_view_literals; /// /// @see .platformio/packages/framework-arduinoespressif32/cores/esp32/esp32-hal-misc.c /// @return true -bool verifyRollbackLater() { +bool verifyRollbackLater() +{ return true; } @@ -44,11 +45,12 @@ enum OtaTaskEventFlag : uint32_t { static esp_ota_img_states_t _otaImageState; static OpenShock::FirmwareBootType _bootType; -static TaskHandle_t _watcherTaskHandle = nullptr; -static SemaphoreHandle_t _updateClientMutex = xSemaphoreCreateMutex(); +static TaskHandle_t _watcherTaskHandle = nullptr; +static SemaphoreHandle_t _updateClientMutex = xSemaphoreCreateMutex(); static std::unique_ptr _updateClient = nullptr; -bool _tryStartUpdate(const OpenShock::SemVer& version) { +bool _tryStartUpdate(const OpenShock::SemVer& version) +{ if (xSemaphoreTake(_updateClientMutex, pdMS_TO_TICKS(1000)) != pdTRUE) { OS_LOGE(TAG, "Failed to take requested version mutex"); return false; @@ -76,19 +78,21 @@ bool _tryStartUpdate(const OpenShock::SemVer& version) { return true; } -void _otaEvGotIPHandler(arduino_event_t*) { +void _otaEvGotIPHandler(arduino_event_t*) +{ xTaskNotify(_watcherTaskHandle, OTA_TASK_EVENT_WIFI_CONNECTED, eSetBits); } -void _otaEvWiFiDisconnectedHandler(arduino_event_t*) { +void _otaEvWiFiDisconnectedHandler(arduino_event_t*) +{ xTaskNotify(_watcherTaskHandle, OTA_TASK_EVENT_WIFI_DISCONNECTED, eSetBits); } -void _otaWatcherTask(void*) { - +void _otaWatcherTask(void*) +{ OS_LOGD(TAG, "OTA update task started"); - bool connected = false; - bool updateRequested = false; + bool connected = false; + bool updateRequested = false; int64_t lastUpdateCheck = 0; // Update task loop. @@ -128,7 +132,7 @@ void _otaWatcherTask(void*) { continue; } - bool firstCheck = lastUpdateCheck == 0; + bool firstCheck = lastUpdateCheck == 0; int64_t diff = now - lastUpdateCheck; int64_t diffMins = diff / 60'000LL; @@ -175,12 +179,11 @@ void _otaWatcherTask(void*) { OS_LOGI(TAG, "Requested version is already installed"); continue; } - - } } -bool OtaUpdateManager::Init() { +bool OtaUpdateManager::Init() +{ OS_LOGN(TAG, "Fetching current partition"); // Fetch current partition info. @@ -238,7 +241,8 @@ bool OtaUpdateManager::Init() { return true; } -bool OtaUpdateManager::TryGetFirmwareRelease(const OpenShock::SemVer& version, FirmwareReleaseInfo& release) { +bool OtaUpdateManager::TryGetFirmwareRelease(const OpenShock::SemVer& version, FirmwareReleaseInfo& release) +{ auto versionStr = version.toString(); // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this if (!FormatToString(release.appBinaryUrl, OPENSHOCK_FW_CDN_APP_URL_FORMAT, versionStr.c_str())) { @@ -271,21 +275,25 @@ bool OtaUpdateManager::TryGetFirmwareRelease(const OpenShock::SemVer& version, F return true; } -bool OtaUpdateManager::TryStartFirmwareInstallation(const OpenShock::SemVer& version) { +bool OtaUpdateManager::TryStartFirmwareInstallation(const OpenShock::SemVer& version) +{ OS_LOGD(TAG, "Requesting firmware version %s", version.toString().c_str()); // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this return _tryStartUpdate(version); } -FirmwareBootType OtaUpdateManager::GetFirmwareBootType() { +FirmwareBootType OtaUpdateManager::GetFirmwareBootType() +{ return _bootType; } -bool OtaUpdateManager::IsValidatingApp() { +bool OtaUpdateManager::IsValidatingApp() +{ return _otaImageState == ESP_OTA_IMG_PENDING_VERIFY; } -void OtaUpdateManager::InvalidateAndRollback() { +void OtaUpdateManager::InvalidateAndRollback() +{ // Set OTA boot type in config. if (!Config::SetOtaUpdateStep(OpenShock::OtaUpdateStep::RollingBack)) { OS_PANIC(TAG, "Failed to set OTA firmware boot type in critical section"); // TODO: THIS IS A CRITICAL SECTION, WHAT DO WE DO? @@ -312,7 +320,8 @@ void OtaUpdateManager::InvalidateAndRollback() { esp_restart(); } -void OtaUpdateManager::ValidateApp() { +void OtaUpdateManager::ValidateApp() +{ if (esp_ota_mark_app_valid_cancel_rollback() != ESP_OK) { OS_PANIC(TAG, "Unable to mark app as valid, WTF?"); // TODO: Wtf do we do here? } From 2d73dfda443b7f52da17b69e872efda4c8341e1d Mon Sep 17 00:00:00 2001 From: hhvrc Date: Fri, 11 Oct 2024 00:34:20 +0200 Subject: [PATCH 3/9] Oops --- src/ota/OtaUpdateClient.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ota/OtaUpdateClient.cpp b/src/ota/OtaUpdateClient.cpp index 391ef36d..e202bb8f 100644 --- a/src/ota/OtaUpdateClient.cpp +++ b/src/ota/OtaUpdateClient.cpp @@ -6,7 +6,7 @@ #include "serialization/WSGateway.h" #include "util/FnProxy.h" -#include "util/HextUtils.h" +#include "util/HexUtils.h" #include "util/PartitionUtils.h" #include "util/TaskUtils.h" From 0311393e3db08ab16e59b296d0833838981d3c7a Mon Sep 17 00:00:00 2001 From: hhvrc Date: Fri, 11 Oct 2024 14:12:19 +0200 Subject: [PATCH 4/9] Fix http call --- src/ota/OtaUpdateManager.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ota/OtaUpdateManager.cpp b/src/ota/OtaUpdateManager.cpp index f2463a8d..6d72b210 100644 --- a/src/ota/OtaUpdateManager.cpp +++ b/src/ota/OtaUpdateManager.cpp @@ -167,7 +167,8 @@ void _otaWatcherTask(void*) OS_LOGD(TAG, "Checking for updates"); // Fetch current version. - if (!OtaUpdateManager::TryGetFirmwareVersion(config.updateChannel, version)) { + auto result = HTTP::FirmwareCDN::GetFirmwareVersion(config.updateChannel); + if (result.result != HTTP::RequestResult::Success) { OS_LOGE(TAG, "Failed to fetch firmware version"); continue; } From ea0f0c6dd5b84694f2c5e96fecb613d83d5bbb2a Mon Sep 17 00:00:00 2001 From: hhvrc Date: Fri, 11 Oct 2024 15:41:04 +0200 Subject: [PATCH 5/9] More refactoring fixup --- include/http/FirmwareCDN.h | 7 ++++ include/ota/OtaUpdateManager.h | 2 - src/http/FirmwareCDN.cpp | 53 +++++++++++++++++++---- src/ota/OtaUpdateClient.cpp | 23 +++++++--- src/ota/OtaUpdateManager.cpp | 77 ++++++---------------------------- 5 files changed, 82 insertions(+), 80 deletions(-) diff --git a/include/http/FirmwareCDN.h b/include/http/FirmwareCDN.h index 7134a9d3..2634bbe2 100644 --- a/include/http/FirmwareCDN.h +++ b/include/http/FirmwareCDN.h @@ -2,6 +2,7 @@ #include "http/HTTPRequestManager.h" #include "ota/FirmwareBinaryHash.h" +#include "ota/FirmwareReleaseInfo.h" #include "ota/OtaUpdateChannel.h" #include "SemVer.h" @@ -25,4 +26,10 @@ namespace OpenShock::HTTP::FirmwareCDN { /// @param version The firmware version to fetch the binary hashes for. /// @return The binary hashes or an error response. HTTP::Response> GetFirmwareBinaryHashes(const OpenShock::SemVer& version); + + /// @brief Fetches the firmware release information for the given firmware version from the firmware CDN. + /// Valid response codes: 200, 304 + /// @param version The firmware version to fetch the release information for. + /// @return The firmware release information or an error response. + HTTP::Response GetFirmwareReleaseInfo(const OpenShock::SemVer& version); } // namespace OpenShock::HTTP::FirmwareCDN diff --git a/include/ota/OtaUpdateManager.h b/include/ota/OtaUpdateManager.h index 2816817a..f8cda171 100644 --- a/include/ota/OtaUpdateManager.h +++ b/include/ota/OtaUpdateManager.h @@ -13,8 +13,6 @@ namespace OpenShock::OtaUpdateManager { [[nodiscard]] bool Init(); - bool TryGetFirmwareRelease(const OpenShock::SemVer& version, FirmwareReleaseInfo& release); - bool TryStartFirmwareInstallation(const OpenShock::SemVer& version); FirmwareBootType GetFirmwareBootType(); diff --git a/src/http/FirmwareCDN.cpp b/src/http/FirmwareCDN.cpp index 35ed5670..04c0f4bd 100644 --- a/src/http/FirmwareCDN.cpp +++ b/src/http/FirmwareCDN.cpp @@ -4,27 +4,27 @@ #include "Common.h" #include "Logging.h" -#include "util/StringUtils.h" #include "util/HexUtils.h" +#include "util/StringUtils.h" const char* const TAG = "FirmwareCDN"; using namespace std::string_view_literals; - using namespace OpenShock; -HTTP::Response HTTP::FirmwareCDN::GetFirmwareVersion(OtaUpdateChannel channel) { +HTTP::Response HTTP::FirmwareCDN::GetFirmwareVersion(OtaUpdateChannel channel) +{ std::string_view channelIndexUrl; switch (channel) { case OtaUpdateChannel::Stable: - channelIndexUrl = OPENSHOCK_FW_CDN_STABLE_URL""sv; + channelIndexUrl = OPENSHOCK_FW_CDN_STABLE_URL ""sv; break; case OtaUpdateChannel::Beta: - channelIndexUrl = OPENSHOCK_FW_CDN_BETA_URL""sv; + channelIndexUrl = OPENSHOCK_FW_CDN_BETA_URL ""sv; break; case OtaUpdateChannel::Develop: - channelIndexUrl = OPENSHOCK_FW_CDN_DEVELOP_URL""sv; + channelIndexUrl = OPENSHOCK_FW_CDN_DEVELOP_URL ""sv; break; default: OS_LOGE(TAG, "Unknown channel: %u", channel); @@ -55,7 +55,8 @@ HTTP::Response HTTP::FirmwareCDN::GetFirmwareVersion(OtaUpdat return {response.result, response.code, version}; } -HTTP::Response> HTTP::FirmwareCDN::GetFirmwareBoards(const OpenShock::SemVer& version) { +HTTP::Response> HTTP::FirmwareCDN::GetFirmwareBoards(const OpenShock::SemVer& version) +{ std::string channelIndexUrl; if (!FormatToString(channelIndexUrl, OPENSHOCK_FW_CDN_BOARDS_INDEX_URL_FORMAT, version.toString().c_str())) { // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this OS_LOGE(TAG, "Failed to format URL"); @@ -95,7 +96,8 @@ HTTP::Response> HTTP::FirmwareCDN::GetFirmwareBoards(co return {response.result, response.code, std::move(boards)}; } -HTTP::Response> HTTP::FirmwareCDN::GetFirmwareBinaryHashes(const OpenShock::SemVer& version) { +HTTP::Response> HTTP::FirmwareCDN::GetFirmwareBinaryHashes(const OpenShock::SemVer& version) +{ auto versionStr = version.toString(); // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this // Construct hash URLs. @@ -155,3 +157,38 @@ HTTP::Response> HTTP::FirmwareCDN::GetFirmwareBi return {RequestResult::Success, 200, std::move(hashes)}; } + +HTTP::Response HTTP::FirmwareCDN::GetFirmwareReleaseInfo(const OpenShock::SemVer& version) +{ + auto versionStr = version.toString(); // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this + + FirmwareReleaseInfo release; + if (!FormatToString(release.appBinaryUrl, OPENSHOCK_FW_CDN_APP_URL_FORMAT, versionStr.c_str())) { + OS_LOGE(TAG, "Failed to format URL"); + return {RequestResult::InternalError, 0, {}}; + } + + if (!FormatToString(release.filesystemBinaryUrl, OPENSHOCK_FW_CDN_FILESYSTEM_URL_FORMAT, versionStr.c_str())) { + OS_LOGE(TAG, "Failed to format URL"); + return {RequestResult::InternalError, 0, {}}; + } + + // Fetch hashes. + auto response = GetFirmwareBinaryHashes(version); + if (response.result != HTTP::RequestResult::Success) { + OS_LOGE(TAG, "Failed to fetch hashes: [%u]", response.code); + return {response.result, response.code, {}}; + } + + for (auto binaryHash : response.data) { + if (binaryHash.name == "app.bin") { + static_assert(sizeof(release.appBinaryHash) == sizeof(binaryHash.hash), "Hash size mismatch"); + memcpy(release.appBinaryHash, binaryHash.hash, sizeof(release.appBinaryHash)); + } else if (binaryHash.name == "staticfs.bin") { + static_assert(sizeof(release.filesystemBinaryHash) == sizeof(binaryHash.hash), "Hash size mismatch"); + memcpy(release.filesystemBinaryHash, binaryHash.hash, sizeof(release.filesystemBinaryHash)); + } + } + + return {response.result, response.code, release}; +} diff --git a/src/ota/OtaUpdateClient.cpp b/src/ota/OtaUpdateClient.cpp index e202bb8f..fbb35f37 100644 --- a/src/ota/OtaUpdateClient.cpp +++ b/src/ota/OtaUpdateClient.cpp @@ -1,24 +1,33 @@ #include +#include "CaptivePortal.h" +#include "config/Config.h" +#include "GatewayConnectionManager.h" +#include "http/FirmwareCDN.h" #include "Logging.h" +#include "ota/FirmwareReleaseInfo.h" #include "ota/OtaUpdateClient.h" +#include "ota/OtaUpdateManager.h" #include "ota/OtaUpdateStep.h" #include "serialization/WSGateway.h" - #include "util/FnProxy.h" #include "util/HexUtils.h" #include "util/PartitionUtils.h" #include "util/TaskUtils.h" +#include + #include #include #include +#include #include const char* const TAG = "OtaUpdateClient"; using namespace OpenShock; +using namespace std::string_view_literals; bool _tryStartUpdate(const OpenShock::SemVer& version) { @@ -209,7 +218,7 @@ void OtaUpdateClient::_task() continue; } - if (!Serialization::Gateway::SerializeOtaInstallStartedMessage(updateId, version, GatewayConnectionManager::SendMessageBIN)) { + if (!Serialization::Gateway::SerializeOtaInstallStartedMessage(updateId, m_version, GatewayConnectionManager::SendMessageBIN)) { OS_LOGE(TAG, "Failed to serialize OTA install started message"); continue; } @@ -219,16 +228,18 @@ void OtaUpdateClient::_task() } // Fetch current release. - OtaUpdateManager::FirmwareRelease release; - if (!OtaUpdateManager::TryGetFirmwareRelease(version, release)) { - OS_LOGE(TAG, "Failed to fetch firmware release"); // TODO: Send error message to server + auto response = HTTP::FirmwareCDN::GetFirmwareReleaseInfo(m_version); + if (response.result != HTTP::RequestResult::Success) { + OS_LOGE(TAG, "Failed to fetch firmware release: [%u]", response.code); _sendFailureMessage("Failed to fetch firmware release"sv); continue; } + auto& release = response.data; + // Print release. OS_LOGD(TAG, "Firmware release:"); - OS_LOGD(TAG, " Version: %s", version.toString().c_str()); // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this + OS_LOGD(TAG, " Version: %s", m_version.toString().c_str()); // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this OS_LOGD(TAG, " App binary URL: %s", release.appBinaryUrl.c_str()); OS_LOGD(TAG, " App binary hash: %s", HexUtils::ToHex<32>(release.appBinaryHash).data()); OS_LOGD(TAG, " Filesystem binary URL: %s", release.filesystemBinaryUrl.c_str()); diff --git a/src/ota/OtaUpdateManager.cpp b/src/ota/OtaUpdateManager.cpp index 6d72b210..ba215930 100644 --- a/src/ota/OtaUpdateManager.cpp +++ b/src/ota/OtaUpdateManager.cpp @@ -38,9 +38,8 @@ bool verifyRollbackLater() using namespace OpenShock; enum OtaTaskEventFlag : uint32_t { - OTA_TASK_EVENT_UPDATE_REQUESTED = 1 << 0, - OTA_TASK_EVENT_WIFI_DISCONNECTED = 1 << 1, // If both connected and disconnected are set, disconnected takes priority. - OTA_TASK_EVENT_WIFI_CONNECTED = 1 << 2, + OTA_TASK_EVENT_WIFI_DISCONNECTED = 1 << 0, // If both connected and disconnected are set, disconnected takes priority. + OTA_TASK_EVENT_WIFI_CONNECTED = 1 << 1, }; static esp_ota_img_states_t _otaImageState; @@ -92,7 +91,6 @@ void _otaWatcherTask(void*) OS_LOGD(TAG, "OTA update task started"); bool connected = false; - bool updateRequested = false; int64_t lastUpdateCheck = 0; // Update task loop. @@ -101,8 +99,6 @@ void _otaWatcherTask(void*) uint32_t eventBits = 0; xTaskNotifyWait(0, UINT32_MAX, &eventBits, pdMS_TO_TICKS(5000)); // TODO: wait for rest time - updateRequested |= (eventBits & OTA_TASK_EVENT_UPDATE_REQUESTED) != 0; - if ((eventBits & OTA_TASK_EVENT_WIFI_DISCONNECTED) != 0) { OS_LOGD(TAG, "WiFi disconnected"); connected = false; @@ -139,7 +135,6 @@ void _otaWatcherTask(void*) bool check = false; check |= config.checkOnStartup && firstCheck; // On startup check |= config.checkPeriodically && diffMins >= config.checkInterval; // Periodically - check |= updateRequested && (firstCheck || diffMins >= 1); // Update requested if (!check) { continue; @@ -153,33 +148,16 @@ void _otaWatcherTask(void*) continue; } - OpenShock::SemVer version; - if (updateRequested) { - updateRequested = false; - - if (!_tryGetRequestedVersion(version)) { - OS_LOGE(TAG, "Failed to get requested version"); - continue; - } - - OS_LOGD(TAG, "Update requested for version %s", version.toString().c_str()); // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this - } else { - OS_LOGD(TAG, "Checking for updates"); - - // Fetch current version. - auto result = HTTP::FirmwareCDN::GetFirmwareVersion(config.updateChannel); - if (result.result != HTTP::RequestResult::Success) { - OS_LOGE(TAG, "Failed to fetch firmware version"); - continue; - } - - OS_LOGD(TAG, "Remote version: %s", version.toString().c_str()); // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this - } + OS_LOGD(TAG, "Checking for updates"); - if (version.toString() == OPENSHOCK_FW_VERSION) { // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this - OS_LOGI(TAG, "Requested version is already installed"); + // Fetch current version. + auto result = HTTP::FirmwareCDN::GetFirmwareVersion(config.updateChannel); + if (result.result != HTTP::RequestResult::Success) { + OS_LOGE(TAG, "Failed to fetch firmware version"); continue; } + + OS_LOGD(TAG, "Remote version: %s", result.data.toString().c_str()); // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this } } @@ -242,42 +220,13 @@ bool OtaUpdateManager::Init() return true; } -bool OtaUpdateManager::TryGetFirmwareRelease(const OpenShock::SemVer& version, FirmwareReleaseInfo& release) +bool OtaUpdateManager::TryStartFirmwareInstallation(const OpenShock::SemVer& version) { - auto versionStr = version.toString(); // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this - - if (!FormatToString(release.appBinaryUrl, OPENSHOCK_FW_CDN_APP_URL_FORMAT, versionStr.c_str())) { - OS_LOGE(TAG, "Failed to format URL"); - return false; - } - - if (!FormatToString(release.filesystemBinaryUrl, OPENSHOCK_FW_CDN_FILESYSTEM_URL_FORMAT, versionStr.c_str())) { - OS_LOGE(TAG, "Failed to format URL"); - return false; - } - - // Fetch hashes. - auto response = HTTP::FirmwareCDN::GetFirmwareBinaryHashes(version); - if (response.result != HTTP::RequestResult::Success) { - OS_LOGE(TAG, "Failed to fetch hashes: [%u]", response.code); - return false; - } - - for (auto binaryHash : response.data) { - if (binaryHash.name == "app.bin") { - static_assert(sizeof(release.appBinaryHash) == sizeof(binaryHash.hash), "Hash size mismatch"); - memcpy(release.appBinaryHash, binaryHash.hash, sizeof(release.appBinaryHash)); - } else if (binaryHash.name == "staticfs.bin") { - static_assert(sizeof(release.filesystemBinaryHash) == sizeof(binaryHash.hash), "Hash size mismatch"); - memcpy(release.filesystemBinaryHash, binaryHash.hash, sizeof(release.filesystemBinaryHash)); - } + if (version == OPENSHOCK_FW_VERSION ""sv) { + OS_LOGI(TAG, "Requested version is already installed"); + return true; } - return true; -} - -bool OtaUpdateManager::TryStartFirmwareInstallation(const OpenShock::SemVer& version) -{ OS_LOGD(TAG, "Requesting firmware version %s", version.toString().c_str()); // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this return _tryStartUpdate(version); From e4077493185447e7b44a8378fcd9f9c9815fdf9a Mon Sep 17 00:00:00 2001 From: hhvrc Date: Thu, 14 Nov 2024 14:31:48 +0100 Subject: [PATCH 6/9] Apply other lost changes --- src/ota/OtaUpdateClient.cpp | 2 - src/ota/OtaUpdateManager.cpp | 99 +++++++++++++++++++++++------------- 2 files changed, 65 insertions(+), 36 deletions(-) diff --git a/src/ota/OtaUpdateClient.cpp b/src/ota/OtaUpdateClient.cpp index fbb35f37..d2fe361c 100644 --- a/src/ota/OtaUpdateClient.cpp +++ b/src/ota/OtaUpdateClient.cpp @@ -172,8 +172,6 @@ bool _flashFilesystemPartition(const esp_partition_t* parition, std::string_view } test.end(); - OpenShock::CaptivePortal::ForceClose(false); - return true; } diff --git a/src/ota/OtaUpdateManager.cpp b/src/ota/OtaUpdateManager.cpp index ba215930..d39c5cb2 100644 --- a/src/ota/OtaUpdateManager.cpp +++ b/src/ota/OtaUpdateManager.cpp @@ -11,6 +11,7 @@ const char* const TAG = "OtaUpdateManager"; #include "ota/OtaUpdateClient.h" #include "ota/OtaUpdateStep.h" #include "SemVer.h" +#include "SimpleMutex.h" #include "util/StringUtils.h" #include "util/TaskUtils.h" @@ -35,58 +36,78 @@ bool verifyRollbackLater() return true; } -using namespace OpenShock; - enum OtaTaskEventFlag : uint32_t { OTA_TASK_EVENT_WIFI_DISCONNECTED = 1 << 0, // If both connected and disconnected are set, disconnected takes priority. OTA_TASK_EVENT_WIFI_CONNECTED = 1 << 1, }; -static esp_ota_img_states_t _otaImageState; -static OpenShock::FirmwareBootType _bootType; -static TaskHandle_t _watcherTaskHandle = nullptr; -static SemaphoreHandle_t _updateClientMutex = xSemaphoreCreateMutex(); -static std::unique_ptr _updateClient = nullptr; +static esp_ota_img_states_t s_otaImageState; +static OpenShock::FirmwareBootType s_bootType; +static TaskHandle_t s_taskHandle = nullptr; +static OpenShock::SimpleMutex s_clientMtx = {}; +static std::unique_ptr s_client = nullptr; -bool _tryStartUpdate(const OpenShock::SemVer& version) +using namespace OpenShock; + +static bool tryStartUpdate(const OpenShock::SemVer& version) { - if (xSemaphoreTake(_updateClientMutex, pdMS_TO_TICKS(1000)) != pdTRUE) { + if (!s_clientMtx.lock(pdMS_TO_TICKS(1000))) { OS_LOGE(TAG, "Failed to take requested version mutex"); return false; } - if (_updateClient != nullptr) { - xSemaphoreGive(_updateClientMutex); + if (s_client != nullptr) { + s_clientMtx.unlock(); OS_LOGE(TAG, "Update client already started"); return false; } - _updateClient = std::make_unique(version); + s_client = std::make_unique(version); - if (!_updateClient->Start()) { - _updateClient.reset(); - xSemaphoreGive(_updateClientMutex); + if (!s_client->Start()) { + s_client.reset(); + s_clientMtx.unlock(); OS_LOGE(TAG, "Failed to start update client"); return false; } - xSemaphoreGive(_updateClientMutex); + s_clientMtx.unlock(); OS_LOGD(TAG, "Update client started"); return true; } -void _otaEvGotIPHandler(arduino_event_t*) +static void wifiDisconnectedEventHandler(void* event_handler_arg, esp_event_base_t event_base, int32_t event_id, void* event_data) { - xTaskNotify(_watcherTaskHandle, OTA_TASK_EVENT_WIFI_CONNECTED, eSetBits); + (void)event_handler_arg; + (void)event_base; + (void)event_id; + (void)event_data; + + xTaskNotify(s_taskHandle, OTA_TASK_EVENT_WIFI_DISCONNECTED, eSetBits); } -void _otaEvWiFiDisconnectedHandler(arduino_event_t*) + +static void ipEventHandler(void* event_handler_arg, esp_event_base_t event_base, int32_t event_id, void* event_data) { - xTaskNotify(_watcherTaskHandle, OTA_TASK_EVENT_WIFI_DISCONNECTED, eSetBits); + (void)event_handler_arg; + (void)event_base; + (void)event_data; + + switch (event_id) { + case IP_EVENT_GOT_IP6: + case IP_EVENT_STA_GOT_IP: + xTaskNotify(s_taskHandle, OTA_TASK_EVENT_WIFI_CONNECTED, eSetBits); + break; + case IP_EVENT_STA_LOST_IP: + xTaskNotify(s_taskHandle, OTA_TASK_EVENT_WIFI_DISCONNECTED, eSetBits); + break; + default: + return; + } } -void _otaWatcherTask(void*) +static void watcherTask(void*) { OS_LOGD(TAG, "OTA update task started"); @@ -163,6 +184,8 @@ void _otaWatcherTask(void*) bool OtaUpdateManager::Init() { + esp_err_t err; + OS_LOGN(TAG, "Fetching current partition"); // Fetch current partition info. @@ -175,7 +198,7 @@ bool OtaUpdateManager::Init() OS_LOGD(TAG, "Fetching partition state"); // Get OTA state for said partition. - esp_err_t err = esp_ota_get_state_partition(partition, &_otaImageState); + err = esp_ota_get_state_partition(partition, &s_otaImageState); if (err != ESP_OK) { OS_PANIC(TAG, "Failed to get partition state: %s", esp_err_to_name(err)); return false; // This will never be reached, but the compiler doesn't know that. @@ -191,14 +214,14 @@ bool OtaUpdateManager::Init() // Infer boot type from update step. switch (updateStep) { case OtaUpdateStep::Updated: - _bootType = FirmwareBootType::NewFirmware; + s_bootType = FirmwareBootType::NewFirmware; break; case OtaUpdateStep::Validating: // If the update step is validating, we have failed in the middle of validating the new firmware, meaning this is a rollback. case OtaUpdateStep::RollingBack: - _bootType = FirmwareBootType::Rollback; + s_bootType = FirmwareBootType::Rollback; break; default: - _bootType = FirmwareBootType::Normal; + s_bootType = FirmwareBootType::Normal; break; } @@ -208,11 +231,19 @@ bool OtaUpdateManager::Init() } } - WiFi.onEvent(_otaEvGotIPHandler, ARDUINO_EVENT_WIFI_STA_GOT_IP); - WiFi.onEvent(_otaEvGotIPHandler, ARDUINO_EVENT_WIFI_STA_GOT_IP6); - WiFi.onEvent(_otaEvWiFiDisconnectedHandler, ARDUINO_EVENT_WIFI_STA_DISCONNECTED); + err = esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID, ipEventHandler, nullptr); + if (err != ESP_OK) { + OS_LOGE(TAG, "Failed to register event handler for IP_EVENT: %s", esp_err_to_name(err)); + return false; + } + + err = esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, wifiDisconnectedEventHandler, nullptr); + if (err != ESP_OK) { + OS_LOGE(TAG, "Failed to register event handler for WIFI_EVENT: %s", esp_err_to_name(err)); + return false; + } - if (TaskUtils::TaskCreateExpensive(_otaWatcherTask, "OtaWatcherTask", 8192, nullptr, 1, &_watcherTaskHandle) != pdPASS) { + if (TaskUtils::TaskCreateExpensive(watcherTask, "OtaWatcherTask", 8192, nullptr, 1, &s_taskHandle) != pdPASS) { OS_LOGE(TAG, "Failed to create OTA watcher task"); return false; } @@ -220,7 +251,7 @@ bool OtaUpdateManager::Init() return true; } -bool OtaUpdateManager::TryStartFirmwareInstallation(const OpenShock::SemVer& version) +bool OtaUpdateManager::TryStartFirmwareUpdate(const OpenShock::SemVer& version) { if (version == OPENSHOCK_FW_VERSION ""sv) { OS_LOGI(TAG, "Requested version is already installed"); @@ -229,17 +260,17 @@ bool OtaUpdateManager::TryStartFirmwareInstallation(const OpenShock::SemVer& ver OS_LOGD(TAG, "Requesting firmware version %s", version.toString().c_str()); // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this - return _tryStartUpdate(version); + return tryStartUpdate(version); } FirmwareBootType OtaUpdateManager::GetFirmwareBootType() { - return _bootType; + return s_bootType; } bool OtaUpdateManager::IsValidatingApp() { - return _otaImageState == ESP_OTA_IMG_PENDING_VERIFY; + return s_otaImageState == ESP_OTA_IMG_PENDING_VERIFY; } void OtaUpdateManager::InvalidateAndRollback() @@ -281,5 +312,5 @@ void OtaUpdateManager::ValidateApp() OS_PANIC(TAG, "Failed to set OTA firmware boot type in critical section"); // TODO: THIS IS A CRITICAL SECTION, WHAT DO WE DO? } - _otaImageState = ESP_OTA_IMG_VALID; + s_otaImageState = ESP_OTA_IMG_VALID; } From 816cb6fca615ad517ed45f20206e2c83e2870826 Mon Sep 17 00:00:00 2001 From: HeavenVR Date: Thu, 26 Mar 2026 23:20:28 +0100 Subject: [PATCH 7/9] refactor(ota): split OTA into Manager, Client, and FirmwareCDN Separate OTA update system into three concerns: - OtaUpdateManager: watcher task, lifecycle, WiFi events, periodic checks - OtaUpdateClient: single update execution (fetch, flash, reboot) - FirmwareCDN: HTTP calls to firmware CDN (already existed, now wired in) Fix OtaUpdateClient to be a clean one-shot task instead of containing the watcher loop. Use FirmwareCDN::GetFirmwareReleaseInfo instead of inline HTTP. Fix FnProxy usage, wrong serialization namespaces, stale API calls, and missing includes. Remove _ prefix from all statics. Update FirmwareCDN to current HTTP API (tcb::span, ContentType constants). Co-Authored-By: Claude Opus 4.6 (1M context) --- .changeset/clever-foxes-build.md | 9 + include/ota/OtaUpdateClient.h | 2 +- include/ota/OtaUpdateManager.h | 7 - src/captiveportal/CaptivePortalInstance.cpp | 10 +- src/http/FirmwareCDN.cpp | 39 ++--- src/ota/OtaUpdateClient.cpp | 178 +++++++++----------- src/ota/OtaUpdateManager.cpp | 152 +++++++++-------- 7 files changed, 198 insertions(+), 199 deletions(-) create mode 100644 .changeset/clever-foxes-build.md diff --git a/.changeset/clever-foxes-build.md b/.changeset/clever-foxes-build.md new file mode 100644 index 00000000..6878591d --- /dev/null +++ b/.changeset/clever-foxes-build.md @@ -0,0 +1,9 @@ +--- +'firmware': patch +--- + +refactor: Split OTA update system into Manager, Client, and FirmwareCDN modules + +- **OtaUpdateManager**: Watcher task with WiFi event handling, periodic update checks, and firmware lifecycle management (boot type, validation, rollback) +- **OtaUpdateClient**: Single-shot update execution — fetches release metadata, flashes filesystem and app partitions, reboots into new firmware +- **FirmwareCDN**: HTTP client for the firmware CDN — version checks, board listings, binary hash fetching, and release info assembly diff --git a/include/ota/OtaUpdateClient.h b/include/ota/OtaUpdateClient.h index ed985b40..10ccd891 100644 --- a/include/ota/OtaUpdateClient.h +++ b/include/ota/OtaUpdateClient.h @@ -13,7 +13,7 @@ namespace OpenShock { bool Start(); private: - void _task(); + void task(); OpenShock::SemVer m_version; TaskHandle_t m_taskHandle; diff --git a/include/ota/OtaUpdateManager.h b/include/ota/OtaUpdateManager.h index cf532d48..2c510597 100644 --- a/include/ota/OtaUpdateManager.h +++ b/include/ota/OtaUpdateManager.h @@ -1,15 +1,8 @@ #pragma once #include "FirmwareBootType.h" -#include "FirmwareReleaseInfo.h" -#include "OtaUpdateChannel.h" #include "SemVer.h" -#include -#include -#include -#include - namespace OpenShock::OtaUpdateManager { [[nodiscard]] bool Init(); diff --git a/src/captiveportal/CaptivePortalInstance.cpp b/src/captiveportal/CaptivePortalInstance.cpp index ffed2400..c9c20f4e 100644 --- a/src/captiveportal/CaptivePortalInstance.cpp +++ b/src/captiveportal/CaptivePortalInstance.cpp @@ -13,9 +13,9 @@ const char* const TAG = "CaptivePortalInstance"; #include "GatewayConnectionManager.h" #include "http/ContentTypes.h" #include "Logging.h" -#include "RateLimiter.h" #include "message_handlers/WebSocket.h" -#include "OtaUpdateChannel.h" +#include "ota/OtaUpdateChannel.h" +#include "RateLimiter.h" #include "serialization/WSLocal.h" #include "util/FnProxy.h" #include "util/HexUtils.h" @@ -46,15 +46,15 @@ static const char* const JSON_ERR_PASSWORD_SHORT = "{\"error\":\"PasswordTooSho static const char* const JSON_ERR_PASSWORD_LONG = "{\"error\":\"PasswordTooLong\"}"; static const char* const JSON_ERR_CODE_REQUIRED = "{\"error\":\"CodeRequired\"}"; static const char* const JSON_ERR_INVALID_CHANNEL = "{\"error\":\"InvalidChannel\"}"; -static const char* const JSON_ERR_RATE_LIMITED = "{\"error\":\"RateLimited\"}"; +static const char* const JSON_ERR_RATE_LIMITED = "{\"error\":\"RateLimited\"}"; static OpenShock::RateLimiter& getAccountLinkRateLimiter() { static OpenShock::RateLimiter* rl = nullptr; if (rl == nullptr) { rl = new OpenShock::RateLimiter(); - rl->addLimit(60'000, 5); // 5 attempts per minute - rl->addLimit(300'000, 10); // 10 attempts per 5 minutes + rl->addLimit(60'000, 5); // 5 attempts per minute + rl->addLimit(300'000, 10); // 10 attempts per 5 minutes } return *rl; } diff --git a/src/http/FirmwareCDN.cpp b/src/http/FirmwareCDN.cpp index 04c0f4bd..a74c714f 100644 --- a/src/http/FirmwareCDN.cpp +++ b/src/http/FirmwareCDN.cpp @@ -3,6 +3,7 @@ #include "http/FirmwareCDN.h" #include "Common.h" +#include "http/ContentTypes.h" #include "Logging.h" #include "util/HexUtils.h" #include "util/StringUtils.h" @@ -13,6 +14,8 @@ using namespace std::string_view_literals; using namespace OpenShock; +static const uint16_t s_acceptedCodes[] = {200, 304}; + HTTP::Response HTTP::FirmwareCDN::GetFirmwareVersion(OtaUpdateChannel channel) { std::string_view channelIndexUrl; @@ -36,9 +39,9 @@ HTTP::Response HTTP::FirmwareCDN::GetFirmwareVersion(OtaUpdat auto response = OpenShock::HTTP::GetString( channelIndexUrl, { - {"Accept", "text/plain"} + {"Accept", HTTP::ContentType::TextPlain} }, - {200, 304} + s_acceptedCodes ); if (response.result != OpenShock::HTTP::RequestResult::Success) { @@ -58,7 +61,7 @@ HTTP::Response HTTP::FirmwareCDN::GetFirmwareVersion(OtaUpdat HTTP::Response> HTTP::FirmwareCDN::GetFirmwareBoards(const OpenShock::SemVer& version) { std::string channelIndexUrl; - if (!FormatToString(channelIndexUrl, OPENSHOCK_FW_CDN_BOARDS_INDEX_URL_FORMAT, version.toString().c_str())) { // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this + if (!FormatToString(channelIndexUrl, OPENSHOCK_FW_CDN_BOARDS_INDEX_URL_FORMAT, version.toString().c_str())) { OS_LOGE(TAG, "Failed to format URL"); return {RequestResult::InternalError, 0, {}}; } @@ -68,9 +71,9 @@ HTTP::Response> HTTP::FirmwareCDN::GetFirmwareBoards(co auto response = OpenShock::HTTP::GetString( channelIndexUrl, { - {"Accept", "text/plain"} + {"Accept", HTTP::ContentType::TextPlain} }, - {200, 304} + s_acceptedCodes ); if (response.result != OpenShock::HTTP::RequestResult::Success) { @@ -78,12 +81,12 @@ HTTP::Response> HTTP::FirmwareCDN::GetFirmwareBoards(co return {RequestResult::InternalError, 0, {}}; } - auto lines = OpenShock::StringSplitNewLines(response.data); + std::vector lines = OpenShock::StringSplitNewLines(response.data); std::vector boards; boards.reserve(lines.size()); - for (auto line : lines) { + for (std::string_view line : lines) { line = OpenShock::StringTrim(line); if (line.empty()) { @@ -98,34 +101,31 @@ HTTP::Response> HTTP::FirmwareCDN::GetFirmwareBoards(co HTTP::Response> HTTP::FirmwareCDN::GetFirmwareBinaryHashes(const OpenShock::SemVer& version) { - auto versionStr = version.toString(); // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this + auto versionStr = version.toString(); - // Construct hash URLs. std::string sha256HashesUrl; if (!FormatToString(sha256HashesUrl, OPENSHOCK_FW_CDN_SHA256_HASHES_URL_FORMAT, versionStr.c_str())) { OS_LOGE(TAG, "Failed to format URL"); return {RequestResult::InternalError, 0, {}}; } - // Fetch hashes. auto sha256HashesResponse = OpenShock::HTTP::GetString( sha256HashesUrl, { - {"Accept", "text/plain"} + {"Accept", HTTP::ContentType::TextPlain} }, - {200, 304} + s_acceptedCodes ); if (sha256HashesResponse.result != OpenShock::HTTP::RequestResult::Success) { OS_LOGE(TAG, "Failed to fetch hashes: [%u] %s", sha256HashesResponse.code, sha256HashesResponse.data.c_str()); return {RequestResult::InternalError, 0, {}}; } - auto hashesLines = OpenShock::StringSplitNewLines(sha256HashesResponse.data); + std::vector hashesLines = OpenShock::StringSplitNewLines(sha256HashesResponse.data); - // Parse hashes. std::vector hashes; for (std::string_view line : hashesLines) { - auto parts = OpenShock::StringSplitWhiteSpace(line); + std::vector parts = OpenShock::StringSplitWhiteSpace(line); if (parts.size() != 2) { OS_LOGE(TAG, "Invalid hashes entry: %.*s", line.size(), line.data()); return {RequestResult::InternalError, 0, {}}; @@ -134,9 +134,7 @@ HTTP::Response> HTTP::FirmwareCDN::GetFirmwareBi auto hash = OpenShock::StringTrim(parts[0]); auto file = OpenShock::StringTrim(parts[1]); - if (OpenShock::StringStartsWith(file, "./"sv)) { - file = file.substr(2); - } + file = OpenShock::StringRemovePrefix(file, "./"sv); if (hash.size() != 64) { OS_LOGE(TAG, "Invalid hash: %.*s", hash.size(), hash.data()); @@ -160,7 +158,7 @@ HTTP::Response> HTTP::FirmwareCDN::GetFirmwareBi HTTP::Response HTTP::FirmwareCDN::GetFirmwareReleaseInfo(const OpenShock::SemVer& version) { - auto versionStr = version.toString(); // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this + auto versionStr = version.toString(); FirmwareReleaseInfo release; if (!FormatToString(release.appBinaryUrl, OPENSHOCK_FW_CDN_APP_URL_FORMAT, versionStr.c_str())) { @@ -173,14 +171,13 @@ HTTP::Response HTTP::FirmwareCDN::GetFirmwareReleaseInfo(co return {RequestResult::InternalError, 0, {}}; } - // Fetch hashes. auto response = GetFirmwareBinaryHashes(version); if (response.result != HTTP::RequestResult::Success) { OS_LOGE(TAG, "Failed to fetch hashes: [%u]", response.code); return {response.result, response.code, {}}; } - for (auto binaryHash : response.data) { + for (const auto& binaryHash : response.data) { if (binaryHash.name == "app.bin") { static_assert(sizeof(release.appBinaryHash) == sizeof(binaryHash.hash), "Hash size mismatch"); memcpy(release.appBinaryHash, binaryHash.hash, sizeof(release.appBinaryHash)); diff --git a/src/ota/OtaUpdateClient.cpp b/src/ota/OtaUpdateClient.cpp index d2fe361c..57e9305c 100644 --- a/src/ota/OtaUpdateClient.cpp +++ b/src/ota/OtaUpdateClient.cpp @@ -1,13 +1,14 @@ #include -#include "CaptivePortal.h" +#include "ota/OtaUpdateClient.h" + +const char* const TAG = "OtaUpdateClient"; + +#include "captiveportal/Manager.h" #include "config/Config.h" #include "GatewayConnectionManager.h" #include "http/FirmwareCDN.h" #include "Logging.h" -#include "ota/FirmwareReleaseInfo.h" -#include "ota/OtaUpdateClient.h" -#include "ota/OtaUpdateManager.h" #include "ota/OtaUpdateStep.h" #include "serialization/WSGateway.h" #include "util/FnProxy.h" @@ -20,46 +21,13 @@ #include #include #include -#include #include -const char* const TAG = "OtaUpdateClient"; - using namespace OpenShock; using namespace std::string_view_literals; -bool _tryStartUpdate(const OpenShock::SemVer& version) -{ - if (xSemaphoreTake(_requestedVersionMutex, pdMS_TO_TICKS(1000)) != pdTRUE) { - OS_LOGE(TAG, "Failed to take requested version mutex"); - return false; - } - - _requestedVersion = version; - - xSemaphoreGive(_requestedVersionMutex); - - xTaskNotify(_taskHandle, OTA_TASK_EVENT_UPDATE_REQUESTED, eSetBits); - - return true; -} - -bool _tryGetRequestedVersion(OpenShock::SemVer& version) -{ - if (xSemaphoreTake(_requestedVersionMutex, pdMS_TO_TICKS(1000)) != pdTRUE) { - OS_LOGE(TAG, "Failed to take requested version mutex"); - return false; - } - - version = _requestedVersion; - - xSemaphoreGive(_requestedVersionMutex); - - return true; -} - -bool _sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask task, float progress) +static bool sendProgressMessage(Serialization::Types::OtaUpdateProgressTask task, float progress) { int32_t updateId; if (!Config::GetOtaUpdateId(updateId)) { @@ -67,14 +35,15 @@ bool _sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask task, f return false; } - if (!Serialization::Gateway::SerializeOtaInstallProgressMessage(updateId, task, progress, GatewayConnectionManager::SendMessageBIN)) { - OS_LOGE(TAG, "Failed to send OTA install progress message"); + if (!Serialization::Gateway::SerializeOtaUpdateProgressMessage(updateId, task, progress, GatewayConnectionManager::SendMessageBIN)) { + OS_LOGE(TAG, "Failed to send OTA update progress message"); return false; } return true; } -bool _sendFailureMessage(std::string_view message, bool fatal = false) + +static bool sendFailureMessage(std::string_view message, bool fatal = false) { int32_t updateId; if (!Config::GetOtaUpdateId(updateId)) { @@ -82,92 +51,87 @@ bool _sendFailureMessage(std::string_view message, bool fatal = false) return false; } - if (!Serialization::Gateway::SerializeOtaInstallFailedMessage(updateId, message, fatal, GatewayConnectionManager::SendMessageBIN)) { - OS_LOGE(TAG, "Failed to send OTA install failed message"); + if (!Serialization::Gateway::SerializeOtaUpdateFailedMessage(updateId, message, fatal, GatewayConnectionManager::SendMessageBIN)) { + OS_LOGE(TAG, "Failed to send OTA update failed message"); return false; } return true; } -bool _flashAppPartition(const esp_partition_t* partition, std::string_view remoteUrl, const uint8_t (&remoteHash)[32]) +static bool flashAppPartition(const esp_partition_t* partition, std::string_view remoteUrl, const uint8_t (&remoteHash)[32]) { OS_LOGD(TAG, "Flashing app partition"); - if (!_sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask::FlashingApplication, 0.0f)) { + if (!sendProgressMessage(Serialization::Types::OtaUpdateProgressTask::FlashingApplication, 0.0f)) { return false; } auto onProgress = [](std::size_t current, std::size_t total, float progress) -> bool { OS_LOGD(TAG, "Flashing app partition: %u / %u (%.2f%%)", current, total, progress * 100.0f); - - _sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask::FlashingApplication, progress); - + sendProgressMessage(Serialization::Types::OtaUpdateProgressTask::FlashingApplication, progress); return true; }; if (!OpenShock::FlashPartitionFromUrl(partition, remoteUrl, remoteHash, onProgress)) { OS_LOGE(TAG, "Failed to flash app partition"); - _sendFailureMessage("Failed to flash app partition"sv); + sendFailureMessage("Failed to flash app partition"sv); return false; } - if (!_sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask::MarkingApplicationBootable, 0.0f)) { + if (!sendProgressMessage(Serialization::Types::OtaUpdateProgressTask::MarkingApplicationBootable, 0.0f)) { return false; } - // Set app partition bootable. if (esp_ota_set_boot_partition(partition) != ESP_OK) { OS_LOGE(TAG, "Failed to set app partition bootable"); - _sendFailureMessage("Failed to set app partition bootable"sv); + sendFailureMessage("Failed to set app partition bootable"sv); return false; } return true; } -bool _flashFilesystemPartition(const esp_partition_t* parition, std::string_view remoteUrl, const uint8_t (&remoteHash)[32]) +static bool flashFilesystemPartition(const esp_partition_t* partition, std::string_view remoteUrl, const uint8_t (&remoteHash)[32]) { - if (!_sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask::PreparingForInstall, 0.0f)) { + if (!sendProgressMessage(Serialization::Types::OtaUpdateProgressTask::PreparingForUpdate, 0.0f)) { return false; } // Make sure captive portal is stopped, timeout after 5 seconds. if (!CaptivePortal::ForceClose(5000U)) { OS_LOGE(TAG, "Failed to force close captive portal (timed out)"); - _sendFailureMessage("Failed to force close captive portal (timed out)"sv); + sendFailureMessage("Failed to force close captive portal (timed out)"sv); return false; } OS_LOGD(TAG, "Flashing filesystem partition"); - if (!_sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask::FlashingFilesystem, 0.0f)) { + if (!sendProgressMessage(Serialization::Types::OtaUpdateProgressTask::FlashingFilesystem, 0.0f)) { return false; } auto onProgress = [](std::size_t current, std::size_t total, float progress) -> bool { OS_LOGD(TAG, "Flashing filesystem partition: %u / %u (%.2f%%)", current, total, progress * 100.0f); - - _sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask::FlashingFilesystem, progress); - + sendProgressMessage(Serialization::Types::OtaUpdateProgressTask::FlashingFilesystem, progress); return true; }; - if (!OpenShock::FlashPartitionFromUrl(parition, remoteUrl, remoteHash, onProgress)) { + if (!OpenShock::FlashPartitionFromUrl(partition, remoteUrl, remoteHash, onProgress)) { OS_LOGE(TAG, "Failed to flash filesystem partition"); - _sendFailureMessage("Failed to flash filesystem partition"sv); + sendFailureMessage("Failed to flash filesystem partition"sv); return false; } - if (!_sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask::VerifyingFilesystem, 0.0f)) { + if (!sendProgressMessage(Serialization::Types::OtaUpdateProgressTask::VerifyingFilesystem, 0.0f)) { return false; } - // Attempt to mount filesystem. + // Attempt to mount filesystem to verify it's valid. fs::LittleFSFS test; if (!test.begin(false, "/static", 10, "static0")) { OS_LOGE(TAG, "Failed to mount filesystem"); - _sendFailureMessage("Failed to mount filesystem"sv); + sendFailureMessage("Failed to mount filesystem"sv); return false; } test.end(); @@ -195,7 +159,7 @@ bool OtaUpdateClient::Start() return false; } - if (TaskUtils::TaskCreateExpensive(&Util::FnProxy<&OtaUpdateClient::_task>, TAG, 8192, this, 1, &m_taskHandle) != pdPASS) { + if (TaskUtils::TaskCreateExpensive(Util::FnProxy<&OtaUpdateClient::task>, TAG, 8192, this, 1, &m_taskHandle) != pdPASS) { OS_LOGE(TAG, "Failed to create OTA update task"); return false; } @@ -203,88 +167,104 @@ bool OtaUpdateClient::Start() return true; } -void OtaUpdateClient::_task() +void OtaUpdateClient::task() { + OS_LOGI(TAG, "OTA update task started for version %s", m_version.toString().c_str()); + + std::string versionStr = m_version.toString(); + // Generate random int32_t for this update. int32_t updateId = static_cast(esp_random()); if (!Config::SetOtaUpdateId(updateId)) { OS_LOGE(TAG, "Failed to set OTA update ID"); - continue; + vTaskDelete(nullptr); + return; } if (!Config::SetOtaUpdateStep(OpenShock::OtaUpdateStep::Updating)) { OS_LOGE(TAG, "Failed to set OTA update step"); - continue; + vTaskDelete(nullptr); + return; } - if (!Serialization::Gateway::SerializeOtaInstallStartedMessage(updateId, m_version, GatewayConnectionManager::SendMessageBIN)) { - OS_LOGE(TAG, "Failed to serialize OTA install started message"); - continue; + if (!Serialization::Gateway::SerializeOtaUpdateStartedMessage(updateId, m_version, GatewayConnectionManager::SendMessageBIN)) { + OS_LOGE(TAG, "Failed to serialize OTA update started message"); + vTaskDelete(nullptr); + return; } - if (!_sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask::FetchingMetadata, 0.0f)) { - continue; + if (!sendProgressMessage(Serialization::Types::OtaUpdateProgressTask::FetchingMetadata, 0.0f)) { + vTaskDelete(nullptr); + return; } - // Fetch current release. + // Fetch release info from CDN. auto response = HTTP::FirmwareCDN::GetFirmwareReleaseInfo(m_version); if (response.result != HTTP::RequestResult::Success) { - OS_LOGE(TAG, "Failed to fetch firmware release: [%u]", response.code); - _sendFailureMessage("Failed to fetch firmware release"sv); - continue; + OS_LOGE(TAG, "Failed to fetch firmware release info"); + sendFailureMessage("Failed to fetch firmware release info"sv); + vTaskDelete(nullptr); + return; } auto& release = response.data; - // Print release. OS_LOGD(TAG, "Firmware release:"); - OS_LOGD(TAG, " Version: %s", m_version.toString().c_str()); // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this - OS_LOGD(TAG, " App binary URL: %s", release.appBinaryUrl.c_str()); + OS_LOGD(TAG, " Version: %.*s", versionStr.length(), versionStr.data()); + OS_LOGD(TAG, " App binary URL: %.*s", release.appBinaryUrl.length(), release.appBinaryUrl.data()); OS_LOGD(TAG, " App binary hash: %s", HexUtils::ToHex<32>(release.appBinaryHash).data()); - OS_LOGD(TAG, " Filesystem binary URL: %s", release.filesystemBinaryUrl.c_str()); + OS_LOGD(TAG, " Filesystem binary URL: %.*s", release.filesystemBinaryUrl.length(), release.filesystemBinaryUrl.data()); OS_LOGD(TAG, " Filesystem binary hash: %s", HexUtils::ToHex<32>(release.filesystemBinaryHash).data()); // Get available app update partition. const esp_partition_t* appPartition = esp_ota_get_next_update_partition(nullptr); if (appPartition == nullptr) { - OS_LOGE(TAG, "Failed to get app update partition"); // TODO: Send error message to server - _sendFailureMessage("Failed to get app update partition"sv); - continue; + OS_LOGE(TAG, "Failed to get app update partition"); + sendFailureMessage("Failed to get app update partition"sv); + vTaskDelete(nullptr); + return; } // Get filesystem partition. const esp_partition_t* filesystemPartition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, "static0"); if (filesystemPartition == nullptr) { - OS_LOGE(TAG, "Failed to find filesystem partition"); // TODO: Send error message to server - _sendFailureMessage("Failed to find filesystem partition"sv); - continue; + OS_LOGE(TAG, "Failed to find filesystem partition"); + sendFailureMessage("Failed to find filesystem partition"sv); + vTaskDelete(nullptr); + return; } - // Increase task watchdog timeout. - // Prevents panics on some ESP32s when clearing large partitions. + // Increase task watchdog timeout to prevent panics when clearing large partitions. esp_task_wdt_init(15, true); - // Flash app and filesystem partitions. - if (!_flashFilesystemPartition(filesystemPartition, release.filesystemBinaryUrl, release.filesystemBinaryHash)) continue; - if (!_flashAppPartition(appPartition, release.appBinaryUrl, release.appBinaryHash)) continue; + // Flash filesystem first, then app. + if (!flashFilesystemPartition(filesystemPartition, release.filesystemBinaryUrl, release.filesystemBinaryHash)) { + esp_task_wdt_init(5, true); + vTaskDelete(nullptr); + return; + } + if (!flashAppPartition(appPartition, release.appBinaryUrl, release.appBinaryHash)) { + esp_task_wdt_init(5, true); + vTaskDelete(nullptr); + return; + } // Set OTA boot type in config. if (!Config::SetOtaUpdateStep(OpenShock::OtaUpdateStep::Updated)) { OS_LOGE(TAG, "Failed to set OTA update step"); - _sendFailureMessage("Failed to set OTA update step"sv); - continue; + sendFailureMessage("Failed to set OTA update step"sv); + esp_task_wdt_init(5, true); + vTaskDelete(nullptr); + return; } - // Set task watchdog timeout back to default. + // Restore task watchdog timeout. esp_task_wdt_init(5, true); // Send reboot message. - _sendProgressMessage(Serialization::Gateway::OtaInstallProgressTask::Rebooting, 0.0f); + sendProgressMessage(Serialization::Types::OtaUpdateProgressTask::Rebooting, 0.0f); - // Reboot into new firmware. OS_LOGI(TAG, "Restarting into new firmware..."); vTaskDelay(pdMS_TO_TICKS(200)); - break; - // Restart. esp_restart(); } diff --git a/src/ota/OtaUpdateManager.cpp b/src/ota/OtaUpdateManager.cpp index d39c5cb2..252f1aac 100644 --- a/src/ota/OtaUpdateManager.cpp +++ b/src/ota/OtaUpdateManager.cpp @@ -6,18 +6,18 @@ const char* const TAG = "OtaUpdateManager"; #include "Common.h" #include "config/Config.h" +#include "Core.h" #include "http/FirmwareCDN.h" #include "Logging.h" #include "ota/OtaUpdateClient.h" #include "ota/OtaUpdateStep.h" #include "SemVer.h" #include "SimpleMutex.h" -#include "util/StringUtils.h" #include "util/TaskUtils.h" -#include // TODO: Get rid of Arduino entirely. >:( - +#include #include +#include #include #include @@ -37,48 +37,50 @@ bool verifyRollbackLater() } enum OtaTaskEventFlag : uint32_t { - OTA_TASK_EVENT_WIFI_DISCONNECTED = 1 << 0, // If both connected and disconnected are set, disconnected takes priority. - OTA_TASK_EVENT_WIFI_CONNECTED = 1 << 1, + OTA_TASK_EVENT_UPDATE_REQUESTED = 1 << 0, + OTA_TASK_EVENT_WIFI_DISCONNECTED = 1 << 1, // If both connected and disconnected are set, disconnected takes priority. + OTA_TASK_EVENT_WIFI_CONNECTED = 1 << 2, }; static esp_ota_img_states_t s_otaImageState; static OpenShock::FirmwareBootType s_bootType; -static TaskHandle_t s_taskHandle = nullptr; -static OpenShock::SimpleMutex s_clientMtx = {}; -static std::unique_ptr s_client = nullptr; +static TaskHandle_t s_taskHandle; +static OpenShock::SemVer s_requestedVersion; +static OpenShock::SimpleMutex s_requestedVersionMutex = {}; using namespace OpenShock; -static bool tryStartUpdate(const OpenShock::SemVer& version) +static bool tryQueueUpdateRequest(const OpenShock::SemVer& version) { - if (!s_clientMtx.lock(pdMS_TO_TICKS(1000))) { + if (!s_requestedVersionMutex.lock(pdMS_TO_TICKS(1000))) { OS_LOGE(TAG, "Failed to take requested version mutex"); return false; } - if (s_client != nullptr) { - s_clientMtx.unlock(); - OS_LOGE(TAG, "Update client already started"); - return false; - } + s_requestedVersion = version; - s_client = std::make_unique(version); + s_requestedVersionMutex.unlock(); + + xTaskNotify(s_taskHandle, OTA_TASK_EVENT_UPDATE_REQUESTED, eSetBits); + + return true; +} - if (!s_client->Start()) { - s_client.reset(); - s_clientMtx.unlock(); - OS_LOGE(TAG, "Failed to start update client"); +static bool tryGetRequestedVersion(OpenShock::SemVer& version) +{ + if (!s_requestedVersionMutex.lock(pdMS_TO_TICKS(1000))) { + OS_LOGE(TAG, "Failed to take requested version mutex"); return false; } - s_clientMtx.unlock(); + version = s_requestedVersion; - OS_LOGD(TAG, "Update client started"); + s_requestedVersionMutex.unlock(); return true; } -static void wifiDisconnectedEventHandler(void* event_handler_arg, esp_event_base_t event_base, int32_t event_id, void* event_data) +static void evWiFiDisconnectedHandler(void* event_handler_arg, esp_event_base_t event_base, int32_t event_id, void* event_data) { (void)event_handler_arg; (void)event_base; @@ -88,7 +90,7 @@ static void wifiDisconnectedEventHandler(void* event_handler_arg, esp_event_base xTaskNotify(s_taskHandle, OTA_TASK_EVENT_WIFI_DISCONNECTED, eSetBits); } -static void ipEventHandler(void* event_handler_arg, esp_event_base_t event_base, int32_t event_id, void* event_data) +static void evIpEventHandler(void* event_handler_arg, esp_event_base_t event_base, int32_t event_id, void* event_data) { (void)event_handler_arg; (void)event_base; @@ -107,23 +109,25 @@ static void ipEventHandler(void* event_handler_arg, esp_event_base_t event_base, } } -static void watcherTask(void*) +static void otaUpdateTask(void* pvParameters) { - OS_LOGD(TAG, "OTA update task started"); + (void)pvParameters; bool connected = false; + bool updateRequested = false; int64_t lastUpdateCheck = 0; - // Update task loop. - while (true) { - // Wait for event. + for (;;) { + // Wait for event uint32_t eventBits = 0; - xTaskNotifyWait(0, UINT32_MAX, &eventBits, pdMS_TO_TICKS(5000)); // TODO: wait for rest time + xTaskNotifyWait(0, UINT32_MAX, &eventBits, pdMS_TO_TICKS(5000)); + + updateRequested |= (eventBits & OTA_TASK_EVENT_UPDATE_REQUESTED) != 0; if ((eventBits & OTA_TASK_EVENT_WIFI_DISCONNECTED) != 0) { OS_LOGD(TAG, "WiFi disconnected"); connected = false; - continue; // No further processing needed. + continue; } if ((eventBits & OTA_TASK_EVENT_WIFI_CONNECTED) != 0 && !connected) { @@ -131,7 +135,6 @@ static void watcherTask(void*) connected = true; } - // If we're not connected, continue. if (!connected) { continue; } @@ -154,8 +157,9 @@ static void watcherTask(void*) int64_t diffMins = diff / 60'000LL; bool check = false; - check |= config.checkOnStartup && firstCheck; // On startup - check |= config.checkPeriodically && diffMins >= config.checkInterval; // Periodically + check |= config.checkOnStartup && firstCheck; + check |= config.checkPeriodically && diffMins >= config.checkInterval; + check |= updateRequested && (firstCheck || diffMins >= 1); if (!check) { continue; @@ -169,16 +173,46 @@ static void watcherTask(void*) continue; } - OS_LOGD(TAG, "Checking for updates"); + OpenShock::SemVer version; + if (updateRequested) { + updateRequested = false; + + if (!tryGetRequestedVersion(version)) { + OS_LOGE(TAG, "Failed to get requested version"); + continue; + } + } else { + OS_LOGD(TAG, "Checking for updates"); + + auto response = HTTP::FirmwareCDN::GetFirmwareVersion(config.updateChannel); + if (response.result != HTTP::RequestResult::Success) { + OS_LOGE(TAG, "Failed to fetch firmware version"); + continue; + } + version = response.data; + } + + std::string versionStr = version.toString(); + + if (versionStr == OPENSHOCK_FW_VERSION ""sv) { + OS_LOGI(TAG, "Requested version is already installed"); + continue; + } + + OS_LOGI(TAG, "Starting update to version: %.*s", versionStr.length(), versionStr.data()); - // Fetch current version. - auto result = HTTP::FirmwareCDN::GetFirmwareVersion(config.updateChannel); - if (result.result != HTTP::RequestResult::Success) { - OS_LOGE(TAG, "Failed to fetch firmware version"); + auto client = std::make_unique(version); + if (!client->Start()) { + OS_LOGE(TAG, "Failed to start OTA update client"); continue; } - OS_LOGD(TAG, "Remote version: %s", result.data.toString().c_str()); // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this + // Client runs in its own task and will reboot on success. + // Leak the unique_ptr intentionally — the task owns itself until reboot or failure. + (void)client.release(); + + // Wait a long time before checking again (the client will reboot on success) + vTaskDelay(pdMS_TO_TICKS(300'000)); } } @@ -188,20 +222,18 @@ bool OtaUpdateManager::Init() OS_LOGN(TAG, "Fetching current partition"); - // Fetch current partition info. const esp_partition_t* partition = esp_ota_get_running_partition(); if (partition == nullptr) { OS_PANIC(TAG, "Failed to get currently running partition"); - return false; // This will never be reached, but the compiler doesn't know that. + return false; } OS_LOGD(TAG, "Fetching partition state"); - // Get OTA state for said partition. err = esp_ota_get_state_partition(partition, &s_otaImageState); if (err != ESP_OK) { OS_PANIC(TAG, "Failed to get partition state: %s", esp_err_to_name(err)); - return false; // This will never be reached, but the compiler doesn't know that. + return false; } OS_LOGD(TAG, "Fetching previous update step"); @@ -211,12 +243,11 @@ bool OtaUpdateManager::Init() return false; } - // Infer boot type from update step. switch (updateStep) { case OtaUpdateStep::Updated: s_bootType = FirmwareBootType::NewFirmware; break; - case OtaUpdateStep::Validating: // If the update step is validating, we have failed in the middle of validating the new firmware, meaning this is a rollback. + case OtaUpdateStep::Validating: case OtaUpdateStep::RollingBack: s_bootType = FirmwareBootType::Rollback; break; @@ -227,40 +258,32 @@ bool OtaUpdateManager::Init() if (updateStep == OtaUpdateStep::Updated) { if (!Config::SetOtaUpdateStep(OtaUpdateStep::Validating)) { - OS_PANIC(TAG, "Failed to set OTA update step in critical section"); // TODO: THIS IS A CRITICAL SECTION, WHAT DO WE DO? + OS_PANIC(TAG, "Failed to set OTA update step in critical section"); } } - err = esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID, ipEventHandler, nullptr); + err = esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID, evIpEventHandler, nullptr); if (err != ESP_OK) { OS_LOGE(TAG, "Failed to register event handler for IP_EVENT: %s", esp_err_to_name(err)); return false; } - err = esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, wifiDisconnectedEventHandler, nullptr); + err = esp_event_handler_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, evWiFiDisconnectedHandler, nullptr); if (err != ESP_OK) { OS_LOGE(TAG, "Failed to register event handler for WIFI_EVENT: %s", esp_err_to_name(err)); return false; } - if (TaskUtils::TaskCreateExpensive(watcherTask, "OtaWatcherTask", 8192, nullptr, 1, &s_taskHandle) != pdPASS) { - OS_LOGE(TAG, "Failed to create OTA watcher task"); - return false; - } + TaskUtils::TaskCreateExpensive(otaUpdateTask, "OTA Update", 8192, nullptr, 1, &s_taskHandle); // PROFILED: 6.2KB stack usage return true; } bool OtaUpdateManager::TryStartFirmwareUpdate(const OpenShock::SemVer& version) { - if (version == OPENSHOCK_FW_VERSION ""sv) { - OS_LOGI(TAG, "Requested version is already installed"); - return true; - } - - OS_LOGD(TAG, "Requesting firmware version %s", version.toString().c_str()); // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this + OS_LOGD(TAG, "Requesting firmware update to version %s", version.toString().c_str()); - return tryStartUpdate(version); + return tryQueueUpdateRequest(version); } FirmwareBootType OtaUpdateManager::GetFirmwareBootType() @@ -275,9 +298,8 @@ bool OtaUpdateManager::IsValidatingApp() void OtaUpdateManager::InvalidateAndRollback() { - // Set OTA boot type in config. if (!Config::SetOtaUpdateStep(OpenShock::OtaUpdateStep::RollingBack)) { - OS_PANIC(TAG, "Failed to set OTA firmware boot type in critical section"); // TODO: THIS IS A CRITICAL SECTION, WHAT DO WE DO? + OS_PANIC(TAG, "Failed to set OTA firmware boot type in critical section"); return; } @@ -293,7 +315,6 @@ void OtaUpdateManager::InvalidateAndRollback() break; } - // Set OTA boot type in config. if (!Config::SetOtaUpdateStep(OpenShock::OtaUpdateStep::None)) { OS_LOGE(TAG, "Failed to set OTA firmware boot type"); } @@ -304,12 +325,11 @@ void OtaUpdateManager::InvalidateAndRollback() void OtaUpdateManager::ValidateApp() { if (esp_ota_mark_app_valid_cancel_rollback() != ESP_OK) { - OS_PANIC(TAG, "Unable to mark app as valid, WTF?"); // TODO: Wtf do we do here? + OS_PANIC(TAG, "Unable to mark app as valid"); } - // Set OTA boot type in config. if (!Config::SetOtaUpdateStep(OpenShock::OtaUpdateStep::Validated)) { - OS_PANIC(TAG, "Failed to set OTA firmware boot type in critical section"); // TODO: THIS IS A CRITICAL SECTION, WHAT DO WE DO? + OS_PANIC(TAG, "Failed to set OTA firmware boot type in critical section"); } s_otaImageState = ESP_OTA_IMG_VALID; From af0df4076362be3d21eca724fc5fe7dfb828b608 Mon Sep 17 00:00:00 2001 From: HeavenVR Date: Thu, 26 Mar 2026 23:24:58 +0100 Subject: [PATCH 8/9] chore: revert schemas submodule to match develop Co-Authored-By: Claude Opus 4.6 (1M context) --- schemas | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schemas b/schemas index d3dc67c2..94ee44ad 160000 --- a/schemas +++ b/schemas @@ -1 +1 @@ -Subproject commit d3dc67c2deab9000d88337b670d2fcea4ae708be +Subproject commit 94ee44ad59c0ec77506a5d17e1603666a5d2e6ff From 64bbce5dfc3111ca35c2e3b2e8471a34a5f3fc1f Mon Sep 17 00:00:00 2001 From: HeavenVR Date: Mon, 30 Mar 2026 11:06:32 +0200 Subject: [PATCH 9/9] use JSON api --- .env | 2 +- .github/actions/cdn-bump-version/action.yml | 51 ---- .github/actions/cdn-prepare/action.yml | 20 -- .../actions/cdn-upload-firmware/action.yml | 68 ----- .../cdn-upload-version-info/action.yml | 47 ---- .../actions/repo-publish-version/action.yml | 63 +++++ .github/actions/repo-upload-board/action.yml | 75 +++++ .github/workflows/ci-build.yml | 92 ++---- .../lib/components/sections/OtaSection.svelte | 12 +- frontend/src/lib/mappers/ConfigMapper.ts | 8 +- include/Common.h | 16 +- include/config/OtaUpdateConfig.h | 4 +- include/http/FirmwareCDN.h | 40 ++- include/ota/OtaUpdateClient.h | 4 +- src/captiveportal/CaptivePortalInstance.cpp | 2 +- src/config/OtaUpdateConfig.cpp | 16 +- src/http/FirmwareCDN.cpp | 266 +++++++++--------- src/ota/OtaUpdateClient.cpp | 32 +-- src/ota/OtaUpdateManager.cpp | 25 +- 19 files changed, 375 insertions(+), 468 deletions(-) delete mode 100644 .github/actions/cdn-bump-version/action.yml delete mode 100644 .github/actions/cdn-prepare/action.yml delete mode 100644 .github/actions/cdn-upload-firmware/action.yml delete mode 100644 .github/actions/cdn-upload-version-info/action.yml create mode 100644 .github/actions/repo-publish-version/action.yml create mode 100644 .github/actions/repo-upload-board/action.yml diff --git a/.env b/.env index b9c8220a..dea068ca 100644 --- a/.env +++ b/.env @@ -1,5 +1,5 @@ OPENSHOCK_API_DOMAIN=api.openshock.app -OPENSHOCK_FW_CDN_DOMAIN=firmware.openshock.org +OPENSHOCK_REPO_DOMAIN=repo.openshock.org OPENSHOCK_FW_HOSTNAME=OpenShock OPENSHOCK_FW_AP_PREFIX=OpenShock- OPENSHOCK_URI_BUFFER_SIZE=256 diff --git a/.github/actions/cdn-bump-version/action.yml b/.github/actions/cdn-bump-version/action.yml deleted file mode 100644 index 7de86cbe..00000000 --- a/.github/actions/cdn-bump-version/action.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: cdn-bump-version -description: Uploads version file to CDN -inputs: - bunny-stor-hostname: - description: Bunny SFTP Hostname - required: true - bunny-stor-username: - description: Bunny SFTP Username - required: true - bunny-stor-password: - description: Bunny SFTP Password - required: true - bunny-api-key: - description: Bunny API key - required: true - bunny-cdn-url: - description: Bunny pull zone base url e.g. https://firmware.openshock.org (no trailing slash) - required: true - version: - description: 'Version of the release' - required: true - release-channel: - description: 'Release channel that describes this upload' - required: true - -runs: - using: composite - steps: - - name: Prepare Upload Folder - shell: bash - run: | - mkdir -p upload - echo "${{ inputs.version }}" >> upload/version-${{ inputs.release-channel }}.txt - - - name: Upload version file - uses: milanmk/actions-file-deployer@master - with: - remote-protocol: "sftp" - remote-host: "${{ inputs.bunny-stor-hostname }}" - remote-user: "${{ inputs.bunny-stor-username }}" - remote-password: "${{ inputs.bunny-stor-password }}" - remote-path: "/" - local-path: "upload" - sync: "full" - - - name: Purge CDN cache - shell: bash - run: | - curl -X POST "https://api.bunny.net/purge?url=${{ inputs.bunny-cdn-url }}/version-${{ inputs.release-channel }}.txt" \ - -H "Content-Type: application/json" \ - -H "AccessKey: ${{ inputs.bunny-api-key }}" \ No newline at end of file diff --git a/.github/actions/cdn-prepare/action.yml b/.github/actions/cdn-prepare/action.yml deleted file mode 100644 index c00e3b67..00000000 --- a/.github/actions/cdn-prepare/action.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: cdn-prepare -description: Bunny sshpass and knowhosts setup -inputs: - bunny-ssh-knownhosts: - description: Bunny SFTP Hostname - required: true - -runs: - using: composite - steps: - - name: Install sshpass - shell: bash - run: sudo apt-get install -y sshpass - - - name: Configure known hosts - shell: bash - run: | - mkdir -p ~/.ssh - echo "${{ inputs.bunny-ssh-knownhosts }}" >> ~/.ssh/known_hosts - diff --git a/.github/actions/cdn-upload-firmware/action.yml b/.github/actions/cdn-upload-firmware/action.yml deleted file mode 100644 index 1d858480..00000000 --- a/.github/actions/cdn-upload-firmware/action.yml +++ /dev/null @@ -1,68 +0,0 @@ -name: cdn-upload-firmware -description: Uploads firmware partitions and merged binaries to CDN along with SHA256 checksums -inputs: - bunny-stor-hostname: - description: Bunny SFTP Hostname - required: true - bunny-stor-username: - description: Bunny SFTP Username - required: true - bunny-stor-password: - description: Bunny SFTP Password - required: true - fw-version: - description: Firmware version - required: true - board: - description: 'Board to upload' - required: true - -runs: - using: composite - steps: - - name: Download static filesystem partition - uses: actions/download-artifact@v4 - with: - name: firmware_staticfs - path: . - - - name: Download firmware partitions - uses: actions/download-artifact@v4 - with: - name: firmware_build_${{ inputs.board }} - path: . - - - name: Download merged firmware binary - uses: actions/download-artifact@v4 - with: - name: firmware_merged_${{ inputs.board }} - path: . - - - name: Rename firmware binaries - shell: bash - run: | - mv OpenShock_*.bin firmware.bin - - - name: Generate SHA256 checksums - shell: bash - run: | - find . -type f -name '*.bin' -exec md5sum {} \; > hashes.md5.txt - find . -type f -name '*.bin' -exec sha256sum {} \; > hashes.sha256.txt - - - name: Prepare Upload Folder - shell: bash - run: | - mkdir -p upload - mv *.bin upload/ - mv hashes.*.txt upload/ - - - name: Upload artifacts to CDN - uses: milanmk/actions-file-deployer@master - with: - remote-protocol: "sftp" - remote-host: "${{ inputs.bunny-stor-hostname }}" - remote-user: "${{ inputs.bunny-stor-username }}" - remote-password: "${{ inputs.bunny-stor-password }}" - remote-path: "/${{ inputs.fw-version }}/${{ inputs.board }}" - local-path: "upload" - sync: "full" \ No newline at end of file diff --git a/.github/actions/cdn-upload-version-info/action.yml b/.github/actions/cdn-upload-version-info/action.yml deleted file mode 100644 index 4813f0e5..00000000 --- a/.github/actions/cdn-upload-version-info/action.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: cdn-upload-version-info -description: Uploads version specific info to CDN -inputs: - bunny-stor-hostname: - description: Bunny SFTP Hostname - required: true - bunny-stor-username: - description: Bunny SFTP Username - required: true - bunny-stor-password: - description: Bunny SFTP Password - required: true - fw-version: - description: Firmware version - required: true - release-channel: - description: 'Release channel that describes this upload' - required: true - boards: - description: 'List of boards, separated by newlines' - required: true - -runs: - using: composite - steps: - - - name: Prepare Upload Folder - shell: bash - run: | - rm -rf upload/ - mkdir -p upload/ - - - name: Create boards.txt - shell: bash - run: | - echo -e '${{ inputs.boards }}' >> upload/boards.txt - - - name: Upload artifacts to CDN - uses: milanmk/actions-file-deployer@master - with: - remote-protocol: "sftp" - remote-host: "${{ inputs.bunny-stor-hostname }}" - remote-user: "${{ inputs.bunny-stor-username }}" - remote-password: "${{ inputs.bunny-stor-password }}" - remote-path: "/${{ inputs.fw-version }}/" - local-path: "upload" - sync: "full" \ No newline at end of file diff --git a/.github/actions/repo-publish-version/action.yml b/.github/actions/repo-publish-version/action.yml new file mode 100644 index 00000000..2ea9c07b --- /dev/null +++ b/.github/actions/repo-publish-version/action.yml @@ -0,0 +1,63 @@ +name: repo-publish-version +description: Publishes firmware version metadata to the repository server +inputs: + repo-server-url: + description: Repository server base URL + required: true + repo-admin-token: + description: Admin authentication token + required: true + fw-version: + description: Firmware semantic version + required: true + release-channel: + description: Release channel (stable, beta, develop) + required: true + commit-hash: + description: Git commit hash + required: true + release-url: + description: GitHub release URL (optional) + required: false + default: '' + +runs: + using: composite + steps: + - name: Publish version metadata to repository server + shell: bash + run: | + set -euo pipefail + + REPO_URL="${{ inputs.repo-server-url }}" + VERSION="${{ inputs.fw-version }}" + RELEASE_URL="${{ inputs.release-url }}" + + REQUEST_BODY=$(jq -n \ + --arg channel "${{ inputs.release-channel }}" \ + --arg commitHash "${{ inputs.commit-hash }}" \ + --arg releaseUrl "$RELEASE_URL" \ + '{ + "channel": $channel, + "releaseDate": (now | strftime("%Y-%m-%dT%H:%M:%SZ")), + "commitHash": $commitHash, + "releaseUrl": (if $releaseUrl == "" then null else $releaseUrl end), + "releaseNotes": [] + }') + + echo "Publishing version $VERSION to repository server..." + + HTTP_CODE=$(curl -s -o /tmp/repo-response.txt -w "%{http_code}" \ + -X PUT \ + -H "Content-Type: application/json" \ + -H "Authorization: ${{ inputs.repo-admin-token }}" \ + -d "$REQUEST_BODY" \ + "${REPO_URL}/v2/firmware/admin/versions/${VERSION}") + + if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then + echo "Successfully published version $VERSION (HTTP $HTTP_CODE)" + else + echo "Failed to publish version $VERSION (HTTP $HTTP_CODE)" + cat /tmp/repo-response.txt + exit 1 + fi diff --git a/.github/actions/repo-upload-board/action.yml b/.github/actions/repo-upload-board/action.yml new file mode 100644 index 00000000..a4dd3933 --- /dev/null +++ b/.github/actions/repo-upload-board/action.yml @@ -0,0 +1,75 @@ +name: repo-upload-board +description: Uploads firmware binaries for a single board to the repository server +inputs: + repo-server-url: + description: Repository server base URL + required: true + repo-admin-token: + description: Admin authentication token + required: true + fw-version: + description: Firmware semantic version + required: true + board: + description: Board name + required: true + +runs: + using: composite + steps: + - name: Download static filesystem partition + uses: actions/download-artifact@v4 + with: + name: firmware_staticfs + path: upload_staging + + - name: Download firmware build + uses: actions/download-artifact@v4 + with: + name: firmware_build_${{ inputs.board }} + path: upload_staging + + - name: Download merged firmware binary + uses: actions/download-artifact@v4 + with: + name: firmware_merged_${{ inputs.board }} + path: upload_staging + + - name: Rename merged binary + shell: bash + run: | + cd upload_staging + if ls OpenShock_*.bin 1>/dev/null 2>&1; then + mv OpenShock_*.bin firmware.bin + fi + + - name: Upload artifacts to repository server + shell: bash + run: | + set -euo pipefail + + REPO_URL="${{ inputs.repo-server-url }}" + CURL_ARGS=(-s -w "\n%{http_code}" \ + -X PUT \ + -H "Authorization: ${{ inputs.repo-admin-token }}" \ + "${REPO_URL}/v2/firmware/admin/versions/${{ inputs.fw-version }}/boards/${{ inputs.board }}/upload") + + # Add file fields based on what exists + [ -f upload_staging/app.bin ] && CURL_ARGS+=(-F "app=@upload_staging/app.bin") + [ -f upload_staging/staticfs.bin ] && CURL_ARGS+=(-F "staticfs=@upload_staging/staticfs.bin") + [ -f upload_staging/firmware.bin ] && CURL_ARGS+=(-F "merged=@upload_staging/firmware.bin") + [ -f upload_staging/bootloader.bin ] && CURL_ARGS+=(-F "bootloader=@upload_staging/bootloader.bin") + [ -f upload_staging/partitions.bin ] && CURL_ARGS+=(-F "partitions=@upload_staging/partitions.bin") + + RESPONSE=$(curl "${CURL_ARGS[@]}") + HTTP_CODE=$(echo "$RESPONSE" | tail -1) + BODY=$(echo "$RESPONSE" | sed '$d') + + if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then + echo "Successfully uploaded artifacts for board ${{ inputs.board }} (HTTP $HTTP_CODE)" + echo "$BODY" | jq . 2>/dev/null || echo "$BODY" + else + echo "Failed to upload artifacts for board ${{ inputs.board }} (HTTP $HTTP_CODE)" + echo "$BODY" + exit 1 + fi diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 3471682a..9514145c 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -96,42 +96,13 @@ jobs: steps: - run: echo "Builds checkpoint reached" - cdn-upload-firmware: + # Publish version metadata to the repository server (must exist before uploading board artifacts). + repo-publish-version: runs-on: ubuntu-latest needs: [getvars, checkpoint-build] - timeout-minutes: 15 - if: ${{ needs.getvars.outputs.should-deploy == 'true' }} - environment: cdn-firmware-r2 - strategy: - fail-fast: true - matrix: ${{ fromJson(needs.getvars.outputs.board-matrix) }} - - steps: - - uses: actions/checkout@v6 - with: - sparse-checkout: | - .github - - # Set up rclone for CDN uploads. - - uses: ./.github/actions/cdn-prepare - with: - bunny-ssh-knownhosts: ${{ vars.BUNNY_SSH_KNOWNHOSTS }} - - # Upload firmware to CDN. - - uses: ./.github/actions/cdn-upload-firmware - with: - bunny-stor-hostname: ${{ vars.BUNNY_STOR_HOSTNAME }} - bunny-stor-username: ${{ secrets.BUNNY_STOR_USERNAME }} - bunny-stor-password: ${{ secrets.BUNNY_STOR_PASSWORD }} - fw-version: ${{ needs.getvars.outputs.version }} - board: ${{ matrix.board }} - - cdn-upload-version-info: - runs-on: ubuntu-latest - needs: [getvars, checkpoint-build] - timeout-minutes: 10 + timeout-minutes: 5 if: ${{ needs.getvars.outputs.should-deploy == 'true' }} - environment: cdn-firmware-r2 + environment: repo-server steps: - uses: actions/checkout@v6 @@ -139,26 +110,24 @@ jobs: sparse-checkout: | .github - # Set up rclone for CDN uploads. - - uses: ./.github/actions/cdn-prepare + - uses: ./.github/actions/repo-publish-version with: - bunny-ssh-knownhosts: ${{ vars.BUNNY_SSH_KNOWNHOSTS }} - - # Upload firmware to CDN. - - uses: ./.github/actions/cdn-upload-version-info - with: - bunny-stor-hostname: ${{ vars.BUNNY_STOR_HOSTNAME }} - bunny-stor-username: ${{ secrets.BUNNY_STOR_USERNAME }} - bunny-stor-password: ${{ secrets.BUNNY_STOR_PASSWORD }} + repo-server-url: ${{ vars.REPO_SERVER_URL }} + repo-admin-token: ${{ secrets.REPO_ADMIN_TOKEN }} fw-version: ${{ needs.getvars.outputs.version }} release-channel: ${{ needs.getvars.outputs.release-channel }} - boards: ${{ needs.getvars.outputs.board-list }} + commit-hash: ${{ github.sha }} + release-url: ${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ needs.getvars.outputs.version }} - cdn-bump-version: + # Upload firmware binaries per board to the repository server (which handles CDN upload). + repo-upload-board: runs-on: ubuntu-latest - needs: [getvars, cdn-upload-firmware] # only after version is complete - timeout-minutes: 5 - environment: cdn-firmware-r2 + needs: [getvars, repo-publish-version] + timeout-minutes: 15 + environment: repo-server + strategy: + fail-fast: true + matrix: ${{ fromJson(needs.getvars.outputs.board-matrix) }} steps: - uses: actions/checkout@v6 @@ -166,32 +135,23 @@ jobs: sparse-checkout: | .github - # Set up rclone for CDN uploads. - - uses: ./.github/actions/cdn-prepare - with: - bunny-ssh-knownhosts: ${{ vars.BUNNY_SSH_KNOWNHOSTS }} - - # Upload firmware to CDN. - - uses: ./.github/actions/cdn-bump-version + - uses: ./.github/actions/repo-upload-board with: - bunny-stor-hostname: ${{ vars.BUNNY_STOR_HOSTNAME }} - bunny-stor-username: ${{ secrets.BUNNY_STOR_USERNAME }} - bunny-stor-password: ${{ secrets.BUNNY_STOR_PASSWORD }} - bunny-api-key: ${{ secrets.BUNNY_APIKEY }} - bunny-cdn-url: ${{ vars.BUNNY_CDN_URL }} - version: ${{ needs.getvars.outputs.version }} - release-channel: ${{ needs.getvars.outputs.release-channel }} + repo-server-url: ${{ vars.REPO_SERVER_URL }} + repo-admin-token: ${{ secrets.REPO_ADMIN_TOKEN }} + fw-version: ${{ needs.getvars.outputs.version }} + board: ${{ matrix.board }} - checkpoint-cdn: + checkpoint-deploy: runs-on: ubuntu-latest - needs: [cdn-upload-firmware, cdn-upload-version-info, cdn-bump-version] + needs: [repo-upload-board] timeout-minutes: 1 steps: - - run: echo "CDN checkpoint reached" + - run: echo "Deploy checkpoint reached" release: runs-on: ubuntu-latest - needs: [getvars, checkpoint-cdn] + needs: [getvars, checkpoint-deploy] timeout-minutes: 1 if: (needs.getvars.outputs.release-channel == 'stable' || needs.getvars.outputs.release-channel == 'beta') diff --git a/frontend/src/lib/components/sections/OtaSection.svelte b/frontend/src/lib/components/sections/OtaSection.svelte index 306922ef..2094b7e4 100644 --- a/frontend/src/lib/components/sections/OtaSection.svelte +++ b/frontend/src/lib/components/sections/OtaSection.svelte @@ -16,11 +16,11 @@ let otaConfig = $derived(hubState.config?.otaUpdate); - let cdnDomain = $state(''); + let repoDomain = $state(''); let checkInterval = $state(0); $effect(() => { - if (otaConfig?.cdnDomain) cdnDomain = otaConfig.cdnDomain; + if (otaConfig?.repoDomain) repoDomain = otaConfig.repoDomain; if (otaConfig?.checkInterval) checkInterval = otaConfig.checkInterval; }); @@ -29,7 +29,7 @@ } function saveDomain() { - setOtaDomain(cdnDomain); + setOtaDomain(repoDomain); } function setChannel(channel: string) { @@ -90,11 +90,11 @@ /> - +
- +
- +
diff --git a/frontend/src/lib/mappers/ConfigMapper.ts b/frontend/src/lib/mappers/ConfigMapper.ts index 31206bdf..c0ff193a 100644 --- a/frontend/src/lib/mappers/ConfigMapper.ts +++ b/frontend/src/lib/mappers/ConfigMapper.ts @@ -35,7 +35,7 @@ export interface SerialInputConfig { export interface OtaUpdateConfig { isEnabled: boolean; - cdnDomain: string; + repoDomain: string; updateChannel: OtaUpdateChannel; checkOnStartup: boolean; checkInterval: number; @@ -150,18 +150,18 @@ function mapOtaUpdateConfig(hubConfig: HubConfig): OtaUpdateConfig { if (!otaUpdate) throw new Error('hubConfig.otaUpdate is null'); const isEnabled = otaUpdate.isEnabled(); - const cdnDomain = otaUpdate.cdnDomain(); + const repoDomain = otaUpdate.repoDomain(); const updateChannel = otaUpdate.updateChannel(); const checkOnStartup = otaUpdate.checkOnStartup(); const checkInterval = otaUpdate.checkInterval(); const allowBackendManagement = otaUpdate.allowBackendManagement(); const requireManualApproval = otaUpdate.requireManualApproval(); - if (!cdnDomain) throw new Error('otaUpdate.cdnDomain is null'); + if (!repoDomain) throw new Error('otaUpdate.repoDomain is null'); return { isEnabled, - cdnDomain, + repoDomain, updateChannel, checkOnStartup, checkInterval, diff --git a/include/Common.h b/include/Common.h index b6c683b0..a79e0605 100644 --- a/include/Common.h +++ b/include/Common.h @@ -14,24 +14,14 @@ #ifndef OPENSHOCK_API_DOMAIN #error "OPENSHOCK_API_DOMAIN must be defined" #endif -#ifndef OPENSHOCK_FW_CDN_DOMAIN -#error "OPENSHOCK_FW_CDN_DOMAIN must be defined" +#ifndef OPENSHOCK_REPO_DOMAIN +#error "OPENSHOCK_REPO_DOMAIN must be defined" #endif #ifndef OPENSHOCK_FW_VERSION #error "OPENSHOCK_FW_VERSION must be defined" #endif -#define OPENSHOCK_FW_CDN_URL(path) "https://" OPENSHOCK_FW_CDN_DOMAIN path -#define OPENSHOCK_FW_CDN_CHANNEL_URL(ch) OPENSHOCK_FW_CDN_URL("/version-" ch ".txt") -#define OPENSHOCK_FW_CDN_STABLE_URL OPENSHOCK_FW_CDN_CHANNEL_URL("stable") -#define OPENSHOCK_FW_CDN_BETA_URL OPENSHOCK_FW_CDN_CHANNEL_URL("beta") -#define OPENSHOCK_FW_CDN_DEVELOP_URL OPENSHOCK_FW_CDN_CHANNEL_URL("develop") -#define OPENSHOCK_FW_CDN_BOARDS_BASE_URL_FORMAT OPENSHOCK_FW_CDN_URL("/%s") -#define OPENSHOCK_FW_CDN_BOARDS_INDEX_URL_FORMAT OPENSHOCK_FW_CDN_BOARDS_BASE_URL_FORMAT "/boards.txt" -#define OPENSHOCK_FW_CDN_VERSION_BASE_URL_FORMAT OPENSHOCK_FW_CDN_BOARDS_BASE_URL_FORMAT "/" OPENSHOCK_FW_BOARD -#define OPENSHOCK_FW_CDN_APP_URL_FORMAT OPENSHOCK_FW_CDN_VERSION_BASE_URL_FORMAT "/app.bin" -#define OPENSHOCK_FW_CDN_FILESYSTEM_URL_FORMAT OPENSHOCK_FW_CDN_VERSION_BASE_URL_FORMAT "/staticfs.bin" -#define OPENSHOCK_FW_CDN_SHA256_HASHES_URL_FORMAT OPENSHOCK_FW_CDN_VERSION_BASE_URL_FORMAT "/hashes.sha256.txt" +#define OPENSHOCK_REPO_URL(path) "https://" OPENSHOCK_REPO_DOMAIN path #define OPENSHOCK_GPIO_INVALID -1 diff --git a/include/config/OtaUpdateConfig.h b/include/config/OtaUpdateConfig.h index 1f67bcb1..eab9b396 100644 --- a/include/config/OtaUpdateConfig.h +++ b/include/config/OtaUpdateConfig.h @@ -11,11 +11,11 @@ namespace OpenShock::Config { struct OtaUpdateConfig : public ConfigBase { OtaUpdateConfig(); OtaUpdateConfig( - bool isEnabled, std::string cdnDomain, OtaUpdateChannel updateChannel, bool checkOnStartup, bool checkPeriodically, uint16_t checkInterval, bool allowBackendManagement, bool requireManualApproval, int32_t updateId, OtaUpdateStep updateStep + bool isEnabled, std::string repoDomain, OtaUpdateChannel updateChannel, bool checkOnStartup, bool checkPeriodically, uint16_t checkInterval, bool allowBackendManagement, bool requireManualApproval, int32_t updateId, OtaUpdateStep updateStep ); bool isEnabled; - std::string cdnDomain; + std::string repoDomain; OtaUpdateChannel updateChannel; bool checkOnStartup; bool checkPeriodically; diff --git a/include/http/FirmwareCDN.h b/include/http/FirmwareCDN.h index 2634bbe2..f37c1e35 100644 --- a/include/http/FirmwareCDN.h +++ b/include/http/FirmwareCDN.h @@ -1,35 +1,29 @@ #pragma once #include "http/HTTPRequestManager.h" -#include "ota/FirmwareBinaryHash.h" #include "ota/FirmwareReleaseInfo.h" #include "ota/OtaUpdateChannel.h" #include "SemVer.h" -#include +#include namespace OpenShock::HTTP::FirmwareCDN { - /// @brief Fetches the firmware version for the given channel from the firmware CDN. - /// Valid response codes: 200, 304 - /// @param channel The channel to fetch the firmware version for. - /// @return The firmware version or an error response. - HTTP::Response GetFirmwareVersion(OtaUpdateChannel channel); + struct LatestRelease { + OpenShock::SemVer version; + FirmwareReleaseInfo release; + }; - /// @brief Fetches the list of available boards for the given firmware version from the firmware CDN. - /// Valid response codes: 200, 304 - /// @param version The firmware version to fetch the boards for. - /// @return The list of available boards or an error response. - HTTP::Response> GetFirmwareBoards(const OpenShock::SemVer& version); + /// @brief Fetches the latest firmware release for the given channel and board from the repository server. + /// Makes a single request to /v2/firmware/latest/{channel}?board={board} and parses the JSON response. + /// @param channel The update channel (stable, beta, develop). + /// @param repoDomain The repository server domain. + /// @return The latest firmware version and release info, or an error response. + HTTP::Response GetLatestRelease(OtaUpdateChannel channel, const std::string& repoDomain); - /// @brief Fetches the binary hashes for the given firmware version from the firmware CDN. - /// Valid response codes: 200, 304 - /// @param version The firmware version to fetch the binary hashes for. - /// @return The binary hashes or an error response. - HTTP::Response> GetFirmwareBinaryHashes(const OpenShock::SemVer& version); - - /// @brief Fetches the firmware release information for the given firmware version from the firmware CDN. - /// Valid response codes: 200, 304 - /// @param version The firmware version to fetch the release information for. - /// @return The firmware release information or an error response. - HTTP::Response GetFirmwareReleaseInfo(const OpenShock::SemVer& version); + /// @brief Fetches a specific firmware version's release info from the repository server. + /// Makes a request to /v2/firmware/versions/{version}?board={board} and parses the JSON response. + /// @param version The specific firmware version to fetch. + /// @param repoDomain The repository server domain. + /// @return The firmware release info, or an error response. + HTTP::Response GetRelease(const OpenShock::SemVer& version, const std::string& repoDomain); } // namespace OpenShock::HTTP::FirmwareCDN diff --git a/include/ota/OtaUpdateClient.h b/include/ota/OtaUpdateClient.h index 10ccd891..54db622b 100644 --- a/include/ota/OtaUpdateClient.h +++ b/include/ota/OtaUpdateClient.h @@ -1,5 +1,6 @@ #pragma once +#include "ota/FirmwareReleaseInfo.h" #include "SemVer.h" #include @@ -7,7 +8,7 @@ namespace OpenShock { class OtaUpdateClient { public: - OtaUpdateClient(const OpenShock::SemVer& version); + OtaUpdateClient(const OpenShock::SemVer& version, const FirmwareReleaseInfo& release); ~OtaUpdateClient(); bool Start(); @@ -16,6 +17,7 @@ namespace OpenShock { void task(); OpenShock::SemVer m_version; + FirmwareReleaseInfo m_release; TaskHandle_t m_taskHandle; }; } // namespace OpenShock diff --git a/src/captiveportal/CaptivePortalInstance.cpp b/src/captiveportal/CaptivePortalInstance.cpp index c9c20f4e..3ff736a6 100644 --- a/src/captiveportal/CaptivePortalInstance.cpp +++ b/src/captiveportal/CaptivePortalInstance.cpp @@ -379,7 +379,7 @@ CaptivePortal::CaptivePortalInstance::CaptivePortalInstance() request->send(500, HTTP::ContentType::JSON, JSON_ERR_INTERNAL); return; } - cfg.cdnDomain = std::string(domain.c_str(), domain.length()); + cfg.repoDomain = std::string(domain.c_str(), domain.length()); if (!Config::SetOtaUpdateConfig(cfg)) { request->send(500, HTTP::ContentType::JSON, JSON_ERR_INTERNAL); return; diff --git a/src/config/OtaUpdateConfig.cpp b/src/config/OtaUpdateConfig.cpp index 4ae0735a..0a406f3f 100644 --- a/src/config/OtaUpdateConfig.cpp +++ b/src/config/OtaUpdateConfig.cpp @@ -9,7 +9,7 @@ using namespace OpenShock::Config; OtaUpdateConfig::OtaUpdateConfig() : isEnabled(true) - , cdnDomain(OPENSHOCK_FW_CDN_DOMAIN) + , repoDomain(OPENSHOCK_REPO_DOMAIN) , updateChannel(OtaUpdateChannel::Stable) , checkOnStartup(false) , checkPeriodically(false) @@ -22,10 +22,10 @@ OtaUpdateConfig::OtaUpdateConfig() } OtaUpdateConfig::OtaUpdateConfig( - bool isEnabled, std::string cdnDomain, OtaUpdateChannel updateChannel, bool checkOnStartup, bool checkPeriodically, uint16_t checkInterval, bool allowBackendManagement, bool requireManualApproval, int32_t updateId, OtaUpdateStep updateStep + bool isEnabled, std::string repoDomain, OtaUpdateChannel updateChannel, bool checkOnStartup, bool checkPeriodically, uint16_t checkInterval, bool allowBackendManagement, bool requireManualApproval, int32_t updateId, OtaUpdateStep updateStep ) : isEnabled(isEnabled) - , cdnDomain(std::move(cdnDomain)) + , repoDomain(std::move(repoDomain)) , updateChannel(updateChannel) , checkOnStartup(checkOnStartup) , checkPeriodically(checkPeriodically) @@ -40,7 +40,7 @@ OtaUpdateConfig::OtaUpdateConfig( void OtaUpdateConfig::ToDefault() { isEnabled = true; - cdnDomain = OPENSHOCK_FW_CDN_DOMAIN; + repoDomain = OPENSHOCK_REPO_DOMAIN; updateChannel = OtaUpdateChannel::Stable; checkOnStartup = false; checkPeriodically = false; @@ -60,7 +60,7 @@ bool OtaUpdateConfig::FromFlatbuffers(const Serialization::Configuration::OtaUpd } isEnabled = config->is_enabled(); - Internal::Utils::FromFbsStr(cdnDomain, config->cdn_domain(), OPENSHOCK_FW_CDN_DOMAIN); + Internal::Utils::FromFbsStr(repoDomain, config->repo_domain(), OPENSHOCK_REPO_DOMAIN); updateChannel = config->update_channel(); checkOnStartup = config->check_on_startup(); checkPeriodically = config->check_periodically(); @@ -75,7 +75,7 @@ bool OtaUpdateConfig::FromFlatbuffers(const Serialization::Configuration::OtaUpd flatbuffers::Offset OtaUpdateConfig::ToFlatbuffers(flatbuffers::FlatBufferBuilder& builder, bool withSensitiveData) const { - return Serialization::Configuration::CreateOtaUpdateConfig(builder, isEnabled, builder.CreateString(cdnDomain), updateChannel, checkOnStartup, checkPeriodically, checkInterval, allowBackendManagement, requireManualApproval, updateId, updateStep); + return Serialization::Configuration::CreateOtaUpdateConfig(builder, isEnabled, builder.CreateString(repoDomain), updateChannel, checkOnStartup, checkPeriodically, checkInterval, allowBackendManagement, requireManualApproval, updateId, updateStep); } bool OtaUpdateConfig::FromJSON(const cJSON* json) @@ -92,7 +92,7 @@ bool OtaUpdateConfig::FromJSON(const cJSON* json) } Internal::Utils::FromJsonBool(isEnabled, json, "isEnabled", true); - Internal::Utils::FromJsonStr(cdnDomain, json, "cdnDomain", OPENSHOCK_FW_CDN_DOMAIN); + Internal::Utils::FromJsonStr(repoDomain, json, "repoDomain", OPENSHOCK_REPO_DOMAIN); Internal::Utils::FromJsonStrParsed(updateChannel, json, "updateChannel", OpenShock::TryParseOtaUpdateChannel, OpenShock::OtaUpdateChannel::Stable); Internal::Utils::FromJsonBool(checkOnStartup, json, "checkOnStartup", false); Internal::Utils::FromJsonBool(checkPeriodically, json, "checkPeriodically", false); @@ -110,7 +110,7 @@ cJSON* OtaUpdateConfig::ToJSON(bool withSensitiveData) const cJSON* root = cJSON_CreateObject(); cJSON_AddBoolToObject(root, "isEnabled", isEnabled); - cJSON_AddStringToObject(root, "cdnDomain", cdnDomain.c_str()); + cJSON_AddStringToObject(root, "repoDomain", repoDomain.c_str()); cJSON_AddStringToObject(root, "updateChannel", OpenShock::Serialization::Configuration::EnumNameOtaUpdateChannel(updateChannel)); cJSON_AddBoolToObject(root, "checkOnStartup", checkOnStartup); cJSON_AddBoolToObject(root, "checkPeriodically", checkPeriodically); diff --git a/src/http/FirmwareCDN.cpp b/src/http/FirmwareCDN.cpp index a74c714f..8834c727 100644 --- a/src/http/FirmwareCDN.cpp +++ b/src/http/FirmwareCDN.cpp @@ -6,7 +6,8 @@ #include "http/ContentTypes.h" #include "Logging.h" #include "util/HexUtils.h" -#include "util/StringUtils.h" + +#include const char* const TAG = "FirmwareCDN"; @@ -14,178 +15,185 @@ using namespace std::string_view_literals; using namespace OpenShock; -static const uint16_t s_acceptedCodes[] = {200, 304}; +static bool parseLatestResponse(const std::string& jsonStr, OpenShock::SemVer& version, FirmwareReleaseInfo& release) +{ + cJSON* root = cJSON_Parse(jsonStr.c_str()); + if (root == nullptr) { + OS_LOGE(TAG, "Failed to parse JSON response"); + return false; + } + + // Extract version string. + cJSON* versionObj = cJSON_GetObjectItemCaseSensitive(root, "version"); + if (!cJSON_IsString(versionObj) || versionObj->valuestring == nullptr) { + OS_LOGE(TAG, "Missing or invalid 'version' field in response"); + cJSON_Delete(root); + return false; + } + + if (!OpenShock::TryParseSemVer(versionObj->valuestring, version)) { + OS_LOGE(TAG, "Failed to parse version string: %s", versionObj->valuestring); + cJSON_Delete(root); + return false; + } + + // Extract artifacts for our board. + cJSON* artifacts = cJSON_GetObjectItemCaseSensitive(root, "artifacts"); + if (!cJSON_IsObject(artifacts)) { + OS_LOGE(TAG, "Missing or invalid 'artifacts' field in response"); + cJSON_Delete(root); + return false; + } + + cJSON* boardArtifacts = cJSON_GetObjectItemCaseSensitive(artifacts, OPENSHOCK_FW_BOARD); + if (!cJSON_IsArray(boardArtifacts)) { + OS_LOGE(TAG, "No artifacts found for board '%s'", OPENSHOCK_FW_BOARD); + cJSON_Delete(root); + return false; + } + + bool foundApp = false, foundStaticFs = false; + cJSON* artifact = nullptr; + cJSON_ArrayForEach(artifact, boardArtifacts) + { + if (!cJSON_IsObject(artifact)) { + continue; + } + + cJSON* typeObj = cJSON_GetObjectItemCaseSensitive(artifact, "type"); + cJSON* urlObj = cJSON_GetObjectItemCaseSensitive(artifact, "url"); + cJSON* hashObj = cJSON_GetObjectItemCaseSensitive(artifact, "sha256Hash"); + + if (!cJSON_IsString(typeObj) || !cJSON_IsString(urlObj) || !cJSON_IsString(hashObj)) { + continue; + } + + std::string_view type = typeObj->valuestring; + const char* url = urlObj->valuestring; + const char* hash = hashObj->valuestring; + + if (type == "app"sv) { + release.appBinaryUrl = url; + if (HexUtils::TryParseHex(hash, 64, release.appBinaryHash, 32) != 32) { + OS_LOGE(TAG, "Failed to parse app binary hash"); + cJSON_Delete(root); + return false; + } + foundApp = true; + } else if (type == "staticfs"sv) { + release.filesystemBinaryUrl = url; + if (HexUtils::TryParseHex(hash, 64, release.filesystemBinaryHash, 32) != 32) { + OS_LOGE(TAG, "Failed to parse filesystem binary hash"); + cJSON_Delete(root); + return false; + } + foundStaticFs = true; + } + } + + cJSON_Delete(root); -HTTP::Response HTTP::FirmwareCDN::GetFirmwareVersion(OtaUpdateChannel channel) + if (!foundApp) { + OS_LOGE(TAG, "No 'app' artifact found for board '%s'", OPENSHOCK_FW_BOARD); + return false; + } + + if (!foundStaticFs) { + OS_LOGE(TAG, "No 'staticfs' artifact found for board '%s'", OPENSHOCK_FW_BOARD); + return false; + } + + return true; +} + +HTTP::Response HTTP::FirmwareCDN::GetLatestRelease(OtaUpdateChannel channel, const std::string& repoDomain) { - std::string_view channelIndexUrl; + const char* channelStr; switch (channel) { case OtaUpdateChannel::Stable: - channelIndexUrl = OPENSHOCK_FW_CDN_STABLE_URL ""sv; + channelStr = "stable"; break; case OtaUpdateChannel::Beta: - channelIndexUrl = OPENSHOCK_FW_CDN_BETA_URL ""sv; + channelStr = "beta"; break; case OtaUpdateChannel::Develop: - channelIndexUrl = OPENSHOCK_FW_CDN_DEVELOP_URL ""sv; + channelStr = "develop"; break; default: OS_LOGE(TAG, "Unknown channel: %u", channel); return {RequestResult::InternalError, 0, {}}; } - OS_LOGD(TAG, "Fetching firmware version from %.*s", channelIndexUrl.size(), channelIndexUrl.data()); + // Build URL: https://{repoDomain}/v2/firmware/latest/{channel}?board={board} + std::string url = "https://"; + url += repoDomain; + url += "/v2/firmware/latest/"; + url += channelStr; + url += "?board="; + url += OPENSHOCK_FW_BOARD; + + OS_LOGD(TAG, "Fetching latest firmware from %s", url.c_str()); + + static const uint16_t s_acceptedCodes[] = {200, 304}; auto response = OpenShock::HTTP::GetString( - channelIndexUrl, + url, { - {"Accept", HTTP::ContentType::TextPlain} + {"Accept", HTTP::ContentType::JSON} }, s_acceptedCodes ); if (response.result != OpenShock::HTTP::RequestResult::Success) { - OS_LOGE(TAG, "Failed to fetch firmware version: [%u] %s", response.code, response.data.c_str()); + OS_LOGE(TAG, "Failed to fetch latest firmware: [%u] %s", response.code, response.data.c_str()); return {RequestResult::InternalError, 0, {}}; } - OpenShock::SemVer version; - if (!OpenShock::TryParseSemVer(response.data, version)) { - OS_LOGE(TAG, "Failed to parse firmware version: %.*s", response.data.size(), response.data.data()); + LatestRelease latest; + if (!parseLatestResponse(response.data, latest.version, latest.release)) { + OS_LOGE(TAG, "Failed to parse firmware release response"); return {RequestResult::ParseFailed, response.code, {}}; } - return {response.result, response.code, version}; + return {response.result, response.code, std::move(latest)}; } -HTTP::Response> HTTP::FirmwareCDN::GetFirmwareBoards(const OpenShock::SemVer& version) +HTTP::Response HTTP::FirmwareCDN::GetRelease(const OpenShock::SemVer& version, const std::string& repoDomain) { - std::string channelIndexUrl; - if (!FormatToString(channelIndexUrl, OPENSHOCK_FW_CDN_BOARDS_INDEX_URL_FORMAT, version.toString().c_str())) { - OS_LOGE(TAG, "Failed to format URL"); - return {RequestResult::InternalError, 0, {}}; - } + std::string versionStr = version.toString(); + + // Build URL: https://{repoDomain}/v2/firmware/versions/{version}?board={board} + std::string url = "https://"; + url += repoDomain; + url += "/v2/firmware/versions/"; + url += versionStr; + url += "?board="; + url += OPENSHOCK_FW_BOARD; - OS_LOGD(TAG, "Fetching firmware boards from %s", channelIndexUrl.c_str()); + OS_LOGD(TAG, "Fetching firmware release %s from %s", versionStr.c_str(), url.c_str()); + + static const uint16_t s_acceptedCodes[] = {200, 304}; auto response = OpenShock::HTTP::GetString( - channelIndexUrl, + url, { - {"Accept", HTTP::ContentType::TextPlain} + {"Accept", HTTP::ContentType::JSON} }, s_acceptedCodes ); if (response.result != OpenShock::HTTP::RequestResult::Success) { - OS_LOGE(TAG, "Failed to fetch firmware boards: [%u] %s", response.code, response.data.c_str()); - return {RequestResult::InternalError, 0, {}}; - } - - std::vector lines = OpenShock::StringSplitNewLines(response.data); - - std::vector boards; - boards.reserve(lines.size()); - - for (std::string_view line : lines) { - line = OpenShock::StringTrim(line); - - if (line.empty()) { - continue; - } - - boards.push_back(std::string(line)); - } - - return {response.result, response.code, std::move(boards)}; -} - -HTTP::Response> HTTP::FirmwareCDN::GetFirmwareBinaryHashes(const OpenShock::SemVer& version) -{ - auto versionStr = version.toString(); - - std::string sha256HashesUrl; - if (!FormatToString(sha256HashesUrl, OPENSHOCK_FW_CDN_SHA256_HASHES_URL_FORMAT, versionStr.c_str())) { - OS_LOGE(TAG, "Failed to format URL"); - return {RequestResult::InternalError, 0, {}}; - } - - auto sha256HashesResponse = OpenShock::HTTP::GetString( - sha256HashesUrl, - { - {"Accept", HTTP::ContentType::TextPlain} - }, - s_acceptedCodes - ); - if (sha256HashesResponse.result != OpenShock::HTTP::RequestResult::Success) { - OS_LOGE(TAG, "Failed to fetch hashes: [%u] %s", sha256HashesResponse.code, sha256HashesResponse.data.c_str()); + OS_LOGE(TAG, "Failed to fetch firmware release: [%u] %s", response.code, response.data.c_str()); return {RequestResult::InternalError, 0, {}}; } - std::vector hashesLines = OpenShock::StringSplitNewLines(sha256HashesResponse.data); - - std::vector hashes; - for (std::string_view line : hashesLines) { - std::vector parts = OpenShock::StringSplitWhiteSpace(line); - if (parts.size() != 2) { - OS_LOGE(TAG, "Invalid hashes entry: %.*s", line.size(), line.data()); - return {RequestResult::InternalError, 0, {}}; - } - - auto hash = OpenShock::StringTrim(parts[0]); - auto file = OpenShock::StringTrim(parts[1]); - - file = OpenShock::StringRemovePrefix(file, "./"sv); - - if (hash.size() != 64) { - OS_LOGE(TAG, "Invalid hash: %.*s", hash.size(), hash.data()); - return {RequestResult::InternalError, 0, {}}; - } - - FirmwareBinaryHash binaryHash; - - if (!HexUtils::TryParseHex(hash.data(), hash.size(), binaryHash.hash, sizeof(binaryHash.hash))) { - OS_LOGE(TAG, "Failed to parse hash: %.*s", hash.size(), hash.data()); - return {RequestResult::InternalError, 0, {}}; - } - - binaryHash.name = std::string(file); - - hashes.push_back(std::move(binaryHash)); - } - - return {RequestResult::Success, 200, std::move(hashes)}; -} - -HTTP::Response HTTP::FirmwareCDN::GetFirmwareReleaseInfo(const OpenShock::SemVer& version) -{ - auto versionStr = version.toString(); - + // Reuse the same parser — the response format includes version + artifacts. + OpenShock::SemVer parsedVersion; FirmwareReleaseInfo release; - if (!FormatToString(release.appBinaryUrl, OPENSHOCK_FW_CDN_APP_URL_FORMAT, versionStr.c_str())) { - OS_LOGE(TAG, "Failed to format URL"); - return {RequestResult::InternalError, 0, {}}; - } - - if (!FormatToString(release.filesystemBinaryUrl, OPENSHOCK_FW_CDN_FILESYSTEM_URL_FORMAT, versionStr.c_str())) { - OS_LOGE(TAG, "Failed to format URL"); - return {RequestResult::InternalError, 0, {}}; - } - - auto response = GetFirmwareBinaryHashes(version); - if (response.result != HTTP::RequestResult::Success) { - OS_LOGE(TAG, "Failed to fetch hashes: [%u]", response.code); - return {response.result, response.code, {}}; - } - - for (const auto& binaryHash : response.data) { - if (binaryHash.name == "app.bin") { - static_assert(sizeof(release.appBinaryHash) == sizeof(binaryHash.hash), "Hash size mismatch"); - memcpy(release.appBinaryHash, binaryHash.hash, sizeof(release.appBinaryHash)); - } else if (binaryHash.name == "staticfs.bin") { - static_assert(sizeof(release.filesystemBinaryHash) == sizeof(binaryHash.hash), "Hash size mismatch"); - memcpy(release.filesystemBinaryHash, binaryHash.hash, sizeof(release.filesystemBinaryHash)); - } + if (!parseLatestResponse(response.data, parsedVersion, release)) { + OS_LOGE(TAG, "Failed to parse firmware release response"); + return {RequestResult::ParseFailed, response.code, {}}; } - return {response.result, response.code, release}; + return {response.result, response.code, std::move(release)}; } diff --git a/src/ota/OtaUpdateClient.cpp b/src/ota/OtaUpdateClient.cpp index 57e9305c..143b60d6 100644 --- a/src/ota/OtaUpdateClient.cpp +++ b/src/ota/OtaUpdateClient.cpp @@ -7,7 +7,6 @@ const char* const TAG = "OtaUpdateClient"; #include "captiveportal/Manager.h" #include "config/Config.h" #include "GatewayConnectionManager.h" -#include "http/FirmwareCDN.h" #include "Logging.h" #include "ota/OtaUpdateStep.h" #include "serialization/WSGateway.h" @@ -139,8 +138,9 @@ static bool flashFilesystemPartition(const esp_partition_t* partition, std::stri return true; } -OtaUpdateClient::OtaUpdateClient(const OpenShock::SemVer& version) +OtaUpdateClient::OtaUpdateClient(const OpenShock::SemVer& version, const FirmwareReleaseInfo& release) : m_version(version) + , m_release(release) , m_taskHandle(nullptr) { } @@ -192,28 +192,12 @@ void OtaUpdateClient::task() return; } - if (!sendProgressMessage(Serialization::Types::OtaUpdateProgressTask::FetchingMetadata, 0.0f)) { - vTaskDelete(nullptr); - return; - } - - // Fetch release info from CDN. - auto response = HTTP::FirmwareCDN::GetFirmwareReleaseInfo(m_version); - if (response.result != HTTP::RequestResult::Success) { - OS_LOGE(TAG, "Failed to fetch firmware release info"); - sendFailureMessage("Failed to fetch firmware release info"sv); - vTaskDelete(nullptr); - return; - } - - auto& release = response.data; - OS_LOGD(TAG, "Firmware release:"); OS_LOGD(TAG, " Version: %.*s", versionStr.length(), versionStr.data()); - OS_LOGD(TAG, " App binary URL: %.*s", release.appBinaryUrl.length(), release.appBinaryUrl.data()); - OS_LOGD(TAG, " App binary hash: %s", HexUtils::ToHex<32>(release.appBinaryHash).data()); - OS_LOGD(TAG, " Filesystem binary URL: %.*s", release.filesystemBinaryUrl.length(), release.filesystemBinaryUrl.data()); - OS_LOGD(TAG, " Filesystem binary hash: %s", HexUtils::ToHex<32>(release.filesystemBinaryHash).data()); + OS_LOGD(TAG, " App binary URL: %.*s", m_release.appBinaryUrl.length(), m_release.appBinaryUrl.data()); + OS_LOGD(TAG, " App binary hash: %s", HexUtils::ToHex<32>(m_release.appBinaryHash).data()); + OS_LOGD(TAG, " Filesystem binary URL: %.*s", m_release.filesystemBinaryUrl.length(), m_release.filesystemBinaryUrl.data()); + OS_LOGD(TAG, " Filesystem binary hash: %s", HexUtils::ToHex<32>(m_release.filesystemBinaryHash).data()); // Get available app update partition. const esp_partition_t* appPartition = esp_ota_get_next_update_partition(nullptr); @@ -237,12 +221,12 @@ void OtaUpdateClient::task() esp_task_wdt_init(15, true); // Flash filesystem first, then app. - if (!flashFilesystemPartition(filesystemPartition, release.filesystemBinaryUrl, release.filesystemBinaryHash)) { + if (!flashFilesystemPartition(filesystemPartition, m_release.filesystemBinaryUrl, m_release.filesystemBinaryHash)) { esp_task_wdt_init(5, true); vTaskDelete(nullptr); return; } - if (!flashAppPartition(appPartition, release.appBinaryUrl, release.appBinaryHash)) { + if (!flashAppPartition(appPartition, m_release.appBinaryUrl, m_release.appBinaryHash)) { esp_task_wdt_init(5, true); vTaskDelete(nullptr); return; diff --git a/src/ota/OtaUpdateManager.cpp b/src/ota/OtaUpdateManager.cpp index 252f1aac..179b8810 100644 --- a/src/ota/OtaUpdateManager.cpp +++ b/src/ota/OtaUpdateManager.cpp @@ -174,6 +174,9 @@ static void otaUpdateTask(void* pvParameters) } OpenShock::SemVer version; + FirmwareReleaseInfo release; + bool haveRelease = false; + if (updateRequested) { updateRequested = false; @@ -181,15 +184,19 @@ static void otaUpdateTask(void* pvParameters) OS_LOGE(TAG, "Failed to get requested version"); continue; } + // Release info will be fetched by version below. } else { OS_LOGD(TAG, "Checking for updates"); - auto response = HTTP::FirmwareCDN::GetFirmwareVersion(config.updateChannel); + // Fetch latest version + release from the repository server. + auto response = HTTP::FirmwareCDN::GetLatestRelease(config.updateChannel, config.repoDomain); if (response.result != HTTP::RequestResult::Success) { - OS_LOGE(TAG, "Failed to fetch firmware version"); + OS_LOGE(TAG, "Failed to fetch latest firmware release"); continue; } - version = response.data; + version = response.data.version; + release = response.data.release; + haveRelease = true; } std::string versionStr = version.toString(); @@ -201,7 +208,17 @@ static void otaUpdateTask(void* pvParameters) OS_LOGI(TAG, "Starting update to version: %.*s", versionStr.length(), versionStr.data()); - auto client = std::make_unique(version); + // If we don't have the release info yet (requested version update), fetch it by version. + if (!haveRelease) { + auto response = HTTP::FirmwareCDN::GetRelease(version, config.repoDomain); + if (response.result != HTTP::RequestResult::Success) { + OS_LOGE(TAG, "Failed to fetch firmware release for version %s", versionStr.c_str()); + continue; + } + release = response.data; + } + + auto client = std::make_unique(version, release); if (!client->Start()) { OS_LOGE(TAG, "Failed to start OTA update client"); continue;