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