|
| 1 | +/* |
| 2 | + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD |
| 3 | + * |
| 4 | + * SPDX-License-Identifier: Unlicense OR CC0-1.0 |
| 5 | + */ |
| 6 | +#include "manual_ota.hpp" |
| 7 | +#include "esp_log.h" |
| 8 | +#include "esp_ota_ops.h" |
| 9 | +#include "esp_app_format.h" |
| 10 | +#include "esp_http_client.h" |
| 11 | +#include "esp_partition.h" |
| 12 | +#include "esp_transport_tcp.h" |
| 13 | +#include "transport_batch_tls.hpp" |
| 14 | + |
| 15 | +static const char *TAG = "manual_ota"; |
| 16 | + |
| 17 | +bool manual_ota::begin() |
| 18 | +{ |
| 19 | + if (status != state::UNDEF) { |
| 20 | + ESP_LOGE(TAG, "Invalid state"); |
| 21 | + return false; |
| 22 | + } |
| 23 | + status = state::INIT; |
| 24 | + esp_transport_handle_t tcp = esp_transport_tcp_init(); |
| 25 | + ssl_ = esp_transport_batch_tls_init(tcp, max_buffer_size_); |
| 26 | + |
| 27 | + esp_http_client_config_t config = { }; |
| 28 | + config.skip_cert_common_name_check = true; |
| 29 | + config.url = uri_; |
| 30 | + config.transport = ssl_; |
| 31 | + const esp_partition_t *configured = esp_ota_get_boot_partition(); |
| 32 | + const esp_partition_t *running = esp_ota_get_running_partition(); |
| 33 | + |
| 34 | + if (configured != running) { |
| 35 | + ESP_LOGE(TAG, "Configured OTA boot partition at offset 0x%08" PRIx32 ", but running from offset 0x%08" PRIx32, configured->address, running->address); |
| 36 | + return false; |
| 37 | + } |
| 38 | + |
| 39 | + http_ = esp_http_client_init(&config); |
| 40 | + if (http_ == nullptr) { |
| 41 | + ESP_LOGE(TAG, "Failed to initialise HTTP connection"); |
| 42 | + return false; |
| 43 | + } |
| 44 | + esp_http_client_set_method(http_, HTTP_METHOD_HEAD); |
| 45 | + esp_err_t err = esp_http_client_perform(http_); |
| 46 | + if (err == ESP_OK) { |
| 47 | + int http_status = esp_http_client_get_status_code(http_); |
| 48 | + if (http_status != HttpStatus_Ok) { |
| 49 | + ESP_LOGE(TAG, "Received incorrect http status %d", http_status); |
| 50 | + return false; |
| 51 | + } |
| 52 | + } else { |
| 53 | + ESP_LOGE(TAG, "ESP HTTP client perform failed: %d", err); |
| 54 | + return false; |
| 55 | + } |
| 56 | + image_length_ = esp_http_client_get_content_length(http_); |
| 57 | + ESP_LOGI(TAG, "image_length = %lld", image_length_); |
| 58 | + esp_http_client_close(http_); |
| 59 | + |
| 60 | + if (image_length_ > size_) { |
| 61 | + char *header_val = nullptr; |
| 62 | + asprintf(&header_val, "bytes=0-%d", max_buffer_size_ - 1); |
| 63 | + if (header_val == nullptr) { |
| 64 | + ESP_LOGE(TAG, "Failed to allocate memory for HTTP header"); |
| 65 | + return false; |
| 66 | + } |
| 67 | + esp_http_client_set_header(http_, "Range", header_val); |
| 68 | + free(header_val); |
| 69 | + } |
| 70 | + esp_http_client_set_method(http_, HTTP_METHOD_GET); |
| 71 | + |
| 72 | + partition_ = esp_ota_get_next_update_partition(nullptr); |
| 73 | + if (partition_ == nullptr) { |
| 74 | + ESP_LOGE(TAG, "Invalid update partition"); |
| 75 | + return false; |
| 76 | + } |
| 77 | + ESP_LOGI(TAG, "Writing to partition subtype %d at offset 0x%" PRIx32, partition_->subtype, partition_->address); |
| 78 | + |
| 79 | + file_length_ = 0; |
| 80 | + reconnect_attempts_ = 0; |
| 81 | + buffer_.resize(max_buffer_size_); |
| 82 | + status = state::IMAGE_CHECK; |
| 83 | + return true; |
| 84 | +} |
| 85 | + |
| 86 | +bool manual_ota::perform() |
| 87 | +{ |
| 88 | + if (status != state::IMAGE_CHECK && status != state::START) { |
| 89 | + ESP_LOGE(TAG, "Invalid state"); |
| 90 | + return false; |
| 91 | + } |
| 92 | + esp_err_t err = esp_http_client_open(http_, 0); |
| 93 | + if (err != ESP_OK) { |
| 94 | + if (image_length_ == file_length_) { |
| 95 | + status = state::END; |
| 96 | + return false; |
| 97 | + } |
| 98 | + |
| 99 | + esp_http_client_close(http_); |
| 100 | + ESP_LOGI(TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err)); |
| 101 | + if (reconnect_attempts_++ < max_reconnect_attempts_) { |
| 102 | + if (prepare_reconnect()) { |
| 103 | + return true; // will retry in the next iteration |
| 104 | + } |
| 105 | + } |
| 106 | + return fail_cleanup(); |
| 107 | + } |
| 108 | + esp_http_client_fetch_headers(http_); |
| 109 | + |
| 110 | + int batch_len = esp_transport_batch_tls_pre_read(ssl_, max_buffer_size_, timeout_ * 1000); |
| 111 | + if (batch_len < 0) { |
| 112 | + ESP_LOGE(TAG, "Error: Failed to pre-read plain text data!"); |
| 113 | + return fail_cleanup(); |
| 114 | + } |
| 115 | + |
| 116 | + int data_read = esp_http_client_read(http_, buffer_.data(), batch_len); |
| 117 | + |
| 118 | + if (data_read < 0) { |
| 119 | + ESP_LOGE(TAG, "Error: SSL data read error"); |
| 120 | + return fail_cleanup(); |
| 121 | + } else if (data_read > 0) { |
| 122 | + esp_http_client_close(http_); |
| 123 | + |
| 124 | + if (status == state::IMAGE_CHECK) { |
| 125 | + esp_app_desc_t new_app_info; |
| 126 | + if (data_read > sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t) + sizeof(esp_app_desc_t)) { |
| 127 | + // check current version with downloading |
| 128 | + memcpy(&new_app_info, &buffer_[sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t)], sizeof(esp_app_desc_t)); |
| 129 | + ESP_LOGI(TAG, "New firmware version: %s", new_app_info.version); |
| 130 | + |
| 131 | + esp_app_desc_t running_app_info; |
| 132 | + const esp_partition_t *running = esp_ota_get_running_partition(); |
| 133 | + if (esp_ota_get_partition_description(running, &running_app_info) == ESP_OK) { |
| 134 | + ESP_LOGI(TAG, "Running firmware version: %s", running_app_info.version); |
| 135 | + } |
| 136 | + |
| 137 | + const esp_partition_t *last_invalid_app = esp_ota_get_last_invalid_partition(); |
| 138 | + esp_app_desc_t invalid_app_info; |
| 139 | + if (esp_ota_get_partition_description(last_invalid_app, &invalid_app_info) == ESP_OK) { |
| 140 | + ESP_LOGI(TAG, "Last invalid firmware version: %s", invalid_app_info.version); |
| 141 | + } |
| 142 | + |
| 143 | + // check current version with last invalid partition |
| 144 | + if (last_invalid_app != NULL) { |
| 145 | + if (memcmp(invalid_app_info.version, new_app_info.version, sizeof(new_app_info.version)) == 0) { |
| 146 | + ESP_LOGW(TAG, "New version is the same as invalid version."); |
| 147 | + ESP_LOGW(TAG, "Previously, there was an attempt to launch the firmware with %s version, but it failed.", invalid_app_info.version); |
| 148 | + ESP_LOGW(TAG, "The firmware has been rolled back to the previous version."); |
| 149 | + return fail_cleanup(); |
| 150 | + } |
| 151 | + } |
| 152 | + |
| 153 | + status = state::START; |
| 154 | + err = esp_ota_begin(partition_, OTA_WITH_SEQUENTIAL_WRITES, &update_handle_); |
| 155 | + if (err != ESP_OK) { |
| 156 | + ESP_LOGE(TAG, "esp_ota_begin failed (%s)", esp_err_to_name(err)); |
| 157 | + esp_ota_abort(update_handle_); |
| 158 | + return fail_cleanup(); |
| 159 | + } |
| 160 | + ESP_LOGI(TAG, "esp_ota_begin succeeded"); |
| 161 | + } else { |
| 162 | + ESP_LOGE(TAG, "Received chunk doesn't contain app descriptor"); |
| 163 | + esp_ota_abort(update_handle_); |
| 164 | + return fail_cleanup(); |
| 165 | + } |
| 166 | + } |
| 167 | + err = esp_ota_write(update_handle_, (const void *)buffer_.data(), data_read); |
| 168 | + if (err != ESP_OK) { |
| 169 | + esp_ota_abort(update_handle_); |
| 170 | + return fail_cleanup(); |
| 171 | + } |
| 172 | + file_length_ += data_read; |
| 173 | + ESP_LOGI(TAG, "Written image length %d", file_length_); |
| 174 | + |
| 175 | + if (image_length_ == file_length_) { |
| 176 | + status = state::END; |
| 177 | + return false; |
| 178 | + } |
| 179 | + |
| 180 | + if (!prepare_reconnect()) { |
| 181 | + esp_ota_abort(update_handle_); |
| 182 | + return fail_cleanup(); |
| 183 | + } |
| 184 | + |
| 185 | + } else if (data_read == 0) { |
| 186 | + if (file_length_ == 0) { |
| 187 | + // Try to handle possible HTTP redirections |
| 188 | + int status_code = esp_http_client_get_status_code(http_); |
| 189 | + ESP_LOGW(TAG, "Status code: %d", status_code); |
| 190 | + err = esp_http_client_set_redirection(http_); |
| 191 | + if (err != ESP_OK) { |
| 192 | + ESP_LOGE(TAG, "URL redirection Failed"); |
| 193 | + esp_ota_abort(update_handle_); |
| 194 | + return fail_cleanup(); |
| 195 | + } |
| 196 | + |
| 197 | + err = esp_http_client_open(http_, 0); |
| 198 | + if (err != ESP_OK) { |
| 199 | + ESP_LOGE(TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err)); |
| 200 | + return fail_cleanup(); |
| 201 | + } |
| 202 | + esp_http_client_fetch_headers(http_); |
| 203 | + } |
| 204 | + } |
| 205 | + |
| 206 | + return true; |
| 207 | +} |
| 208 | + |
| 209 | +bool manual_ota::prepare_reconnect() |
| 210 | +{ |
| 211 | + esp_http_client_set_method(http_, HTTP_METHOD_GET); |
| 212 | + char *header_val = nullptr; |
| 213 | + if ((image_length_ - file_length_) > max_buffer_size_) { |
| 214 | + asprintf(&header_val, "bytes=%d-%d", file_length_, (file_length_ + max_buffer_size_ - 1)); |
| 215 | + } else { |
| 216 | + asprintf(&header_val, "bytes=%d-", file_length_); |
| 217 | + } |
| 218 | + if (header_val == nullptr) { |
| 219 | + ESP_LOGE(TAG, "Failed to allocate memory for HTTP header"); |
| 220 | + return false; |
| 221 | + } |
| 222 | + esp_http_client_set_header(http_, "Range", header_val); |
| 223 | + free(header_val); |
| 224 | + return true; |
| 225 | +} |
| 226 | + |
| 227 | +bool manual_ota::fail_cleanup() |
| 228 | +{ |
| 229 | + esp_http_client_close(http_); |
| 230 | + esp_http_client_cleanup(http_); |
| 231 | + status = state::FAIL; |
| 232 | + return false; |
| 233 | +} |
| 234 | + |
| 235 | +bool manual_ota::end() |
| 236 | +{ |
| 237 | + if (status == state::END) { |
| 238 | + if (!esp_http_client_is_complete_data_received(http_)) { |
| 239 | + ESP_LOGE(TAG, "Error in receiving complete file"); |
| 240 | + return fail_cleanup(); |
| 241 | + } |
| 242 | + esp_err_t err = esp_ota_end(update_handle_); |
| 243 | + if (err != ESP_OK) { |
| 244 | + if (err == ESP_ERR_OTA_VALIDATE_FAILED) { |
| 245 | + ESP_LOGE(TAG, "Image validation failed, image is corrupted"); |
| 246 | + } else { |
| 247 | + ESP_LOGE(TAG, "esp_ota_end failed (%s)!", esp_err_to_name(err)); |
| 248 | + } |
| 249 | + return fail_cleanup(); |
| 250 | + } |
| 251 | + err = esp_ota_set_boot_partition(partition_); |
| 252 | + if (err != ESP_OK) { |
| 253 | + ESP_LOGE(TAG, "esp_ota_set_boot_partition failed (%s)!", esp_err_to_name(err)); |
| 254 | + return fail_cleanup(); |
| 255 | + } |
| 256 | + return true; |
| 257 | + } |
| 258 | + return false; |
| 259 | +} |
0 commit comments