From e25bf7155d2a42efd55caba956cbb9717c85d379 Mon Sep 17 00:00:00 2001 From: Terrence Date: Thu, 5 Sep 2024 17:22:01 +0800 Subject: [PATCH] add OTA support --- CMakeLists.txt | 2 + main/Application.cc | 12 +++ main/Application.h | 2 + main/CMakeLists.txt | 1 + main/FirmwareUpgrade.cc | 169 ++++++++++++++++++++++++++++++++++++++++ main/FirmwareUpgrade.h | 24 ++++++ main/Kconfig.projbuild | 8 +- main/SystemInfo.cc | 29 ++++++- main/WifiStation.cc | 7 ++ main/WifiStation.h | 4 + main/idf_component.yml | 2 + main/main.cc | 4 +- sdkconfig.defaults | 1 + 13 files changed, 259 insertions(+), 6 deletions(-) create mode 100644 main/FirmwareUpgrade.cc create mode 100644 main/FirmwareUpgrade.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 0bab4a78..40c1d878 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,5 +4,7 @@ # CMakeLists in this exact order for cmake to work correctly cmake_minimum_required(VERSION 3.16) +set(PROJECT_VER "0.1.0") + include($ENV{IDF_PATH}/tools/cmake/project.cmake) project(xiaozhi) diff --git a/main/Application.cc b/main/Application.cc index c43ceebd..b0edfd6a 100644 --- a/main/Application.cc +++ b/main/Application.cc @@ -98,6 +98,18 @@ void Application::Start() { builtin_led_.SetBlue(); builtin_led_.BlinkOnce(); wifi_station_.Start(); + + // Check if there is a new firmware version available + firmware_upgrade_.CheckVersion(); + if (firmware_upgrade_.HasNewVersion()) { + builtin_led_.TurnOn(); + firmware_upgrade_.StartUpgrade(); + // If upgrade success, the device will reboot and never reach here + ESP_LOGI(TAG, "Firmware upgrade failed..."); + builtin_led_.TurnOff(); + } else { + firmware_upgrade_.MarkValid(); + } StartCommunication(); StartDetection(); diff --git a/main/Application.h b/main/Application.h index 0f04bf6a..63efb0b6 100644 --- a/main/Application.h +++ b/main/Application.h @@ -6,6 +6,7 @@ #include "OpusEncoder.h" #include "WebSocketClient.h" #include "BuiltinLed.h" +#include "FirmwareUpgrade.h" #include "opus.h" #include "resampler_structs.h" @@ -40,6 +41,7 @@ private: WifiStation wifi_station_; AudioDevice audio_device_; BuiltinLed builtin_led_; + FirmwareUpgrade firmware_upgrade_; std::recursive_mutex mutex_; WebSocketClient* ws_client_ = nullptr; diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 63225bfe..e6a45c08 100755 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -7,6 +7,7 @@ set(SOURCES "AudioDevice.cc" "WifiConfigurationAp.cc" "main.cc" "WifiStation.cc" + "FirmwareUpgrade.cc" ) idf_component_register(SRCS ${SOURCES} diff --git a/main/FirmwareUpgrade.cc b/main/FirmwareUpgrade.cc new file mode 100644 index 00000000..b329c869 --- /dev/null +++ b/main/FirmwareUpgrade.cc @@ -0,0 +1,169 @@ +#include "FirmwareUpgrade.h" +#include "SystemInfo.h" +#include "esp_log.h" +#include "esp_ota_ops.h" +#include "esp_https_ota.h" +#include "esp_partition.h" +#include "esp_http_client.h" +#include "esp_crt_bundle.h" +#include "cJSON.h" + + +#define TAG "FirmwareUpgrade" + + +FirmwareUpgrade::FirmwareUpgrade() { +} + +FirmwareUpgrade::~FirmwareUpgrade() { +} + +void FirmwareUpgrade::CheckVersion() { + std::string current_version = esp_app_get_description()->version; + ESP_LOGI(TAG, "Current version: %s", current_version.c_str()); + + // Get device info and request the latest firmware from the server + std::string device_info = SystemInfo::GetJsonString(); + + esp_http_client_config_t config = {}; + config.url = CONFIG_OTA_UPGRADE_URL; + config.crt_bundle_attach = esp_crt_bundle_attach; + esp_http_client_handle_t client = esp_http_client_init(&config); + esp_http_client_set_method(client, HTTP_METHOD_POST); + esp_http_client_set_header(client, "Content-Type", "application/json"); + esp_http_client_set_header(client, "Device-Id", SystemInfo::GetMacAddress().c_str()); + esp_http_client_set_post_field(client, device_info.c_str(), device_info.length()); + esp_err_t err = esp_http_client_open(client, 0); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to perform HTTP request: %s", esp_err_to_name(err)); + esp_http_client_cleanup(client); + return; + } + int content_length = esp_http_client_fetch_headers(client); + if (content_length <= 0) { + ESP_LOGE(TAG, "Failed to fetch headers"); + esp_http_client_cleanup(client); + return; + } + + std::string response; + response.resize(content_length); + int ret = esp_http_client_read_response(client, (char*)response.data(), content_length); + if (ret <= 0) { + ESP_LOGE(TAG, "Failed to read response content_length=%d", content_length); + esp_http_client_cleanup(client); + return; + } + esp_http_client_close(client); + esp_http_client_cleanup(client); + + // Response: { "firmware": { "version": "1.0.0", "url": "http://" } } + // Parse the JSON response and check if the version is newer + // If it is, set has_new_version_ to true and store the new version and URL + + cJSON *root = cJSON_Parse(response.c_str()); + if (root == NULL) { + ESP_LOGE(TAG, "Failed to parse JSON response"); + return; + } + cJSON *firmware = cJSON_GetObjectItem(root, "firmware"); + if (firmware == NULL) { + ESP_LOGE(TAG, "Failed to get firmware object"); + cJSON_Delete(root); + return; + } + cJSON *version = cJSON_GetObjectItem(firmware, "version"); + if (version == NULL) { + ESP_LOGE(TAG, "Failed to get version object"); + cJSON_Delete(root); + return; + } + cJSON *url = cJSON_GetObjectItem(firmware, "url"); + if (url == NULL) { + ESP_LOGE(TAG, "Failed to get url object"); + cJSON_Delete(root); + return; + } + + new_version_ = version->valuestring; + new_version_url_ = url->valuestring; + cJSON_Delete(root); + + has_new_version_ = new_version_ != current_version; + if (has_new_version_) { + ESP_LOGI(TAG, "New version available: %s", new_version_.c_str()); + } else { + ESP_LOGI(TAG, "No new version available"); + } + return; +} + +void FirmwareUpgrade::MarkValid() { + auto partition = esp_ota_get_running_partition(); + if (strcmp(partition->label, "factory") == 0) { + ESP_LOGI(TAG, "Running from factory partition, skipping"); + return; + } + + ESP_LOGI(TAG, "Running partition: %s", partition->label); + esp_ota_img_states_t state; + if (esp_ota_get_state_partition(partition, &state) != ESP_OK) { + ESP_LOGE(TAG, "Failed to get state of partition"); + return; + } + + if (state == ESP_OTA_IMG_PENDING_VERIFY) { + ESP_LOGI(TAG, "Marking firmware as valid"); + esp_ota_mark_app_valid_cancel_rollback(); + } +} + +void FirmwareUpgrade::Upgrade(std::string firmware_url) { + ESP_LOGI(TAG, "Upgrading firmware from %s", firmware_url.c_str()); + + esp_http_client_config_t config = {}; + config.url = firmware_url.c_str(); + config.crt_bundle_attach = esp_crt_bundle_attach; + config.event_handler = [](esp_http_client_event_t *evt) { + switch (evt->event_id) { + case HTTP_EVENT_ERROR: + ESP_LOGD(TAG, "HTTP_EVENT_ERROR"); + break; + case HTTP_EVENT_ON_CONNECTED: + ESP_LOGD(TAG, "HTTP_EVENT_ON_CONNECTED"); + break; + case HTTP_EVENT_HEADER_SENT: + ESP_LOGD(TAG, "HTTP_EVENT_HEADER_SENT"); + break; + case HTTP_EVENT_ON_HEADER: + ESP_LOGD(TAG, "HTTP_EVENT_ON_HEADER, key=%s, value=%s", evt->header_key, evt->header_value); + break; + case HTTP_EVENT_ON_DATA: + ESP_LOGD(TAG, "HTTP_EVENT_ON_DATA, len=%d", evt->data_len); + break; + case HTTP_EVENT_ON_FINISH: + ESP_LOGD(TAG, "HTTP_EVENT_ON_FINISH"); + break; + case HTTP_EVENT_DISCONNECTED: + ESP_LOGD(TAG, "HTTP_EVENT_DISCONNECTED"); + break; + case HTTP_EVENT_REDIRECT: + ESP_LOGD(TAG, "HTTP_EVENT_REDIRECT"); + break; + } + return ESP_OK; + }; + config.keep_alive_enable = true; + + esp_https_ota_config_t ota_config = {}; + ota_config.http_config = &config; + esp_err_t err = esp_https_ota(&ota_config); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to upgrade firmware: %s", esp_err_to_name(err)); + return; + } + + ESP_LOGI(TAG, "Firmware upgrade successful, rebooting in 3 seconds..."); + vTaskDelay(pdMS_TO_TICKS(3000)); + esp_restart(); +} diff --git a/main/FirmwareUpgrade.h b/main/FirmwareUpgrade.h new file mode 100644 index 00000000..8fac7a42 --- /dev/null +++ b/main/FirmwareUpgrade.h @@ -0,0 +1,24 @@ +#ifndef _FIRMWARE_UPGRADE_H +#define _FIRMWARE_UPGRADE_H + +#include + +class FirmwareUpgrade { +public: + FirmwareUpgrade(); + ~FirmwareUpgrade(); + + void CheckVersion(); + bool HasNewVersion() { return has_new_version_; } + void StartUpgrade() { Upgrade(new_version_url_); } + void MarkValid(); + +private: + bool has_new_version_ = false; + std::string new_version_; + std::string new_version_url_; + + void Upgrade(std::string firmware_url); +}; + +#endif // _FIRMWARE_UPGRADE_H diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index d5e77fc4..d9c1722d 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -1,8 +1,14 @@ menu "Xiaozhi Assistant" +config OTA_UPGRADE_URL + string "OTA Upgrade URL" + default "https://" + help + The application will access this URL to check for updates when it starts and every 24 hours. + config WEBSOCKET_URL string "Websocket URL" - default "ws://" + default "wss://" help Communication with the server through websocket after wake up. diff --git a/main/SystemInfo.cc b/main/SystemInfo.cc index 0888aede..efcbf25e 100644 --- a/main/SystemInfo.cc +++ b/main/SystemInfo.cc @@ -8,6 +8,8 @@ #include "esp_partition.h" #include "esp_app_desc.h" #include "esp_psram.h" +#include "esp_wifi.h" +#include "esp_ota_ops.h" #define TAG "SystemInfo" @@ -62,14 +64,22 @@ std::string SystemInfo::GetJsonString() { "idf_version": "4.2-dev" "elf_sha256": "" }, - "partition_table": { + "partition_table": [ "app": { "label": "app", "type": 1, "subtype": 2, "address": 0x10000, "size": 0x100000 - }, + } + ], + "ota": { + "label": "ota_0" + }, + "wifi": { + "ssid": "my-wifi", + "rss": -50, + "channel": 6 } } */ @@ -117,7 +127,20 @@ std::string SystemInfo::GetJsonString() { it = esp_partition_next(it); } json.pop_back(); // Remove the last comma - json += "]"; + json += "],"; + + json += "\"ota\":{"; + auto ota_partition = esp_ota_get_running_partition(); + json += "\"label\":\"" + std::string(ota_partition->label) + "\""; + json += "},"; + + wifi_ap_record_t ap_info; + esp_wifi_sta_get_ap_info(&ap_info); + json += "\"wifi\":{"; + json += "\"ssid\":\"" + std::string(reinterpret_cast(ap_info.ssid)) + "\","; + json += "\"rssi\":" + std::to_string(ap_info.rssi) + ","; + json += "\"channel\":" + std::to_string(ap_info.primary); + json += "}"; // Close the JSON object json += "}"; diff --git a/main/WifiStation.cc b/main/WifiStation.cc index 02776d39..79be59fb 100644 --- a/main/WifiStation.cc +++ b/main/WifiStation.cc @@ -93,6 +93,13 @@ void WifiStation::Start() { } else { ESP_LOGI(TAG, "WifiStation started"); } + + // Get station info + wifi_ap_record_t ap_info; + ESP_ERROR_CHECK(esp_wifi_sta_get_ap_info(&ap_info)); + ESP_LOGI(TAG, "Connected to %s rssi=%d channel=%d", ap_info.ssid, ap_info.rssi, ap_info.primary); + rssi_ = ap_info.rssi; + channel_ = ap_info.primary; } bool WifiStation::IsConnected() { diff --git a/main/WifiStation.h b/main/WifiStation.h index 1d4912d1..edb9b27a 100644 --- a/main/WifiStation.h +++ b/main/WifiStation.h @@ -11,12 +11,16 @@ public: bool IsConnected(); std::string ssid() { return ssid_; } std::string ip_address() { return ip_address_; } + int8_t rssi() { return rssi_; } + uint8_t channel() { return channel_; } private: EventGroupHandle_t event_group_; std::string ssid_; std::string password_; std::string ip_address_; + uint8_t rssi_ = 0; + uint8_t channel_ = 0; int reconnect_count_ = 0; }; diff --git a/main/idf_component.yml b/main/idf_component.yml index efcccb6c..0802dfbe 100644 --- a/main/idf_component.yml +++ b/main/idf_component.yml @@ -18,3 +18,5 @@ dependencies: # # `public` flag doesn't have an effect dependencies of the `main` component. # # All dependencies of `main` are public by default. # public: true +description: "An AI voice assistant for ESP32" +url: "https://github.com/78/xiaozhi-esp32" diff --git a/main/main.cc b/main/main.cc index 881ccaed..610e429a 100755 --- a/main/main.cc +++ b/main/main.cc @@ -56,9 +56,9 @@ extern "C" void app_main(void) auto app = new Application(); app->Start(); - // Dump CPU usage every 1 second + // Dump CPU usage every 10 second while (true) { - vTaskDelay(5000 / portTICK_PERIOD_MS); + vTaskDelay(10000 / portTICK_PERIOD_MS); // SystemInfo::PrintRealTimeStats(STATS_TICKS); int free_sram = heap_caps_get_minimum_free_size(MALLOC_CAP_INTERNAL); ESP_LOGI(TAG, "Free heap size: %u minimal internal: %u", SystemInfo::GetFreeHeapSize(), free_sram); diff --git a/sdkconfig.defaults b/sdkconfig.defaults index e7b9dadf..365b56ce 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -1,6 +1,7 @@ CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_PERF=y CONFIG_BOOTLOADER_LOG_LEVEL_NONE=y CONFIG_BOOTLOADER_SKIP_VALIDATE_ALWAYS=y +CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y CONFIG_ESP32S3_INSTRUCTION_CACHE_32KB=y