ESPHome 2026.6.0-dev
Loading...
Searching...
No Matches
esp32_hosted_update.cpp
Go to the documentation of this file.
1#if defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4)
6#include "esphome/core/log.h"
7#include <esp_image_format.h>
8#include <esp_app_desc.h>
9#include <esp_hosted.h>
10#include <esp_hosted_host_fw_ver.h>
11#include <esp_ota_ops.h>
12
13#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
17#endif
18
19extern "C" {
20#include <esp_hosted_ota.h>
21}
22
24
25static const char *const TAG = "esp32_hosted.update";
26
27// Older coprocessor firmware versions have a 1500-byte limit per RPC call
28constexpr size_t CHUNK_SIZE = 1500;
29
30#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
31// Interval/timeout IDs (uint32_t to avoid string comparison)
33#endif
34
35// Compile-time version string from esp_hosted_host_fw_ver.h macros
36#define STRINGIFY_(x) #x
37#define STRINGIFY(x) STRINGIFY_(x)
38static const char *const ESP_HOSTED_VERSION_STR = STRINGIFY(ESP_HOSTED_VERSION_MAJOR_1) "." STRINGIFY(
39 ESP_HOSTED_VERSION_MINOR_1) "." STRINGIFY(ESP_HOSTED_VERSION_PATCH_1);
40
41#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
42// Parse an integer from str, advancing ptr past the number
43// Returns false if no digits were parsed
44static bool parse_int(const char *&ptr, int &value) {
45 char *end;
46 value = static_cast<int>(strtol(ptr, &end, 10));
47 if (end == ptr)
48 return false;
49 ptr = end;
50 return true;
51}
52
53// Parse version string "major.minor.patch" into components
54// Returns true if at least major.minor was parsed
55static bool parse_version(const std::string &version_str, int &major, int &minor, int &patch) {
56 major = minor = patch = 0;
57 const char *ptr = version_str.c_str();
58
59 if (!parse_int(ptr, major) || *ptr != '.')
60 return false;
61 ++ptr;
62 if (!parse_int(ptr, minor))
63 return false;
64 if (*ptr == '.')
65 parse_int(++ptr, patch);
66
67 return true;
68}
69
70// Compare two versions, returns:
71// -1 if v1 < v2
72// 0 if v1 == v2
73// 1 if v1 > v2
74static int compare_versions(int major1, int minor1, int patch1, int major2, int minor2, int patch2) {
75 if (major1 != major2)
76 return major1 < major2 ? -1 : 1;
77 if (minor1 != minor2)
78 return minor1 < minor2 ? -1 : 1;
79 if (patch1 != patch2)
80 return patch1 < patch2 ? -1 : 1;
81 return 0;
82}
83#endif
84
86 this->update_info_.title = "ESP32 Hosted Coprocessor";
87
88#ifndef USE_WIFI
89 // If WiFi is not present, connect to the coprocessor
90 esp_hosted_connect_to_slave(); // NOLINT
91#endif
92
93 // Get coprocessor version
94 esp_hosted_coprocessor_fwver_t ver_info;
95 if (esp_hosted_get_coprocessor_fwversion(&ver_info) == ESP_OK) {
96 // 16 bytes: "255.255.255" (11 chars) + null + safety margin
97 char buf[16];
98 snprintf(buf, sizeof(buf), "%" PRIu32 ".%" PRIu32 ".%" PRIu32, ver_info.major1, ver_info.minor1, ver_info.patch1);
99 this->update_info_.current_version = buf;
100 } else {
101 this->update_info_.current_version = "unknown";
102 }
103 ESP_LOGD(TAG, "Coprocessor version: %s", this->update_info_.current_version.c_str());
104
105#ifndef USE_ESP32_HOSTED_HTTP_UPDATE
106 // Embedded mode: get image version from embedded firmware
107 const int app_desc_offset = sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t);
108 if (this->firmware_size_ >= app_desc_offset + sizeof(esp_app_desc_t)) {
109 esp_app_desc_t *app_desc = (esp_app_desc_t *) (this->firmware_data_ + app_desc_offset);
110 if (app_desc->magic_word == ESP_APP_DESC_MAGIC_WORD) {
111 ESP_LOGD(TAG,
112 "ESP32 Hosted firmware:\n"
113 " Firmware version: %s\n"
114 " Project name: %s\n"
115 " Build date: %s\n"
116 " Build time: %s\n"
117 " IDF version: %s",
118 app_desc->version, app_desc->project_name, app_desc->date, app_desc->time, app_desc->idf_ver);
119 this->update_info_.latest_version = app_desc->version;
120 if (this->update_info_.latest_version != this->update_info_.current_version) {
122 } else {
124 }
125 } else {
126 ESP_LOGW(TAG, "Invalid app description magic word: 0x%08" PRIx32 " (expected 0x%08" PRIx32 ")",
127 app_desc->magic_word, static_cast<uint32_t>(ESP_APP_DESC_MAGIC_WORD));
129 }
130 } else {
131 ESP_LOGW(TAG, "Firmware too small to contain app description");
133 }
134
135 // Publish state
136 this->status_clear_error();
137 this->publish_state();
138#else
139 // HTTP mode: check every 10s until network is ready (max 6 attempts)
140 // Only if update interval is > 1 minute to avoid redundant checks
141 if (this->get_update_interval() > 60000) {
142 this->initial_check_remaining_ = 6;
143 this->set_interval(INITIAL_CHECK_INTERVAL_ID, 10000, [this]() {
144 bool connected = network::is_connected();
145 if (--this->initial_check_remaining_ == 0 || connected) {
146 this->cancel_interval(INITIAL_CHECK_INTERVAL_ID);
147 if (connected) {
148 this->check();
149 }
150 }
151 });
152 }
153#endif
154}
155
157 ESP_LOGCONFIG(TAG,
158 "ESP32 Hosted Update:\n"
159 " Host Library Version: %s\n"
160 " Coprocessor Version: %s\n"
161 " Latest Version: %s",
162 ESP_HOSTED_VERSION_STR, this->update_info_.current_version.c_str(),
163 this->update_info_.latest_version.c_str());
164#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
165 ESP_LOGCONFIG(TAG,
166 " Mode: HTTP\n"
167 " Source URL: %s",
168 this->source_url_.c_str());
169#else
170 ESP_LOGCONFIG(TAG,
171 " Mode: Embedded\n"
172 " Firmware Size: %zu bytes",
173 this->firmware_size_);
174#endif
175}
176
178#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
179 if (!network::is_connected()) {
180 ESP_LOGD(TAG, "Network not connected, skipping update check");
181 return;
182 }
183
184 if (!this->fetch_manifest_()) {
185 return;
186 }
187
188 // Compare versions
189 if (this->update_info_.latest_version.empty() ||
190 this->update_info_.latest_version == this->update_info_.current_version) {
192 } else {
194 }
195
196 this->update_info_.has_progress = false;
197 this->update_info_.progress = 0.0f;
198 this->status_clear_error();
199 this->publish_state();
200#endif
201}
202
203#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
205 ESP_LOGD(TAG, "Fetching manifest");
206
207 auto container = this->http_request_parent_->get(this->source_url_);
208 if (container == nullptr || container->status_code != 200) {
209 ESP_LOGE(TAG, "Failed to fetch manifest from %s", this->source_url_.c_str());
210 this->status_set_error(LOG_STR("Failed to fetch manifest"));
211 return false;
212 }
213
214 // Read manifest JSON into string (manifest is small, ~1KB max)
215 // NOTE: HttpContainer::read() has non-BSD socket semantics - see http_request.h
216 // Use http_read_loop_result() helper instead of checking return values directly
217 std::string json_str;
218 json_str.reserve(container->content_length);
219 uint8_t buf[256];
220 uint32_t last_data_time = millis();
221 const uint32_t read_timeout = this->http_request_parent_->get_timeout();
222 while (container->get_bytes_read() < container->content_length) {
223 int read_or_error = container->read(buf, sizeof(buf));
224 App.feed_wdt();
225 yield();
226 auto result =
227 http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout, container->is_read_complete());
229 continue;
230 // Note: COMPLETE is currently unreachable since the loop condition checks bytes_read < content_length,
231 // but this is defensive code in case chunked transfer encoding support is added in the future.
233 break; // COMPLETE, ERROR, or TIMEOUT
234 json_str.append(reinterpret_cast<char *>(buf), read_or_error);
235 }
236 container->end();
237
238 // Parse JSON manifest
239 // Format: {"versions": [{"version": "2.7.0", "url": "...", "sha256": "..."}]}
240 // Only consider versions <= host library version to avoid compatibility issues
241 bool valid = json::parse_json(json_str, [this](JsonObject root) -> bool {
242 if (!root["versions"].is<JsonArray>()) {
243 ESP_LOGE(TAG, "Manifest does not contain 'versions' array");
244 return false;
245 }
246
247 JsonArray versions = root["versions"].as<JsonArray>();
248 if (versions.size() == 0) {
249 ESP_LOGE(TAG, "Manifest 'versions' array is empty");
250 return false;
251 }
252
253 // Find the highest version that is compatible with the host library
254 // (version <= host version to avoid upgrading coprocessor ahead of host)
255 int best_major = -1, best_minor = -1, best_patch = -1;
256 std::string best_version, best_url, best_sha256;
257
258 for (JsonObject entry : versions) {
259 if (!entry["version"].is<const char *>() || !entry["url"].is<const char *>() ||
260 !entry["sha256"].is<const char *>()) {
261 continue; // Skip malformed entries
262 }
263
264 std::string ver_str = entry["version"].as<std::string>();
265 int major, minor, patch;
266 if (!parse_version(ver_str, major, minor, patch)) {
267 ESP_LOGW(TAG, "Failed to parse version: %s", ver_str.c_str());
268 continue;
269 }
270
271 // Check if this version is compatible (not newer than host)
272 if (compare_versions(major, minor, patch, ESP_HOSTED_VERSION_MAJOR_1, ESP_HOSTED_VERSION_MINOR_1,
273 ESP_HOSTED_VERSION_PATCH_1) > 0) {
274 continue;
275 }
276
277 // Check if this is better than our current best
278 if (best_major < 0 || compare_versions(major, minor, patch, best_major, best_minor, best_patch) > 0) {
279 best_major = major;
280 best_minor = minor;
281 best_patch = patch;
282 best_version = ver_str;
283 best_url = entry["url"].as<std::string>();
284 best_sha256 = entry["sha256"].as<std::string>();
285 }
286 }
287
288 if (best_major < 0) {
289 ESP_LOGW(TAG, "No compatible firmware version found (host is %s)", ESP_HOSTED_VERSION_STR);
290 return false;
291 }
292
293 this->update_info_.latest_version = best_version;
294 this->firmware_url_ = best_url;
295
296 // Parse SHA256 hex string to bytes
297 if (!parse_hex(best_sha256, this->firmware_sha256_.data(), 32)) {
298 ESP_LOGE(TAG, "Invalid SHA256: %s", best_sha256.c_str());
299 return false;
300 }
301
302 ESP_LOGD(TAG, "Best compatible version: %s", this->update_info_.latest_version.c_str());
303
304 return true;
305 });
306
307 if (!valid) {
308 ESP_LOGE(TAG, "Failed to parse manifest JSON");
309 this->status_set_error(LOG_STR("Failed to parse manifest"));
310 return false;
311 }
312
313 return true;
314}
315
317 ESP_LOGI(TAG, "Downloading firmware");
318
319 auto container = this->http_request_parent_->get(this->firmware_url_);
320 if (container == nullptr || container->status_code != 200) {
321 ESP_LOGE(TAG, "Failed to fetch firmware");
322 this->status_set_error(LOG_STR("Failed to fetch firmware"));
323 return false;
324 }
325
326 size_t total_size = container->content_length;
327 ESP_LOGI(TAG, "Firmware size: %zu bytes", total_size);
328
329 // Begin OTA on coprocessor
330 esp_err_t err = esp_hosted_slave_ota_begin(); // NOLINT
331 if (err != ESP_OK) {
332 ESP_LOGE(TAG, "Failed to begin OTA: %s", esp_err_to_name(err));
333 container->end();
334 this->status_set_error(LOG_STR("Failed to begin OTA"));
335 return false;
336 }
337
338 // Stream firmware to coprocessor while computing SHA256
339 // NOTE: HttpContainer::read() has non-BSD socket semantics - see http_request.h
340 // Use http_read_loop_result() helper instead of checking return values directly
341 sha256::SHA256 hasher;
342 hasher.init();
343
344 uint8_t buffer[CHUNK_SIZE];
345 uint32_t last_data_time = millis();
346 const uint32_t read_timeout = this->http_request_parent_->get_timeout();
347 while (container->get_bytes_read() < total_size) {
348 int read_or_error = container->read(buffer, sizeof(buffer));
349
350 // Feed watchdog and give other tasks a chance to run
351 App.feed_wdt();
352 yield();
353
354 auto result =
355 http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout, container->is_read_complete());
357 continue;
358 // Note: COMPLETE is currently unreachable since the loop condition checks bytes_read < content_length,
359 // but this is defensive code in case chunked transfer encoding support is added in the future.
361 break;
364 ESP_LOGE(TAG, "Timeout reading firmware data");
365 } else {
366 ESP_LOGE(TAG, "Error reading firmware data: %d", read_or_error);
367 }
368 esp_hosted_slave_ota_end(); // NOLINT
369 container->end();
370 this->status_set_error(LOG_STR("Download failed"));
371 return false;
372 }
373
374 hasher.add(buffer, read_or_error);
375 err = esp_hosted_slave_ota_write(buffer, read_or_error); // NOLINT
376 if (err != ESP_OK) {
377 ESP_LOGE(TAG, "Failed to write OTA data: %s", esp_err_to_name(err));
378 esp_hosted_slave_ota_end(); // NOLINT
379 container->end();
380 this->status_set_error(LOG_STR("Failed to write OTA data"));
381 return false;
382 }
383 }
384 container->end();
385
386 // Verify SHA256
387 hasher.calculate();
388 if (!hasher.equals_bytes(this->firmware_sha256_.data())) {
389 ESP_LOGE(TAG, "SHA256 mismatch");
390 esp_hosted_slave_ota_end(); // NOLINT
391 this->status_set_error(LOG_STR("SHA256 verification failed"));
392 return false;
393 }
394
395 ESP_LOGI(TAG, "SHA256 verified successfully");
396 return true;
397}
398#else
400 if (this->firmware_data_ == nullptr || this->firmware_size_ == 0) {
401 ESP_LOGE(TAG, "No firmware data available");
402 this->status_set_error(LOG_STR("No firmware data available"));
403 return false;
404 }
405
406 // Verify SHA256 before writing
407 sha256::SHA256 hasher;
408 hasher.init();
409 hasher.add(this->firmware_data_, this->firmware_size_);
410 hasher.calculate();
411 if (!hasher.equals_bytes(this->firmware_sha256_.data())) {
412 ESP_LOGE(TAG, "SHA256 mismatch");
413 this->status_set_error(LOG_STR("SHA256 verification failed"));
414 return false;
415 }
416
417 ESP_LOGI(TAG, "Starting OTA update (%zu bytes)", this->firmware_size_);
418
419 esp_err_t err = esp_hosted_slave_ota_begin(); // NOLINT
420 if (err != ESP_OK) {
421 ESP_LOGE(TAG, "Failed to begin OTA: %s", esp_err_to_name(err));
422 this->status_set_error(LOG_STR("Failed to begin OTA"));
423 return false;
424 }
425
426 uint8_t chunk[CHUNK_SIZE];
427 const uint8_t *data_ptr = this->firmware_data_;
428 size_t remaining = this->firmware_size_;
429 while (remaining > 0) {
430 size_t chunk_size = std::min(remaining, static_cast<size_t>(CHUNK_SIZE));
431 memcpy(chunk, data_ptr, chunk_size);
432 err = esp_hosted_slave_ota_write(chunk, chunk_size); // NOLINT
433 if (err != ESP_OK) {
434 ESP_LOGE(TAG, "Failed to write OTA data: %s", esp_err_to_name(err));
435 esp_hosted_slave_ota_end(); // NOLINT
436 this->status_set_error(LOG_STR("Failed to write OTA data"));
437 return false;
438 }
439 data_ptr += chunk_size;
440 remaining -= chunk_size;
441 App.feed_wdt();
442 }
443
444 return true;
445}
446#endif
447
449 if (this->state_ != update::UPDATE_STATE_AVAILABLE && !force) {
450 ESP_LOGW(TAG, "Update not available");
451 return;
452 }
453
454#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
455 if (this->firmware_url_.empty()) {
456 ESP_LOGW(TAG, "No firmware URL available, run check first");
457 return;
458 }
459#endif
460
461 update::UpdateState prev_state = this->state_;
463 this->update_info_.has_progress = false;
464 this->publish_state();
465
466 watchdog::WatchdogManager watchdog(60000);
467
468#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
470#else
472#endif
473 {
474 this->state_ = prev_state;
475 this->publish_state();
476 return;
477 }
478
479 // End OTA and activate new firmware
480 esp_err_t end_err = esp_hosted_slave_ota_end(); // NOLINT
481 if (end_err != ESP_OK) {
482 ESP_LOGE(TAG, "Failed to end OTA: %s", esp_err_to_name(end_err));
483 this->state_ = prev_state;
484 this->status_set_error(LOG_STR("Failed to end OTA"));
485 this->publish_state();
486 return;
487 }
488
489 esp_err_t activate_err = esp_hosted_slave_ota_activate(); // NOLINT
490 if (activate_err != ESP_OK) {
491 ESP_LOGE(TAG, "Failed to activate OTA: %s", esp_err_to_name(activate_err));
492 this->state_ = prev_state;
493 this->status_set_error(LOG_STR("Failed to activate OTA"));
494 this->publish_state();
495 return;
496 }
497
498 // Update state
499 ESP_LOGI(TAG, "OTA update successful");
501 this->status_clear_error();
502 this->publish_state();
503
504#ifdef USE_OTA_ROLLBACK
505 // Mark the host partition as valid before rebooting, in case the safe mode
506 // timer hasn't expired yet.
507 esp_ota_mark_app_valid_cancel_rollback();
508#endif
509
510 // Schedule a restart to ensure everything is in sync
511 ESP_LOGI(TAG, "Restarting in 1 second");
512 this->set_timeout(1000, []() { App.safe_reboot(); });
513}
514
515} // namespace esphome::esp32_hosted
516#endif
void feed_wdt()
Feed the task watchdog.
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") void set_timeout(const std voi set_timeout)(const char *name, uint32_t timeout, std::function< void()> &&f)
Set a timeout function with a unique name.
Definition component.h:493
void status_clear_error()
Definition component.h:295
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") void set_interval(const std voi set_interval)(const char *name, uint32_t interval, std::function< void()> &&f)
Set an interval function with a unique name.
Definition component.h:400
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") bool cancel_interval(const std boo cancel_interval)(const char *name)
Cancel an interval function.
Definition component.h:422
bool equals_bytes(const uint8_t *expected)
Compare the hash against a provided byte-encoded hash.
Definition hash_base.h:32
virtual uint32_t get_update_interval() const
Get the update interval in ms of this sensor.
http_request::HttpRequestComponent * http_request_parent_
std::shared_ptr< HttpContainer > get(const std::string &url)
SHA256 hash implementation.
Definition sha256.h:51
void calculate() override
Definition sha256.cpp:27
void add(const uint8_t *data, size_t len) override
Definition sha256.cpp:25
void init() override
Definition sha256.cpp:19
void yield(void)
constexpr uint32_t INITIAL_CHECK_INTERVAL_ID
@ TIMEOUT
Timeout waiting for data, caller should exit loop.
@ COMPLETE
All content has been read, caller should exit loop.
@ RETRY
No data yet, already delayed, caller should continue loop.
@ DATA
Data was read, process it.
HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_t &last_data_time, uint32_t timeout_ms, bool is_read_complete)
Process a read result with timeout tracking and delay handling.
bool parse_json(const std::string &data, const json_parse_t &f)
Parse a JSON string and run the provided json parse function if it's valid.
Definition json_util.cpp:26
ESPHOME_ALWAYS_INLINE bool is_connected()
Return whether the node is connected to the network (through wifi, eth, ...)
Definition util.h:27
size_t parse_hex(const char *str, size_t length, uint8_t *data, size_t count)
Parse bytes from a hex-encoded string into a byte array.
Definition helpers.cpp:274
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:28
Application App
Global storage of Application pointer - only one Application can exist.
bool valid
static void uint32_t
uint8_t end[39]
Definition sun_gtil2.cpp:17