## Summary The GitHub recent release API returns lots of data, including the full release notes. For 1.2.0, that data is 30,530 bytes. The existing OtaUpdater HTTP handler and single-buffer JSON parsing can't reliably handle that much data in the constrained ESP32 environment. This change does a few things: 1. Adds a very simple lib/JsonParser/StreamingJsonParser.cpp with SAX-style callbacks to read JSON data incrementally. 2. Adds a very simple lib/JsonParser/ReleaseJsonParser.cpp building on StreamingJsonParser, which specifically parses the GitHub release JSON for the release version, URL, and size. 3. Updates OtaUpdater.cpp to use an instance of ReleaseJsonParser to incrementally parse the large response it may receive from GitHub. Building from this commit while overriding my local version to 1.1.9, I was able to run the OTA update process ~5 times in a row successfully. Fixes #1561 (second part, after #1805). --- ### 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? _**YES**_
210 lines
6.8 KiB
C++
210 lines
6.8 KiB
C++
#include "OtaUpdater.h"
|
|
|
|
#include <Logging.h>
|
|
#include <ReleaseJsonParser.h>
|
|
#include <esp_crt_bundle.h>
|
|
#include <esp_http_client.h>
|
|
#include <esp_https_ota.h>
|
|
#include <esp_wifi.h>
|
|
|
|
namespace {
|
|
constexpr char latestReleaseUrl[] = "https://api.github.com/repos/crosspoint-reader/crosspoint-reader/releases/latest";
|
|
|
|
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);
|
|
}
|
|
|
|
size_t totalBytesReceived = 0;
|
|
|
|
esp_err_t event_handler(esp_http_client_event_t* event) {
|
|
if (event->event_id != HTTP_EVENT_ON_DATA) return ESP_OK;
|
|
totalBytesReceived += event->data_len;
|
|
LOG_DBG("OTA", "HTTP chunk: %d bytes (total: %zu)", event->data_len, totalBytesReceived);
|
|
auto* parser = static_cast<ReleaseJsonParser*>(event->user_data);
|
|
parser->feed(static_cast<const char*>(event->data), event->data_len);
|
|
return ESP_OK;
|
|
}
|
|
} // namespace
|
|
|
|
OtaUpdater::OtaUpdaterError OtaUpdater::checkForUpdate() {
|
|
esp_err_t esp_err;
|
|
ReleaseJsonParser releaseParser;
|
|
|
|
esp_http_client_config_t client_config = {
|
|
.url = latestReleaseUrl,
|
|
.event_handler = event_handler,
|
|
.buffer_size = 8192,
|
|
.buffer_size_tx = 8192,
|
|
.user_data = &releaseParser,
|
|
.skip_cert_common_name_check = true,
|
|
.crt_bundle_attach = esp_crt_bundle_attach,
|
|
.keep_alive_enable = true,
|
|
};
|
|
|
|
totalBytesReceived = 0;
|
|
LOG_DBG("OTA", "Checking for update (current: %s)", CROSSPOINT_VERSION);
|
|
|
|
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_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;
|
|
}
|
|
|
|
LOG_DBG("OTA", "Response received: %zu bytes total", totalBytesReceived);
|
|
LOG_DBG("OTA", "Parser results: tag=%s firmware=%s", releaseParser.foundTag() ? "yes" : "no",
|
|
releaseParser.foundFirmware() ? "yes" : "no");
|
|
|
|
if (!releaseParser.foundTag()) {
|
|
LOG_ERR("OTA", "No tag_name in release JSON");
|
|
return JSON_PARSE_ERROR;
|
|
}
|
|
|
|
if (!releaseParser.foundFirmware()) {
|
|
LOG_ERR("OTA", "No firmware.bin asset found");
|
|
return NO_UPDATE;
|
|
}
|
|
|
|
latestVersion = releaseParser.getTagName();
|
|
otaUrl = releaseParser.getFirmwareUrl();
|
|
otaSize = releaseParser.getFirmwareSize();
|
|
totalSize = otaSize;
|
|
updateAvailable = true;
|
|
|
|
LOG_DBG("OTA", "Found update: tag=%s size=%zu", latestVersion.c_str(), otaSize);
|
|
LOG_DBG("OTA", "Firmware URL: %s", otaUrl.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", ¤tMajor, ¤tMinor, ¤tPatch);
|
|
|
|
/*
|
|
* 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(ProgressCallback onProgress, void* ctx) {
|
|
if (!isUpdateNewer()) {
|
|
return UPDATE_OLDER_ERROR;
|
|
}
|
|
|
|
esp_https_ota_handle_t ota_handle = NULL;
|
|
esp_err_t esp_err;
|
|
|
|
esp_http_client_config_t client_config = {
|
|
.url = otaUrl.c_str(),
|
|
.timeout_ms = 15000,
|
|
/* Default HTTP client buffer size 512 byte only
|
|
* not sufficient 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);
|
|
if (onProgress) onProgress(ctx);
|
|
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;
|
|
}
|