diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 6897165f1d..ac4906780e 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -87,9 +87,6 @@ void ESPHomeOTAComponent::setup() { // no wakes fire and loop() falls back to the self-disable safety net. esphome_fast_select_set_ota_listener_sock(esphome_lwip_get_sock(this->server_->get_fd())); #endif -#ifdef USE_OTA_PARTITIONS - ota::get_running_app_position(this->running_app_offset_, this->running_app_size_); -#endif } void ESPHomeOTAComponent::dump_config() { @@ -104,12 +101,16 @@ void ESPHomeOTAComponent::dump_config() { } #endif #ifdef USE_OTA_PARTITIONS + // Avoid running esp_image_verify here: it reads and checksums the entire app image, which is too + // expensive for a config dump. The address comes from a cached lookup; the precise used size is + // computed lazily by update_partition_table() the first time a partition-table OTA is requested. + const esp_partition_t *running_app_part = esp_ota_get_running_partition(); ESP_LOGCONFIG(TAG, " Partition access allowed\n" " Running app:\n" " Partition address: 0x%X\n" - " Used size: %zu bytes", - this->running_app_offset_, this->running_app_size_); + " Partition size: 0x%X bytes", + running_app_part->address, running_app_part->size); #ifdef USE_ESP32 ESP_LOGCONFIG(TAG, " Partition table:"); esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, nullptr); diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index 2da6e7dcd6..9bed9240aa 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -92,9 +92,6 @@ class ESPHomeOTAComponent final : public ota::OTAComponent { std::unique_ptr auth_buf_; #endif // USE_OTA_PASSWORD #ifdef USE_OTA_PARTITIONS - // 4-byte members first, 1-byte member last to minimize padding. - uint32_t running_app_offset_{0}; - size_t running_app_size_{0}; bool extended_proto_{false}; #endif diff --git a/esphome/components/ota/ota_backend.h b/esphome/components/ota/ota_backend.h index 41dbe5fcda..5888a8e12d 100644 --- a/esphome/components/ota/ota_backend.h +++ b/esphome/components/ota/ota_backend.h @@ -4,6 +4,8 @@ #include "esphome/core/defines.h" #include "esphome/core/helpers.h" +#include + #ifdef USE_OTA_STATE_LISTENER #include #endif @@ -53,7 +55,7 @@ enum OTAState { OTA_ERROR, }; -enum OTAType { +enum OTAType : uint8_t { OTA_TYPE_UPDATE_APP = 0x00, OTA_TYPE_UPDATE_PARTITION_TABLE = 0x01, }; diff --git a/esphome/components/ota/ota_backend_esp_idf.cpp b/esphome/components/ota/ota_backend_esp_idf.cpp index d44e2d0b27..2f75b35a8a 100644 --- a/esphome/components/ota/ota_backend_esp_idf.cpp +++ b/esphome/components/ota/ota_backend_esp_idf.cpp @@ -321,7 +321,12 @@ OTAResponseTypes IDFOTABackend::update_partition_table() { return OTA_RESPONSE_ERROR_PARTITION_TABLE_VERIFY; } - ESP_LOGW(TAG, "Checks passed, starting partition table update. Don't remove power until it is completed!"); + // Past this point any failure (power loss, watchdog reset, write error after the table has been + // partially erased) can leave the device unable to boot. Logged at ERROR severity so the message + // is visible in default log filters. + ESP_LOGE(TAG, "Starting partition table update.\n" + " DO NOT REMOVE POWER until the device reboots successfully.\n" + " Loss of power during this operation may permanently brick the device."); // Deinitialize NVS to prevent unwanted flash writes nvs_flash_deinit(); diff --git a/esphome/espota2.py b/esphome/espota2.py index e8d27d69fa..9cab354514 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -73,6 +73,71 @@ _AUTH_METHODS: dict[int, tuple[Callable[..., Any], int, str]] = { RESPONSE_REQUEST_AUTH: (hashlib.md5, 32, "MD5"), } +# Error response code -> human-readable message (without the "Error: " prefix; check_error() +# prepends it uniformly). Looked up by check_error() to translate a single byte from the device +# into an OTAError. Add new error codes here rather than extending the if-chain in check_error(). +_ERROR_MESSAGES: dict[int, str] = { + RESPONSE_ERROR_MAGIC: "Invalid magic byte", + RESPONSE_ERROR_UPDATE_PREPARE: ( + "Couldn't prepare flash memory for update. Is the binary too big? " + "Please try restarting the ESP." + ), + RESPONSE_ERROR_AUTH_INVALID: "Authentication invalid. Is the password correct?", + RESPONSE_ERROR_WRITING_FLASH: ( + "Writing OTA data to flash memory failed. See USB logs for more information." + ), + RESPONSE_ERROR_UPDATE_END: ( + "Finishing update failed. See the MQTT/USB logs for more information." + ), + RESPONSE_ERROR_INVALID_BOOTSTRAPPING: ( + "Please press the reset button on the ESP. A manual reset is " + "required on the first OTA-Update after flashing via USB." + ), + RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG: ( + "ESP has been flashed with wrong flash size. Please choose the " + "correct 'board' option (esp01_1m always works) and then flash over USB." + ), + RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG: ( + "ESP does not have the requested flash size (wrong board). Please " + "choose the correct 'board' option (esp01_1m always works) and try " + "uploading again." + ), + RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE: ( + "ESP does not have enough space to store OTA file. Please try " + "flashing a minimal firmware (remove everything except ota)" + ), + RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE: ( + "The OTA partition on the ESP is too small. ESPHome needs to resize " + "this partition, please flash over USB." + ), + RESPONSE_ERROR_NO_UPDATE_PARTITION: ( + "The OTA partition on the ESP couldn't be found. ESPHome needs to " + "create this partition, please flash over USB." + ), + RESPONSE_ERROR_MD5_MISMATCH: ( + "Application MD5 code mismatch. Please try again " + "or flash over USB with a good quality cable." + ), + RESPONSE_ERROR_SIGNATURE_INVALID: ( + "Firmware signature verification failed. The firmware was not signed " + "with the correct key. Ensure the signing key matches the one used to build " + "the firmware currently running on the device." + ), + RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE: ( + "The requested OTA type is not supported by the device." + ), + RESPONSE_ERROR_PARTITION_TABLE_VERIFY: ( + "The partition table update could not be verified. No changes were " + "made to the flash content. Check the logs for more information and retry." + ), + RESPONSE_ERROR_PARTITION_TABLE_UPDATE: ( + "An error occurred while updating the partition table. The device may " + "not be able to reboot to a working application. Check the logs and retry " + "the update without rebooting the device." + ), + RESPONSE_ERROR_UNKNOWN: "Unknown error from ESP", +} + class OTAError(EsphomeError): pass @@ -148,82 +213,9 @@ def check_error(data: list[int] | bytes, expect: int | list[int] | None) -> None "a network issue, or the connection was interrupted." ) dat = data[0] - if dat == RESPONSE_ERROR_MAGIC: - raise OTAError("Error: Invalid magic byte") - if dat == RESPONSE_ERROR_UPDATE_PREPARE: - raise OTAError( - "Error: Couldn't prepare flash memory for update. Is the binary too big? " - "Please try restarting the ESP." - ) - if dat == RESPONSE_ERROR_AUTH_INVALID: - raise OTAError("Error: Authentication invalid. Is the password correct?") - if dat == RESPONSE_ERROR_WRITING_FLASH: - raise OTAError( - "Error: Writing OTA data to flash memory failed. See USB logs for more " - "information." - ) - if dat == RESPONSE_ERROR_UPDATE_END: - raise OTAError( - "Error: Finishing update failed. See the MQTT/USB logs for more " - "information." - ) - if dat == RESPONSE_ERROR_INVALID_BOOTSTRAPPING: - raise OTAError( - "Error: Please press the reset button on the ESP. A manual reset is " - "required on the first OTA-Update after flashing via USB." - ) - if dat == RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG: - raise OTAError( - "Error: ESP has been flashed with wrong flash size. Please choose the " - "correct 'board' option (esp01_1m always works) and then flash over USB." - ) - if dat == RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG: - raise OTAError( - "Error: ESP does not have the requested flash size (wrong board). Please " - "choose the correct 'board' option (esp01_1m always works) and try " - "uploading again." - ) - if dat == RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE: - raise OTAError( - "Error: ESP does not have enough space to store OTA file. Please try " - "flashing a minimal firmware (remove everything except ota)" - ) - if dat == RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE: - raise OTAError( - "Error: The OTA partition on the ESP is too small. ESPHome needs to resize " - "this partition, please flash over USB." - ) - if dat == RESPONSE_ERROR_NO_UPDATE_PARTITION: - raise OTAError( - "Error: The OTA partition on the ESP couldn't be found. ESPHome needs to create " - "this partition, please flash over USB." - ) - if dat == RESPONSE_ERROR_MD5_MISMATCH: - raise OTAError( - "Error: Application MD5 code mismatch. Please try again " - "or flash over USB with a good quality cable." - ) - if dat == RESPONSE_ERROR_SIGNATURE_INVALID: - raise OTAError( - "Error: Firmware signature verification failed. The firmware was not signed " - "with the correct key. Ensure the signing key matches the one used to build " - "the firmware currently running on the device." - ) - if dat == RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE: - raise OTAError("Error: The requested OTA type is not supported by the device.") - if dat == RESPONSE_ERROR_PARTITION_TABLE_VERIFY: - raise OTAError( - "Error: The partition table update could not be verified. No changes were " - "made to the flash content. Check the logs for more information and retry." - ) - if dat == RESPONSE_ERROR_PARTITION_TABLE_UPDATE: - raise OTAError( - "Error: An error occurred while updating the partition table. The device may not " - "be able to reboot to a working application. Check the logs and retry the update " - "without rebooting the device." - ) - if dat == RESPONSE_ERROR_UNKNOWN: - raise OTAError("Unknown error from ESP") + error_msg = _ERROR_MESSAGES.get(dat) + if error_msg is not None: + raise OTAError(f"Error: {error_msg}") if not isinstance(expect, (list, tuple)): expect = [expect] if dat not in expect: