Files
crosspoint-reader-mod/src/network/OtaUpdater.cpp

273 lines
8.6 KiB
C++
Raw Normal View History

#include "OtaUpdater.h"
#include <ArduinoJson.h>
#include <Logging.h>
#include "esp_http_client.h"
#include "esp_https_ota.h"
#include "esp_wifi.h"
namespace {
2026-01-14 23:14:00 +11:00
constexpr char latestReleaseUrl[] = "https://api.github.com/repos/crosspoint-reader/crosspoint-reader/releases/latest";
/* This is buffer and size holder to keep upcoming data from latestReleaseUrl */
char* local_buf;
int output_len;
/*
* When esp_crt_bundle.h included, it is pointing wrong header file
* which is something under WifiClientSecure because of our framework based on arduno platform.
* To manage this obstacle, don't include anything, just extern and it will point correct one.
*/
extern "C" {
extern esp_err_t esp_crt_bundle_attach(void* conf);
}
esp_err_t http_client_set_header_cb(esp_http_client_handle_t http_client) {
return esp_http_client_set_header(http_client, "User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION);
}
esp_err_t event_handler(esp_http_client_event_t* event) {
/* We do interested in only HTTP_EVENT_ON_DATA event only */
if (event->event_id != HTTP_EVENT_ON_DATA) return ESP_OK;
if (!esp_http_client_is_chunked_response(event->client)) {
int content_len = esp_http_client_get_content_length(event->client);
int copy_len = 0;
if (local_buf == NULL) {
/* local_buf life span is tracked by caller checkForUpdate */
local_buf = static_cast<char*>(calloc(content_len + 1, sizeof(char)));
output_len = 0;
if (local_buf == NULL) {
LOG_ERR("OTA", "HTTP Client Out of Memory Failed, Allocation %d", content_len);
return ESP_ERR_NO_MEM;
}
}
copy_len = min(event->data_len, (content_len - output_len));
if (copy_len) {
memcpy(local_buf + output_len, event->data, copy_len);
}
output_len += copy_len;
} else {
/* Code might be hits here, It happened once (for version checking) but I need more logs to handle that */
int chunked_len;
esp_http_client_get_chunk_length(event->client, &chunked_len);
LOG_DBG("OTA", "esp_http_client_is_chunked_response failed, chunked_len: %d", chunked_len);
}
return ESP_OK;
} /* event_handler */
} /* namespace */
OtaUpdater::OtaUpdaterError OtaUpdater::checkForUpdate() {
JsonDocument filter;
esp_err_t esp_err;
JsonDocument doc;
esp_http_client_config_t client_config = {
.url = latestReleaseUrl,
.event_handler = event_handler,
/* Default HTTP client buffer size 512 byte only */
.buffer_size = 8192,
.buffer_size_tx = 8192,
.skip_cert_common_name_check = true,
.crt_bundle_attach = esp_crt_bundle_attach,
.keep_alive_enable = true,
};
/* To track life time of local_buf, dtor will be called on exit from that function */
struct localBufCleaner {
char** bufPtr;
~localBufCleaner() {
if (*bufPtr) {
free(*bufPtr);
*bufPtr = NULL;
}
}
} localBufCleaner = {&local_buf};
esp_http_client_handle_t client_handle = esp_http_client_init(&client_config);
if (!client_handle) {
LOG_ERR("OTA", "HTTP Client Handle Failed");
return INTERNAL_UPDATE_ERROR;
}
esp_err = esp_http_client_set_header(client_handle, "User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION);
if (esp_err != ESP_OK) {
LOG_ERR("OTA", "esp_http_client_set_header Failed : %s", esp_err_to_name(esp_err));
esp_http_client_cleanup(client_handle);
return INTERNAL_UPDATE_ERROR;
}
esp_err = esp_http_client_perform(client_handle);
if (esp_err != ESP_OK) {
LOG_ERR("OTA", "esp_http_client_perform Failed : %s", esp_err_to_name(esp_err));
esp_http_client_cleanup(client_handle);
return HTTP_ERROR;
}
/* esp_http_client_close will be called inside cleanup as well*/
esp_err = esp_http_client_cleanup(client_handle);
if (esp_err != ESP_OK) {
LOG_ERR("OTA", "esp_http_client_cleanup Failed : %s", esp_err_to_name(esp_err));
return INTERNAL_UPDATE_ERROR;
}
filter["tag_name"] = true;
filter["assets"][0]["name"] = true;
filter["assets"][0]["browser_download_url"] = true;
filter["assets"][0]["size"] = true;
const DeserializationError error = deserializeJson(doc, local_buf, DeserializationOption::Filter(filter));
if (error) {
LOG_ERR("OTA", "JSON parse failed: %s", error.c_str());
return JSON_PARSE_ERROR;
}
if (!doc["tag_name"].is<std::string>()) {
LOG_ERR("OTA", "No tag_name found");
return JSON_PARSE_ERROR;
}
if (!doc["assets"].is<JsonArray>()) {
LOG_ERR("OTA", "No assets found");
return JSON_PARSE_ERROR;
}
latestVersion = doc["tag_name"].as<std::string>();
for (int i = 0; i < doc["assets"].size(); i++) {
if (doc["assets"][i]["name"] == "firmware.bin") {
otaUrl = doc["assets"][i]["browser_download_url"].as<std::string>();
otaSize = doc["assets"][i]["size"].as<size_t>();
totalSize = otaSize;
updateAvailable = true;
break;
}
}
if (!updateAvailable) {
LOG_ERR("OTA", "No firmware.bin asset found");
return NO_UPDATE;
}
LOG_DBG("OTA", "Found update: %s", latestVersion.c_str());
return OK;
}
bool OtaUpdater::isUpdateNewer() const {
if (!updateAvailable || latestVersion.empty() || latestVersion == CROSSPOINT_VERSION) {
return false;
}
int currentMajor, currentMinor, currentPatch;
int latestMajor, latestMinor, latestPatch;
const auto currentVersion = CROSSPOINT_VERSION;
// semantic version check (only match on 3 segments)
sscanf(latestVersion.c_str(), "%d.%d.%d", &latestMajor, &latestMinor, &latestPatch);
sscanf(currentVersion, "%d.%d.%d", &currentMajor, &currentMinor, &currentPatch);
/*
* Compare major versions.
* If they differ, return true if latest major version greater than current major version
* otherwise return false.
*/
if (latestMajor != currentMajor) return latestMajor > currentMajor;
/*
* Compare minor versions.
* If they differ, return true if latest minor version greater than current minor version
* otherwise return false.
*/
if (latestMinor != currentMinor) return latestMinor > currentMinor;
/*
* Check patch versions.
*/
if (latestPatch != currentPatch) return latestPatch > currentPatch;
// If we reach here, it means all segments are equal.
// One final check, if we're on an RC build (contains "-rc"), we should consider the latest version as newer even if
// the segments are equal, since RC builds are pre-release versions.
if (strstr(currentVersion, "-rc") != nullptr) {
return true;
}
return false;
}
const std::string& OtaUpdater::getLatestVersion() const { return latestVersion; }
OtaUpdater::OtaUpdaterError OtaUpdater::installUpdate() {
if (!isUpdateNewer()) {
return UPDATE_OLDER_ERROR;
}
esp_https_ota_handle_t ota_handle = NULL;
esp_err_t esp_err;
/* Signal for OtaUpdateActivity */
render = false;
esp_http_client_config_t client_config = {
.url = otaUrl.c_str(),
.timeout_ms = 15000,
/* Default HTTP client buffer size 512 byte only
* not sufficent to handle URL redirection cases or
* parsing of large HTTP headers.
*/
.buffer_size = 8192,
.buffer_size_tx = 8192,
.skip_cert_common_name_check = true,
.crt_bundle_attach = esp_crt_bundle_attach,
.keep_alive_enable = true,
};
esp_https_ota_config_t ota_config = {
.http_config = &client_config,
.http_client_init_cb = http_client_set_header_cb,
};
/* For better timing and connectivity, we disable power saving for WiFi */
esp_wifi_set_ps(WIFI_PS_NONE);
esp_err = esp_https_ota_begin(&ota_config, &ota_handle);
if (esp_err != ESP_OK) {
LOG_DBG("OTA", "HTTP OTA Begin Failed: %s", esp_err_to_name(esp_err));
return INTERNAL_UPDATE_ERROR;
}
do {
esp_err = esp_https_ota_perform(ota_handle);
processedSize = esp_https_ota_get_image_len_read(ota_handle);
/* Sent signal to OtaUpdateActivity */
render = true;
refactor: move render() to Activity super class, use freeRTOS notification (#774) ## Summary Currently, each activity has to manage their own `displayTaskLoop` which adds redundant boilerplate code. The loop is a wait loop which is also not the best practice, as the `updateRequested` boolean is not protected by a mutex. In this PR: - Move `displayTaskLoop` to the super `Activity` class - Replace `updateRequested` with freeRTOS's [direct to task notification](https://www.freertos.org/Documentation/02-Kernel/02-Kernel-features/03-Direct-to-task-notifications/01-Task-notifications) - For `ActivityWithSubactivity`, whenever a sub-activity is present, the parent's `render()` automatically goes inactive With this change, activities now only need to expose `render()` function, and anywhere in the code base can call `requestUpdate()` to request a new rendering pass. ## Additional Context In theory, this change may also make the battery life a bit better, since one wait loop is removed. Although the equipment in my home lab wasn't been able to verify it (the electric current is too noisy and small). Would appreciate if anyone has any insights on this subject. Update: I managed to hack [a small piece of code](https://github.com/ngxson/crosspoint-reader/tree/xsn/measure_cpu_usage) that allow tracking CPU idle time. The CPU load does decrease a bit (1.47% down to 1.39%), which make sense, because the display task is now sleeping most of the time unless notified. This should translate to a slightly increase in battery life in the long run. ``` PR: [40012] [MEM] Free: 185856 bytes, Total: 231004 bytes, Min Free: 123316 bytes [40012] [IDLE] Idle time: 98.61% (CPU load: 1.39%) [50017] [MEM] Free: 185856 bytes, Total: 231004 bytes, Min Free: 123316 bytes [50017] [IDLE] Idle time: 98.61% (CPU load: 1.39%) [60022] [MEM] Free: 185856 bytes, Total: 231004 bytes, Min Free: 123316 bytes [60022] [IDLE] Idle time: 98.61% (CPU load: 1.39%) master: [20012] [MEM] Free: 195016 bytes, Total: 231532 bytes, Min Free: 132460 bytes [20012] [IDLE] Idle time: 98.53% (CPU load: 1.47%) [30017] [MEM] Free: 195016 bytes, Total: 231532 bytes, Min Free: 132460 bytes [30017] [IDLE] Idle time: 98.53% (CPU load: 1.47%) [40022] [MEM] Free: 195016 bytes, Total: 231532 bytes, Min Free: 132460 bytes [40022] [IDLE] Idle time: 98.53% (CPU load: 1.47%) ``` --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? **NO** <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Refactor** * Streamlined rendering architecture by consolidating update mechanisms across all activities, improving efficiency and consistency. * Modernized synchronization patterns for display updates to ensure reliable, conflict-free rendering. * **Bug Fixes** * Enhanced rendering stability through improved locking mechanisms and explicit update requests. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: znelson <znelson@users.noreply.github.com>
2026-02-16 11:11:15 +01:00
delay(100); // TODO: should we replace this with something better?
} while (esp_err == ESP_ERR_HTTPS_OTA_IN_PROGRESS);
/* Return back to default power saving for WiFi in case of failing */
esp_wifi_set_ps(WIFI_PS_MIN_MODEM);
if (esp_err != ESP_OK) {
LOG_ERR("OTA", "esp_https_ota_perform Failed: %s", esp_err_to_name(esp_err));
esp_https_ota_finish(ota_handle);
return HTTP_ERROR;
}
if (!esp_https_ota_is_complete_data_received(ota_handle)) {
LOG_ERR("OTA", "esp_https_ota_is_complete_data_received Failed: %s", esp_err_to_name(esp_err));
esp_https_ota_finish(ota_handle);
return INTERNAL_UPDATE_ERROR;
}
esp_err = esp_https_ota_finish(ota_handle);
if (esp_err != ESP_OK) {
LOG_ERR("OTA", "esp_https_ota_finish Failed: %s", esp_err_to_name(esp_err));
return INTERNAL_UPDATE_ERROR;
}
LOG_INF("OTA", "Update completed");
return OK;
}