Add ESP32 OTA firmware update with rollback confirmation.
Stream application binaries to the inactive OTA slot via POST /api/system/ota, validate the esp_app_desc project name in core, and cancel rollback after a healthy network boot through OtaService::confirmBoot(). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* @file OtaError.hpp
|
||||
* @brief Failure causes for ESP32 OTA streaming updates.
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-07
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace ota {
|
||||
|
||||
/**
|
||||
* @brief OtaError — OTA session and flash write failures.
|
||||
*
|
||||
* @dname OtaError
|
||||
* @return n/a (type)
|
||||
* @pubstate n/a
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-07
|
||||
*/
|
||||
enum class OtaError {
|
||||
SessionActive,
|
||||
NoUpdatePartition,
|
||||
BeginFailed,
|
||||
WriteFailed,
|
||||
EndFailed,
|
||||
ImageTooLarge,
|
||||
SetBootFailed,
|
||||
ConfirmFailed,
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief otaErrorToken — stable API/JSON error string.
|
||||
*
|
||||
* @dname otaErrorToken
|
||||
* @param error OTA failure from the service layer.
|
||||
* @return Short token without secrets or image bytes.
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-07
|
||||
*/
|
||||
[[nodiscard]] const char* otaErrorToken(OtaError error) noexcept;
|
||||
|
||||
} // namespace ota
|
||||
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* @file OtaService.hpp
|
||||
* @brief ESP32 firmware OTA streaming and rollback confirmation.
|
||||
*
|
||||
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
|
||||
*
|
||||
* Copyright 2026 Michele Bigi
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-07
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "core/OtaImageError.hpp"
|
||||
#include "ota/OtaError.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
namespace ota {
|
||||
|
||||
/**
|
||||
* @brief OtaService — streams firmware images into the inactive OTA slot.
|
||||
*
|
||||
* @dname OtaService
|
||||
* @return n/a (type)
|
||||
* @pubstate Holds at most one active esp_ota session. Call confirmBoot()
|
||||
* once after network bootstrap on every boot.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-07
|
||||
*/
|
||||
class OtaService {
|
||||
public:
|
||||
/**
|
||||
* @brief confirmBoot — cancel rollback after a healthy boot.
|
||||
*
|
||||
* @dname confirmBoot
|
||||
* @return Ok when pending verification is cleared or absent.
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-07
|
||||
*/
|
||||
[[nodiscard]] static std::expected<void, OtaError> confirmBoot();
|
||||
|
||||
/**
|
||||
* @brief beginStream — open esp_ota session on the inactive slot.
|
||||
*
|
||||
* @dname beginStream
|
||||
* @param contentLength Declared HTTP body size in bytes.
|
||||
* @return Ok on success, or OtaError.
|
||||
* @pubstate starts an active session until finishStream() or abort().
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-07
|
||||
*/
|
||||
[[nodiscard]] std::expected<void, OtaError> beginStream(int contentLength);
|
||||
|
||||
/**
|
||||
* @brief writeChunk — append bytes and validate the app descriptor once.
|
||||
*
|
||||
* @dname writeChunk
|
||||
* @param chunk Next bytes from the HTTP body.
|
||||
* @return Ok on success, core::OtaImageError on descriptor mismatch,
|
||||
* or OtaError on flash write failure.
|
||||
* @pubstate accumulates header prefix until descriptor validation passes.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-07
|
||||
*/
|
||||
[[nodiscard]] std::expected<void, OtaError> writeChunk(
|
||||
std::span<const std::uint8_t> chunk);
|
||||
|
||||
/**
|
||||
* @brief finishStream — finalize image and select the new boot slot.
|
||||
*
|
||||
* @dname finishStream
|
||||
* @return Ok when esp_ota_end and set_boot_partition succeed.
|
||||
* @pubstate closes the active session.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-07
|
||||
*/
|
||||
[[nodiscard]] std::expected<void, OtaError> finishStream();
|
||||
|
||||
/**
|
||||
* @brief abort — cancel an in-progress OTA session.
|
||||
*
|
||||
* @dname abort
|
||||
* @pubstate clears session state without changing the boot partition.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-07
|
||||
*/
|
||||
void abort() noexcept;
|
||||
|
||||
/**
|
||||
* @brief lastImageError — image validation failure from writeChunk().
|
||||
*
|
||||
* @dname lastImageError
|
||||
* @return Most recent core::OtaImageError when writeChunk failed validation.
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-07-07
|
||||
*/
|
||||
[[nodiscard]] core::OtaImageError lastImageError() const noexcept;
|
||||
|
||||
private:
|
||||
void resetSession() noexcept;
|
||||
|
||||
bool active_ = false;
|
||||
bool descriptorValidated_ = false;
|
||||
int expectedSize_ = 0;
|
||||
int bytesWritten_ = 0;
|
||||
void* otaHandle_ = nullptr;
|
||||
const void* updatePartition_ = nullptr;
|
||||
std::vector<std::uint8_t> headerPrefix_;
|
||||
core::OtaImageError lastImageError_ = core::OtaImageError::InsufficientHeader;
|
||||
};
|
||||
|
||||
} // namespace ota
|
||||
Reference in New Issue
Block a user