diff --git a/include/Common.h b/include/Common.h index 23c4d0e8..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,7 +20,17 @@ #error "OPENSHOCK_FW_VERSION must be defined" #endif -#define OPENSHOCK_FW_CDN_URL(path) "https://" OPENSHOCK_FW_CDN_DOMAIN path +#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 @@ -40,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 3bcec5ef..1f67bcb1 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 @@ -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/http/FirmwareCDN.h b/include/http/FirmwareCDN.h new file mode 100644 index 00000000..2634bbe2 --- /dev/null +++ b/include/http/FirmwareCDN.h @@ -0,0 +1,35 @@ +#pragma once + +#include "http/HTTPRequestManager.h" +#include "ota/FirmwareBinaryHash.h" +#include "ota/FirmwareReleaseInfo.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); + + /// @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/FirmwareBinaryHash.h b/include/ota/FirmwareBinaryHash.h new file mode 100644 index 00000000..1b4b642f --- /dev/null +++ b/include/ota/FirmwareBinaryHash.h @@ -0,0 +1,11 @@ +#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..febd8039 --- /dev/null +++ b/include/ota/FirmwareReleaseInfo.h @@ -0,0 +1,13 @@ +#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 96% rename from include/OtaUpdateChannel.h rename to include/ota/OtaUpdateChannel.h index e0bbf2fa..3ab942b3 100644 --- a/include/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 new file mode 100644 index 00000000..ed985b40 --- /dev/null +++ b/include/ota/OtaUpdateClient.h @@ -0,0 +1,21 @@ +#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; + }; +} // namespace OpenShock diff --git a/include/OtaUpdateManager.h b/include/ota/OtaUpdateManager.h similarity index 51% rename from include/OtaUpdateManager.h rename to include/ota/OtaUpdateManager.h index 2d319488..cf532d48 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,17 +13,6 @@ 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 TryStartFirmwareUpdate(const OpenShock::SemVer& version); FirmwareBootType GetFirmwareBootType(); diff --git a/include/OtaUpdateStep.h b/include/ota/OtaUpdateStep.h similarity index 98% rename from include/OtaUpdateStep.h rename to include/ota/OtaUpdateStep.h index 28a9dae7..8fae996f 100644 --- a/include/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/GatewayClient.cpp b/src/GatewayClient.cpp index a98267f7..8dfb39c9 100644 --- a/src/GatewayClient.cpp +++ b/src/GatewayClient.cpp @@ -7,7 +7,7 @@ const char* const TAG = "GatewayClient"; #include "events/Events.h" #include "Logging.h" #include "message_handlers/WebSocket.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 2e8b5680..00000000 --- a/src/OtaUpdateManager.cpp +++ /dev/null @@ -1,736 +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 "SimpleMutex.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; -} - -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 OpenShock::SimpleMutex _requestedVersionMutex = {}; - -using namespace OpenShock; - -static bool _tryQueueUpdateRequest(const OpenShock::SemVer& version) -{ - if (!_requestedVersionMutex.lock(pdMS_TO_TICKS(1000))) { - OS_LOGE(TAG, "Failed to take requested version mutex"); - return false; - } - - _requestedVersion = version; - - _requestedVersionMutex.unlock(); - - xTaskNotify(_taskHandle, OTA_TASK_EVENT_UPDATE_REQUESTED, eSetBits); - - return true; -} - -static bool _tryGetRequestedVersion(OpenShock::SemVer& version) -{ - if (!_requestedVersionMutex.lock(pdMS_TO_TICKS(1000))) { - OS_LOGE(TAG, "Failed to take requested version mutex"); - return false; - } - - version = _requestedVersion; - - _requestedVersionMutex.unlock(); - - return true; -} - -static void _otaEvWiFiDisconnectedHandler(void* event_handler_arg, esp_event_base_t event_base, int32_t event_id, void* event_data) -{ - (void)event_handler_arg; - (void)event_base; - (void)event_id; - (void)event_data; - - xTaskNotify(_taskHandle, OTA_TASK_EVENT_WIFI_DISCONNECTED, eSetBits); -} - -static void _otaEvIpEventHandler(void* event_handler_arg, esp_event_base_t event_base, int32_t event_id, void* event_data) -{ - (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(_taskHandle, OTA_TASK_EVENT_WIFI_CONNECTED, eSetBits); - break; - case IP_EVENT_STA_LOST_IP: - xTaskNotify(_taskHandle, OTA_TASK_EVENT_WIFI_DISCONNECTED, eSetBits); - break; - default: - return; - } -} - -static bool _sendProgressMessage(Serialization::Types::OtaUpdateProgressTask task, float progress) -{ - int32_t updateId; - if (!Config::GetOtaUpdateId(updateId)) { - OS_LOGE(TAG, "Failed to get OTA update ID"); - return false; - } - - if (!Serialization::Gateway::SerializeOtaUpdateProgressMessage(updateId, task, progress, GatewayConnectionManager::SendMessageBIN)) { - OS_LOGE(TAG, "Failed to send OTA install progress message"); - return false; - } - - return true; -} -static 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::SerializeOtaUpdateFailedMessage(updateId, message, fatal, GatewayConnectionManager::SendMessageBIN)) { - OS_LOGE(TAG, "Failed to send OTA install failed message"); - return false; - } - - return true; -} - -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::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::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); - return false; - } - - 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); - return false; - } - - return true; -} - -static bool _flashFilesystemPartition(const esp_partition_t* parition, std::string_view remoteUrl, const uint8_t (&remoteHash)[32]) -{ - 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); - return false; - } - - OS_LOGD(TAG, "Flashing filesystem partition"); - - 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::Types::OtaUpdateProgressTask::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::Types::OtaUpdateProgressTask::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(); - - return true; -} - -static 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::SerializeOtaUpdateStartedMessage(updateId, version, GatewayConnectionManager::SendMessageBIN)) { - OS_LOGE(TAG, "Failed to serialize OTA update started message"); - continue; - } - - if (!_sendProgressMessage(Serialization::Types::OtaUpdateProgressTask::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::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(); -} - -static 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() -{ - esp_err_t err; - - 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. - 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? - } - } - - err = esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID, _otaEvIpEventHandler, 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, _otaEvWiFiDisconnectedHandler, nullptr); - if (err != ESP_OK) { - OS_LOGE(TAG, "Failed to register event handler for WIFI_EVENT: %s", esp_err_to_name(err)); - return false; - } - - // 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 = 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 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; -} - -static 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::TryStartFirmwareUpdate(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/http/FirmwareCDN.cpp b/src/http/FirmwareCDN.cpp new file mode 100644 index 00000000..04c0f4bd --- /dev/null +++ b/src/http/FirmwareCDN.cpp @@ -0,0 +1,194 @@ +#include + +#include "http/FirmwareCDN.h" + +#include "Common.h" +#include "Logging.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) +{ + 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)}; +} + +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/main.cpp b/src/main.cpp index aa187ee6..3af1f68a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,7 +10,7 @@ const char* const TAG = "main"; #include "events/Events.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/message_handlers/websocket/gateway/OtaUpdateRequest.cpp b/src/message_handlers/websocket/gateway/OtaUpdateRequest.cpp index 669df0eb..ebaf99c8 100644 --- a/src/message_handlers/websocket/gateway/OtaUpdateRequest.cpp +++ b/src/message_handlers/websocket/gateway/OtaUpdateRequest.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/ota/OtaUpdateClient.cpp b/src/ota/OtaUpdateClient.cpp new file mode 100644 index 00000000..d2fe361c --- /dev/null +++ b/src/ota/OtaUpdateClient.cpp @@ -0,0 +1,290 @@ +#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) +{ + 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(); + + 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, m_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. + 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", 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()); + 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..d39c5cb2 --- /dev/null +++ b/src/ota/OtaUpdateManager.cpp @@ -0,0 +1,316 @@ +#include + +#include "ota/OtaUpdateManager.h" + +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 "SimpleMutex.h" +#include "util/StringUtils.h" +#include "util/TaskUtils.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; +} + +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 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; + +using namespace OpenShock; + +static bool tryStartUpdate(const OpenShock::SemVer& version) +{ + if (!s_clientMtx.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_client = std::make_unique(version); + + if (!s_client->Start()) { + s_client.reset(); + s_clientMtx.unlock(); + OS_LOGE(TAG, "Failed to start update client"); + return false; + } + + s_clientMtx.unlock(); + + OS_LOGD(TAG, "Update client started"); + + return true; +} + +static void wifiDisconnectedEventHandler(void* event_handler_arg, esp_event_base_t event_base, int32_t event_id, void* event_data) +{ + (void)event_handler_arg; + (void)event_base; + (void)event_id; + (void)event_data; + + 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) +{ + (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; + } +} + +static void watcherTask(void*) +{ + OS_LOGD(TAG, "OTA update task started"); + + bool connected = 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 + + 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 + + if (!check) { + continue; + } + + lastUpdateCheck = now; + + if (config.requireManualApproval) { + OS_LOGD(TAG, "Manual approval required, skipping update check"); + // TODO: IMPLEMENT + continue; + } + + 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", result.data.toString().c_str()); // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this + } +} + +bool OtaUpdateManager::Init() +{ + esp_err_t err; + + 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. + 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. + } + + 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: + 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: + s_bootType = FirmwareBootType::Rollback; + break; + default: + s_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? + } + } + + 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(watcherTask, "OtaWatcherTask", 8192, nullptr, 1, &s_taskHandle) != pdPASS) { + OS_LOGE(TAG, "Failed to create OTA watcher task"); + return false; + } + + 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 + + return tryStartUpdate(version); +} + +FirmwareBootType OtaUpdateManager::GetFirmwareBootType() +{ + return s_bootType; +} + +bool OtaUpdateManager::IsValidatingApp() +{ + return s_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? + } + + s_otaImageState = ESP_OTA_IMG_VALID; +}