Branch data Line data Source code
1 : : // components/ota_manager/src/ota_manager.cpp
2 : : #include "ota_manager.hpp"
3 : : #include "esp_log.h"
4 : : #include "manifest_parser.hpp"
5 : : #include "version_helper.hpp"
6 : : #include <iomanip>
7 : : #include <sstream>
8 : :
9 : : static const char* TAG = "OtaManager";
10 : :
11 : : namespace {
12 : : static constexpr uint32_t OTA_START_BIT = 0x01;
13 : : static constexpr uint32_t OTA_STOP_BIT = 0x02;
14 : : static constexpr uint32_t OTA_CANCEL_BIT = 0x04;
15 : :
16 : 11 : bool is_version_newer(const OtaVersion& current, const OtaVersion& manifest, bool allow_same)
17 : : {
18 [ + + ]: 11 : if (manifest.major > current.major)
19 : : return true;
20 [ + + ]: 5 : if (manifest.major < current.major)
21 : : return false;
22 [ + - ]: 2 : if (manifest.minor > current.minor)
23 : : return true;
24 [ + - ]: 2 : if (manifest.minor < current.minor)
25 : : return false;
26 [ + - ]: 2 : if (manifest.patch > current.patch)
27 : : return true;
28 [ - + ]: 2 : if (manifest.patch < current.patch)
29 : 0 : return false;
30 : : return allow_same;
31 : : }
32 : :
33 : 3 : std::string bytes_to_hex(const uint8_t* bytes, size_t len)
34 : : {
35 : 3 : std::stringstream ss;
36 : 3 : ss << std::hex << std::setfill('0');
37 [ + + ]: 99 : for (size_t i = 0; i < len; ++i) {
38 : 96 : ss << std::setw(2) << static_cast<int>(bytes[i]);
39 : : }
40 : 3 : return ss.str();
41 : 3 : }
42 : : } // namespace
43 : :
44 : 86 : OtaManager::OtaManager(const OtaDependencies& deps)
45 : 86 : : deps_(deps)
46 : 86 : , status_(OtaStatus::IDLE)
47 : : {
48 : 86 : }
49 : :
50 : 86 : OtaManager::~OtaManager()
51 : : {
52 : : // deinit();
53 : 86 : }
54 : :
55 : 54 : bool OtaManager::init(const OtaConfig& config)
56 : : {
57 : 54 : config_ = config;
58 : :
59 [ + + ]: 54 : if (state_mutex_ == nullptr) {
60 : 53 : state_mutex_ = deps_.task_scheduler.mutex_create();
61 [ + + ]: 53 : if (state_mutex_ == nullptr) {
62 : : ESP_LOGE(TAG, "Failed to create state mutex");
63 : : return false;
64 : : }
65 : : }
66 : :
67 [ + + ]: 53 : if (shutdown_done_ == nullptr) {
68 : 52 : shutdown_done_ = deps_.task_scheduler.semaphore_binary_create();
69 [ + + ]: 52 : if (shutdown_done_ == nullptr) {
70 : 1 : ESP_LOGE(TAG, "Failed to create shutdown semaphore");
71 : 1 : deps_.task_scheduler.semaphore_delete(state_mutex_);
72 : 1 : state_mutex_ = nullptr;
73 : 1 : return false;
74 : : }
75 : : }
76 : :
77 : 52 : set_status(OtaStatus::IDLE);
78 : 52 : return true;
79 : : }
80 : :
81 : 10 : bool OtaManager::deinit()
82 : : {
83 : 10 : deps_.ota_session.abort();
84 : 10 : bool shutdown_clean = true;
85 : :
86 : 10 : TaskHandle_t worker_handle = nullptr;
87 [ + + + - ]: 10 : if (state_mutex_ != nullptr && deps_.task_scheduler.semaphore_take(state_mutex_, portMAX_DELAY) == pdPASS) {
88 : 9 : worker_handle = ota_task_handle_;
89 : 9 : deps_.task_scheduler.semaphore_give(state_mutex_);
90 : : }
91 : :
92 [ + + ]: 9 : if (worker_handle != nullptr) {
93 : 4 : deps_.task_scheduler.notify_task(worker_handle, OTA_STOP_BIT, eSetBits);
94 : :
95 [ + - ]: 4 : if (shutdown_done_ != nullptr) {
96 [ + + ]: 4 : if (deps_.task_scheduler.semaphore_take(shutdown_done_, pdMS_TO_TICKS(1000)) != pdPASS) {
97 : : ESP_LOGW(TAG, "OTA worker did not stop in time");
98 : : shutdown_clean = false;
99 : : }
100 : : }
101 : : }
102 : :
103 [ + + ]: 8 : if (shutdown_clean && shutdown_done_ != nullptr) {
104 : 7 : deps_.task_scheduler.semaphore_delete(shutdown_done_);
105 : 7 : shutdown_done_ = nullptr;
106 : : }
107 : :
108 [ + + ]: 8 : if (shutdown_clean && state_mutex_ != nullptr) {
109 : 7 : deps_.task_scheduler.semaphore_delete(state_mutex_);
110 : 7 : state_mutex_ = nullptr;
111 : : }
112 : :
113 : 8 : if (shutdown_clean) {
114 : 8 : status_ = OtaStatus::IDLE;
115 : : }
116 : 10 : return shutdown_clean;
117 : : }
118 : :
119 : 19 : bool OtaManager::start_ota()
120 : : {
121 [ + + + + ]: 19 : if (get_status() != OtaStatus::IDLE && get_status() != OtaStatus::FAILED) {
122 : : return false;
123 : : }
124 : :
125 [ + - ]: 16 : if (state_mutex_ == nullptr) return false;
126 [ + - ]: 16 : if (deps_.task_scheduler.semaphore_take(state_mutex_, portMAX_DELAY) != pdPASS) return false;
127 : :
128 [ + + ]: 16 : if (ota_task_handle_ == nullptr) {
129 : 30 : BaseType_t ret = deps_.task_scheduler.create_task(
130 : : ota_task_func, "ota_worker", config_.task_stack_size,
131 : 15 : this, config_.task_priority, &ota_task_handle_);
132 [ + + ]: 15 : if (ret != pdPASS) {
133 : 1 : deps_.task_scheduler.semaphore_give(state_mutex_);
134 : 1 : ESP_LOGE(TAG, "Failed to create OTA task");
135 : 1 : return false;
136 : : }
137 : : }
138 : : // Notifica enquanto ainda segura o mutex — handle está garantidamente válido
139 : 15 : deps_.task_scheduler.notify_task(ota_task_handle_, OTA_START_BIT, eSetBits);
140 : 15 : deps_.task_scheduler.semaphore_give(state_mutex_);
141 : 15 : return true;
142 : : }
143 : :
144 : 4 : void OtaManager::cancel_ota()
145 : : {
146 : 4 : deps_.ota_session.abort();
147 : :
148 : 4 : TaskHandle_t worker_handle = nullptr;
149 [ + - + - ]: 4 : if (state_mutex_ != nullptr && deps_.task_scheduler.semaphore_take(state_mutex_, portMAX_DELAY) == pdPASS) {
150 : 4 : worker_handle = ota_task_handle_;
151 : 4 : deps_.task_scheduler.semaphore_give(state_mutex_);
152 : : }
153 : :
154 [ + + ]: 4 : if (worker_handle != nullptr) {
155 : 2 : deps_.task_scheduler.notify_task(worker_handle, OTA_CANCEL_BIT, eSetBits);
156 : : }
157 : 4 : }
158 : :
159 : 106 : OtaStatus OtaManager::get_status() const
160 : : {
161 [ + + ]: 106 : if (state_mutex_ == nullptr) {
162 : 6 : return status_;
163 : : }
164 : :
165 [ + + ]: 100 : if (deps_.task_scheduler.semaphore_take(state_mutex_, portMAX_DELAY) != pdPASS) {
166 : 1 : return status_;
167 : : }
168 : :
169 : 99 : OtaStatus current_status = status_;
170 : 99 : deps_.task_scheduler.semaphore_give(state_mutex_);
171 : 99 : return current_status;
172 : : }
173 : :
174 : 2 : bool OtaManager::check_pending_verify() const
175 : : {
176 : 2 : return deps_.rollback_manager.is_pending_verify();
177 : : }
178 : :
179 : 2 : bool OtaManager::confirm_app_valid()
180 : : {
181 : 2 : return deps_.rollback_manager.mark_app_valid() == ESP_OK;
182 : : }
183 : :
184 : 1 : void OtaManager::rollback_and_reboot()
185 : : {
186 : 1 : deps_.rollback_manager.rollback_and_reboot();
187 : 1 : }
188 : :
189 : 7 : void OtaManager::ota_task_func(void* pvParameters)
190 : : {
191 : 7 : OtaManager* self = static_cast<OtaManager*>(pvParameters);
192 : 7 : self->ota_task();
193 : 0 : }
194 : :
195 : 7 : void OtaManager::ota_task()
196 : : {
197 : 7 : uint32_t notifications = 0;
198 : 7 : bool should_exit = false;
199 : :
200 : 7 : while (!should_exit) {
201 : : // Variable blocking: wait forever if idle/failed/pending, otherwise poll
202 : 26 : OtaStatus current_status = get_status();
203 : 52 : TickType_t wait_time =
204 [ - + + ]: 26 : (current_status == OtaStatus::IDLE || current_status == OtaStatus::FAILED || current_status == OtaStatus::PENDING_VERIFY)
205 : : ? portMAX_DELAY
206 : : : 0;
207 : :
208 : : // Peek at notifications
209 [ + + ]: 26 : if (deps_.task_scheduler.task_notify_wait(0, 0, ¬ifications, wait_time) == pdPASS) {
210 [ + + ]: 9 : if ((notifications & OTA_STOP_BIT) == OTA_STOP_BIT) {
211 : 1 : deps_.ota_session.abort(); // Ensure session is closed if task stops
212 : 1 : should_exit = true;
213 : 1 : continue;
214 : : }
215 : :
216 [ + + ]: 8 : if ((notifications & OTA_CANCEL_BIT) == OTA_CANCEL_BIT) {
217 : 1 : ESP_LOGI(TAG, "OTA cancellation requested");
218 : 1 : deps_.ota_session.abort();
219 : 1 : set_status(OtaStatus::IDLE);
220 : : // Clear the bits we just processed
221 : 1 : deps_.task_scheduler.task_notify_wait(0, (OTA_CANCEL_BIT | OTA_START_BIT), ¬ifications, 0);
222 : 1 : continue;
223 : 1 : }
224 : :
225 [ + - ]: 7 : if ((notifications & OTA_START_BIT) == OTA_START_BIT) {
226 : 7 : OtaStatus s = get_status();
227 [ + - ]: 7 : if (s == OtaStatus::IDLE || s == OtaStatus::FAILED) {
228 : 7 : set_status(OtaStatus::MANIFEST_FETCH);
229 : : }
230 : : // Clear the start bit
231 : 7 : deps_.task_scheduler.task_notify_wait(0, OTA_START_BIT, ¬ifications, 0);
232 : : }
233 : : }
234 : :
235 : 19 : OtaStepResult step_res = OtaStepResult::IN_PROGRESS;
236 [ + + + + : 19 : switch (get_status()) {
+ - ]
237 : 7 : case OtaStatus::MANIFEST_FETCH:
238 : 7 : step_res = handle_manifest_state();
239 [ + + ]: 7 : if (step_res == OtaStepResult::SUCCESS) {
240 : 5 : set_status(OtaStatus::VERSION_CHECK);
241 : : }
242 [ + - ]: 2 : else if (step_res == OtaStepResult::FAILED) {
243 : 2 : set_status(OtaStatus::FAILED);
244 : : }
245 : : break;
246 : :
247 : 5 : case OtaStatus::VERSION_CHECK:
248 : 5 : step_res = handle_version_state();
249 [ + + ]: 5 : if (step_res == OtaStepResult::SUCCESS) {
250 : 4 : set_status(OtaStatus::DOWNLOADING);
251 : : }
252 [ + - ]: 1 : else if (step_res == OtaStepResult::FAILED) {
253 : 1 : set_status(OtaStatus::FAILED);
254 : : }
255 : : break;
256 : :
257 : 4 : case OtaStatus::DOWNLOADING:
258 : 4 : step_res = handle_download_state();
259 [ + + ]: 4 : if (step_res == OtaStepResult::SUCCESS) {
260 : 2 : set_status(OtaStatus::VERIFYING);
261 : : }
262 [ + + ]: 2 : else if (step_res == OtaStepResult::FAILED) {
263 : 1 : set_status(OtaStatus::FAILED);
264 : : }
265 : : break;
266 : :
267 : 2 : case OtaStatus::VERIFYING:
268 : 2 : step_res = handle_verification_state();
269 [ + + ]: 2 : if (step_res == OtaStepResult::SUCCESS) {
270 : 1 : set_status(OtaStatus::READY_TO_RESTART);
271 : : }
272 [ + - ]: 1 : else if (step_res == OtaStepResult::FAILED) {
273 : 1 : set_status(OtaStatus::FAILED);
274 : : }
275 : : break;
276 : :
277 : 1 : case OtaStatus::READY_TO_RESTART:
278 [ - + ]: 1 : if (config_.restart_on_success) {
279 : 0 : ESP_LOGI(TAG, "OTA Successful. Restarting...");
280 : 0 : deps_.system.restart();
281 : : }
282 : : should_exit = true;
283 : : break;
284 : :
285 : : default:
286 : : break;
287 : : }
288 : : }
289 : :
290 : 2 : ESP_LOGI(TAG, "OTA Task exiting.");
291 : 2 : finalize_worker_shutdown();
292 : 2 : deps_.task_scheduler.delete_task(nullptr);
293 : 0 : }
294 : :
295 : 19 : bool OtaManager::validate_url(const std::string& url) const
296 : : {
297 [ + + ]: 19 : if (config_.security.allow_http_during_development) {
298 : : return true;
299 : : }
300 : :
301 [ + + ]: 3 : if (url.rfind("https://", 0) == 0) {
302 : 1 : return true;
303 : : }
304 : :
305 : : ESP_LOGE(TAG, "Security policy violation: insecure URL not allowed: %s", url.c_str());
306 : : return false;
307 : : }
308 : :
309 : : // ==================================================================================
310 : : // Private methods
311 : : // ==================================================================================
312 : :
313 : : /**
314 : : * @brief Handles the manifest fetching state.
315 : : * @note This is a synchronous operation and never returns IN_PROGRESS.
316 : : */
317 : 12 : OtaStepResult OtaManager::handle_manifest_state()
318 : : {
319 [ + + ]: 12 : if (!validate_url(config_.manifest_url)) {
320 : : return OtaStepResult::FAILED;
321 : : }
322 : :
323 : 11 : std::string manifest_content;
324 [ + + ]: 11 : if (deps_.http_client.fetch(config_.manifest_url, manifest_content, config_.transport.manifest_timeout_ms) != ESP_OK) {
325 : : ESP_LOGE(TAG, "Failed to fetch manifest from %s", config_.manifest_url.c_str());
326 : : return OtaStepResult::FAILED;
327 : : }
328 : :
329 : 9 : auto manifest_opt = deps_.manifest_parser.parse(manifest_content);
330 [ + + ]: 9 : if (!manifest_opt.has_value()) {
331 : : ESP_LOGE(TAG, "Failed to parse manifest JSON");
332 : : return OtaStepResult::FAILED;
333 : : }
334 : :
335 : 7 : manifest_ = manifest_opt.value();
336 : 7 : ESP_LOGI(
337 : : TAG,
338 : : "Manifest fetched: version %u.%u.%u for %s",
339 : : manifest_.version.major,
340 : : manifest_.version.minor,
341 : : manifest_.version.patch,
342 : : manifest_.device_type.c_str());
343 : 7 : return OtaStepResult::SUCCESS;
344 : 20 : }
345 : :
346 : : /**
347 : : * @brief Handles the version check state.
348 : : * @note This is a synchronous operation and never returns IN_PROGRESS.
349 : : */
350 : 11 : OtaStepResult OtaManager::handle_version_state()
351 : : {
352 : : // 1. Validate Device Type
353 [ + + ]: 11 : if (manifest_.device_type != config_.device_type) {
354 : : ESP_LOGE(
355 : : TAG,
356 : : "Device type mismatch: manifest=%s, config=%s",
357 : : manifest_.device_type.c_str(),
358 : : config_.device_type.c_str());
359 : : return OtaStepResult::FAILED;
360 : : }
361 : :
362 : : // 2. Fetch current version
363 : 10 : const esp_app_desc_t* running_app = deps_.system.get_running_app_desc();
364 : 10 : auto current_v_opt = VersionHelper::parse(running_app->version);
365 [ + + ]: 10 : if (!current_v_opt.has_value()) {
366 : : ESP_LOGE(TAG, "Failed to parse current version string: %s", running_app->version);
367 : : return OtaStepResult::FAILED;
368 : : }
369 : :
370 : : // 3. Compare versions
371 [ + + ]: 9 : if (!is_version_newer(current_v_opt.value(), manifest_.version, config_.allow_same_version)) {
372 : 3 : ESP_LOGW(
373 : : TAG,
374 : : "Version is not newer. Current: %s, Manifest: %u.%u.%u",
375 : : running_app->version,
376 : : manifest_.version.major,
377 : : manifest_.version.minor,
378 : : manifest_.version.patch);
379 : 3 : return OtaStepResult::FAILED;
380 : : }
381 : :
382 : : ESP_LOGI(TAG, "Version check passed. Proceeding with download.");
383 : : return OtaStepResult::SUCCESS;
384 : : }
385 : :
386 : : /**
387 : : * @brief Handles the firmware download state.
388 : : * @note This operation is iterative and returns IN_PROGRESS until complete.
389 : : */
390 : 13 : OtaStepResult OtaManager::handle_download_state()
391 : : {
392 : : // 1. Setup session if not active
393 [ + + ]: 13 : if (!deps_.ota_session.is_active()) {
394 [ + + ]: 7 : if (!validate_url(manifest_.firmware_url)) {
395 : 6 : return OtaStepResult::FAILED;
396 : : }
397 : :
398 : 6 : OtaDownloadRequest request;
399 : 6 : request.url = manifest_.firmware_url;
400 : 6 : request.timeout_ms = config_.transport.firmware_timeout_ms;
401 : :
402 [ + + ]: 6 : if (deps_.ota_session.begin(request) != ESP_OK) {
403 : 2 : ESP_LOGE(TAG, "Failed to begin OTA session");
404 : 2 : deps_.ota_session.abort();
405 : 2 : return OtaStepResult::FAILED;
406 : : }
407 : :
408 : :
409 : :
410 : : // Verify image descriptor (extra safety)
411 : 4 : esp_app_desc_t new_app_info;
412 [ + + ]: 4 : if (deps_.ota_session.get_img_desc(&new_app_info) != ESP_OK) {
413 : 1 : ESP_LOGE(TAG, "Failed to get image descriptor");
414 : 1 : deps_.ota_session.abort();
415 : 1 : return OtaStepResult::FAILED;
416 : : }
417 : :
418 : 3 : auto current_v_opt = VersionHelper::parse(deps_.system.get_running_app_desc()->version);
419 : 3 : auto new_v_opt = VersionHelper::parse(new_app_info.version);
420 [ + + + - : 3 : if (!current_v_opt.has_value() || !new_v_opt.has_value() ||
+ + ]
421 [ + + ]: 2 : !is_version_newer(current_v_opt.value(), new_v_opt.value(), config_.allow_same_version)) {
422 : 2 : ESP_LOGE(TAG, "Image version validation failed: %s", new_app_info.version);
423 : 2 : deps_.ota_session.abort();
424 : 2 : return OtaStepResult::FAILED;
425 : : }
426 : 1 : ESP_LOGI(TAG, "OTA session initialized, starting download...");
427 : 6 : }
428 : :
429 : : // 2. Perform one iteration of download
430 : 7 : esp_err_t ret = deps_.ota_session.perform();
431 : :
432 [ + + ]: 7 : if (ret == ESP_ERR_HTTPS_OTA_IN_PROGRESS) {
433 : : return OtaStepResult::IN_PROGRESS;
434 : : }
435 : :
436 [ + + ]: 5 : if (ret != ESP_OK) {
437 : 1 : ESP_LOGE(TAG, "Download failed: %s", esp_err_to_name(ret));
438 : 1 : deps_.ota_session.abort();
439 : 1 : return OtaStepResult::FAILED;
440 : : }
441 : :
442 : : // 3. Cleanup session on success
443 [ + + ]: 4 : if (deps_.ota_session.finish() != ESP_OK) {
444 : 1 : ESP_LOGE(TAG, "Failed to finish OTA session");
445 : 1 : return OtaStepResult::FAILED;
446 : : }
447 : :
448 : : ESP_LOGI(TAG, "Download completed successfully");
449 : : return OtaStepResult::SUCCESS;
450 : : }
451 : :
452 : : /**
453 : : * @brief Handles the firmware verification state.
454 : : * @note This is a synchronous operation and never returns IN_PROGRESS.
455 : : */
456 : 6 : OtaStepResult OtaManager::handle_verification_state()
457 : : {
458 : 6 : uint8_t sha256[32];
459 : 6 : const esp_partition_t* update_partition = deps_.system.get_update_partition();
460 : :
461 [ + + ]: 6 : if (update_partition == nullptr) {
462 : : ESP_LOGE(TAG, "Failed to get update partition");
463 : : return OtaStepResult::FAILED;
464 : : }
465 : :
466 [ + + ]: 4 : if (deps_.system.get_partition_sha256(update_partition, manifest_.firmware_size, sha256) != ESP_OK) {
467 : : ESP_LOGE(TAG, "Failed to calculate SHA256 for partition");
468 : : return OtaStepResult::FAILED;
469 : : }
470 : :
471 : 3 : std::string calculated_hash = bytes_to_hex(sha256, 32);
472 [ + + ]: 3 : if (calculated_hash != manifest_.sha256_hex) {
473 : 1 : ESP_LOGE(
474 : : TAG, "Hash mismatch! Manifest: %s, Calculated: %s", manifest_.sha256_hex.c_str(), calculated_hash.c_str());
475 : 1 : return OtaStepResult::FAILED;
476 : : }
477 : :
478 : : ESP_LOGI(TAG, "SHA256 verification passed: %s", calculated_hash.c_str());
479 : : return OtaStepResult::SUCCESS;
480 : 3 : }
481 : :
482 : 83 : void OtaManager::set_status(OtaStatus status)
483 : : {
484 [ + + ]: 83 : if (state_mutex_ == nullptr) {
485 : 1 : status_ = status;
486 : 1 : return;
487 : : }
488 : :
489 [ + - ]: 82 : if (deps_.task_scheduler.semaphore_take(state_mutex_, portMAX_DELAY) == pdPASS) {
490 : 82 : status_ = status;
491 : 82 : deps_.task_scheduler.semaphore_give(state_mutex_);
492 : : }
493 : : }
494 : :
495 : 2 : void OtaManager::signal_shutdown_done()
496 : : {
497 [ + - ]: 2 : if (shutdown_done_ != nullptr) {
498 : 2 : deps_.task_scheduler.semaphore_give(shutdown_done_);
499 : : }
500 : 2 : }
501 : :
502 : 2 : void OtaManager::finalize_worker_shutdown()
503 : : {
504 [ + - + - ]: 2 : if (state_mutex_ != nullptr && deps_.task_scheduler.semaphore_take(state_mutex_, portMAX_DELAY) == pdPASS) {
505 : 2 : ota_task_handle_ = nullptr;
506 : 2 : deps_.task_scheduler.semaphore_give(state_mutex_);
507 : : }
508 : :
509 : 2 : signal_shutdown_done();
510 : 2 : }
|