267 lines
11 KiB
Rust
267 lines
11 KiB
Rust
|
use core::fmt;
|
|||
|
use core::mem;
|
|||
|
use core::ptr;
|
|||
|
use esp_idf_sys::{
|
|||
|
esp_ota_abort, esp_ota_begin, esp_ota_end, esp_ota_get_next_update_partition, esp_ota_handle_t,
|
|||
|
esp_ota_mark_app_invalid_rollback_and_reboot, esp_ota_mark_app_valid_cancel_rollback,
|
|||
|
esp_ota_set_boot_partition, esp_ota_write, esp_partition_t, esp_restart, ESP_ERR_FLASH_OP_FAIL,
|
|||
|
ESP_ERR_FLASH_OP_TIMEOUT, ESP_ERR_INVALID_ARG, ESP_ERR_INVALID_SIZE, ESP_ERR_INVALID_STATE,
|
|||
|
ESP_ERR_NOT_FOUND, ESP_ERR_NO_MEM, ESP_ERR_OTA_PARTITION_CONFLICT, ESP_ERR_OTA_ROLLBACK_FAILED,
|
|||
|
ESP_ERR_OTA_ROLLBACK_INVALID_STATE, ESP_ERR_OTA_SELECT_INFO_INVALID,
|
|||
|
ESP_ERR_OTA_VALIDATE_FAILED, ESP_FAIL, ESP_OK, OTA_SIZE_UNKNOWN,
|
|||
|
};
|
|||
|
|
|||
|
pub type Result<T> = core::result::Result<T, Error>;
|
|||
|
|
|||
|
/// An error that can happen during ESP OTA operations.
|
|||
|
#[derive(Debug)]
|
|||
|
pub struct Error {
|
|||
|
kind: ErrorKind,
|
|||
|
}
|
|||
|
|
|||
|
impl Error {
|
|||
|
pub(crate) fn from_kind(kind: ErrorKind) -> Self {
|
|||
|
Self { kind }
|
|||
|
}
|
|||
|
|
|||
|
/// Returns the kind of error as an enum, that can be matched on.
|
|||
|
pub fn kind(&self) -> ErrorKind {
|
|||
|
self.kind
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
impl fmt::Display for Error {
|
|||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|||
|
self.kind.fmt(f)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
impl std::error::Error for Error {}
|
|||
|
|
|||
|
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
|||
|
#[non_exhaustive]
|
|||
|
pub enum ErrorKind {
|
|||
|
/// No suitable partition for writing OTA update to found.
|
|||
|
NoOtaPartition,
|
|||
|
/// Cannot allocate memory for OTA operation.
|
|||
|
AllocFailed,
|
|||
|
/// Rollback enabled, but the currently running application is still pending. The currently
|
|||
|
/// running application must confirm itself before downloading and flashing a new app.
|
|||
|
InvalidRollbackState,
|
|||
|
/// First byte of image contains invalid app image magic byte.
|
|||
|
InvalidMagicByte,
|
|||
|
/// Flash write operation timed out.
|
|||
|
FlashTimeout,
|
|||
|
/// Flash write operation failed.
|
|||
|
FlashFailed,
|
|||
|
/// OTA data partition has invalid contents.
|
|||
|
InvalidOtaPartitionData,
|
|||
|
/// The [`OtaUpdate`] handle was finalized before any app image was written to it.
|
|||
|
NothingWritten,
|
|||
|
/// OTA image is invalid (either not a valid app image, or - if secure boot is enabled - signature failed to verify.)
|
|||
|
InvalidImage,
|
|||
|
/// If flash encryption is enabled, this result indicates an internal error writing the final encrypted bytes to flash.
|
|||
|
WritingEncryptedFailed,
|
|||
|
/// The rollback failed.
|
|||
|
RollbackFailed,
|
|||
|
/// The rollback is not possible due to flash does not have any apps.
|
|||
|
RollbackFailedNoApps,
|
|||
|
}
|
|||
|
|
|||
|
impl fmt::Display for ErrorKind {
|
|||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|||
|
use ErrorKind::*;
|
|||
|
match self {
|
|||
|
NoOtaPartition => "No suitable partition for writing OTA update to found",
|
|||
|
AllocFailed => "Cannot allocate memory for OTA operation",
|
|||
|
InvalidRollbackState => {
|
|||
|
"Rollback enabled, but the currently running application is still pending"
|
|||
|
}
|
|||
|
InvalidMagicByte => "First byte of image contains invalid app image magic byte",
|
|||
|
FlashTimeout => "Flash write operation timed out",
|
|||
|
FlashFailed => "Flash write operation failed",
|
|||
|
InvalidOtaPartitionData => "OTA data partition has invalid contents",
|
|||
|
NothingWritten => "OtaUpdate was never written to",
|
|||
|
InvalidImage => "OTA image is invalid",
|
|||
|
WritingEncryptedFailed => "Internal error writing the final encrypted bytes to flash",
|
|||
|
RollbackFailed => "The rollback failed",
|
|||
|
RollbackFailedNoApps => {
|
|||
|
"The rollback is not possible due to flash does not have any apps"
|
|||
|
}
|
|||
|
}
|
|||
|
.fmt(f)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// Represents an ongoing OTA update.
|
|||
|
///
|
|||
|
/// Dropping this object before calling [`finalize`](OtaUpdate::finalize) will abort the update.
|
|||
|
#[derive(Debug)]
|
|||
|
pub struct OtaUpdate {
|
|||
|
partition: *const esp_partition_t,
|
|||
|
ota_handle: esp_ota_handle_t,
|
|||
|
}
|
|||
|
|
|||
|
impl OtaUpdate {
|
|||
|
/// Starts an OTA update to the next OTA compatible partition.
|
|||
|
///
|
|||
|
/// Finds next partition round-robin, starting from the current running partition.
|
|||
|
/// The entire partition is erased.
|
|||
|
pub fn begin() -> Result<Self> {
|
|||
|
let partition = unsafe { esp_ota_get_next_update_partition(ptr::null()) };
|
|||
|
if partition.is_null() {
|
|||
|
return Err(Error::from_kind(ErrorKind::NoOtaPartition));
|
|||
|
}
|
|||
|
|
|||
|
let mut ota_handle = 0;
|
|||
|
match unsafe { esp_ota_begin(partition, OTA_SIZE_UNKNOWN as usize, &mut ota_handle) } {
|
|||
|
ESP_OK => Ok(()),
|
|||
|
ESP_ERR_INVALID_ARG => panic!("Invalid partition or out_handle"),
|
|||
|
ESP_ERR_NO_MEM => Err(Error::from_kind(ErrorKind::AllocFailed)),
|
|||
|
ESP_ERR_OTA_PARTITION_CONFLICT => Err(Error::from_kind(ErrorKind::NoOtaPartition)),
|
|||
|
ESP_ERR_NOT_FOUND => panic!("Partition argument not found in partition table"),
|
|||
|
ESP_ERR_OTA_SELECT_INFO_INVALID => {
|
|||
|
Err(Error::from_kind(ErrorKind::InvalidOtaPartitionData))
|
|||
|
}
|
|||
|
ESP_ERR_INVALID_SIZE => panic!("Partition doesn’t fit in configured flash size"),
|
|||
|
ESP_ERR_FLASH_OP_TIMEOUT => Err(Error::from_kind(ErrorKind::FlashTimeout)),
|
|||
|
ESP_ERR_FLASH_OP_FAIL => Err(Error::from_kind(ErrorKind::FlashFailed)),
|
|||
|
ESP_ERR_OTA_ROLLBACK_INVALID_STATE => {
|
|||
|
Err(Error::from_kind(ErrorKind::InvalidRollbackState))
|
|||
|
}
|
|||
|
code => panic!("Unexpected esp_ota_begin return code: {}", code),
|
|||
|
}?;
|
|||
|
|
|||
|
Ok(Self {
|
|||
|
partition,
|
|||
|
ota_handle,
|
|||
|
})
|
|||
|
}
|
|||
|
|
|||
|
/// Write app image data to partition.
|
|||
|
///
|
|||
|
/// This method can be called multiple times as data is received during the OTA operation.
|
|||
|
/// Data is written sequentially to the partition.
|
|||
|
///
|
|||
|
/// The format of the app image can be read about in the main README and crate documentation.
|
|||
|
pub fn write(&mut self, app_image_chunk: &[u8]) -> Result<()> {
|
|||
|
let chunk_ptr = app_image_chunk.as_ptr() as *const _;
|
|||
|
let chunk_len = app_image_chunk.len();
|
|||
|
|
|||
|
match unsafe { esp_ota_write(self.ota_handle, chunk_ptr, chunk_len) } {
|
|||
|
ESP_OK => Ok(()),
|
|||
|
ESP_ERR_INVALID_ARG => panic!("Invalid OTA handle"),
|
|||
|
ESP_ERR_OTA_VALIDATE_FAILED => Err(Error::from_kind(ErrorKind::InvalidMagicByte)),
|
|||
|
ESP_ERR_FLASH_OP_TIMEOUT => Err(Error::from_kind(ErrorKind::FlashTimeout)),
|
|||
|
ESP_ERR_FLASH_OP_FAIL => Err(Error::from_kind(ErrorKind::FlashFailed)),
|
|||
|
ESP_ERR_OTA_SELECT_INFO_INVALID => {
|
|||
|
Err(Error::from_kind(ErrorKind::InvalidOtaPartitionData))
|
|||
|
}
|
|||
|
code => panic!("Unexpected esp_ota_write return code: {code}"),
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// Finish OTA update and validate newly written app image.
|
|||
|
///
|
|||
|
/// Unless you also call [`set_as_boot_partition`](CompletedOtaUpdate::set_as_boot_partition) the new app will not
|
|||
|
/// start.
|
|||
|
pub fn finalize(self) -> Result<CompletedOtaUpdate> {
|
|||
|
match unsafe { esp_ota_end(self.ota_handle) } {
|
|||
|
ESP_OK => Ok(()),
|
|||
|
ESP_ERR_NOT_FOUND => panic!("Invalid OTA handle"),
|
|||
|
ESP_ERR_INVALID_ARG => Err(Error::from_kind(ErrorKind::NothingWritten)),
|
|||
|
ESP_ERR_OTA_VALIDATE_FAILED => Err(Error::from_kind(ErrorKind::InvalidImage)),
|
|||
|
ESP_ERR_INVALID_STATE => Err(Error::from_kind(ErrorKind::WritingEncryptedFailed)),
|
|||
|
code => panic!("Unexpected esp_ota_end return code: {code}"),
|
|||
|
}?;
|
|||
|
|
|||
|
let partition = self.partition;
|
|||
|
mem::forget(self);
|
|||
|
|
|||
|
Ok(CompletedOtaUpdate { partition })
|
|||
|
}
|
|||
|
|
|||
|
/// Returns a raw pointer to the partition that the new app is/will be written to.
|
|||
|
pub fn raw_partition(&self) -> *const esp_partition_t {
|
|||
|
self.partition
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
impl Drop for OtaUpdate {
|
|||
|
fn drop(&mut self) {
|
|||
|
#[cfg(feature = "log")]
|
|||
|
log::debug!("Aborting OTA update");
|
|||
|
|
|||
|
let abort_result_code = unsafe { esp_ota_abort(self.ota_handle) };
|
|||
|
if abort_result_code != ESP_OK {
|
|||
|
#[cfg(feature = "log")]
|
|||
|
log::error!(
|
|||
|
"Aborting the OTA update returned an unexpected code: {}",
|
|||
|
abort_result_code
|
|||
|
)
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
pub struct CompletedOtaUpdate {
|
|||
|
partition: *const esp_partition_t,
|
|||
|
}
|
|||
|
|
|||
|
impl CompletedOtaUpdate {
|
|||
|
/// Sets the boot partition to the newly flashed OTA partition.
|
|||
|
pub fn set_as_boot_partition(&mut self) -> Result<()> {
|
|||
|
match unsafe { esp_ota_set_boot_partition(self.partition) } {
|
|||
|
ESP_OK => Ok(()),
|
|||
|
ESP_ERR_INVALID_ARG => panic!("Invalid partition sent to esp_ota_set_boot_partition"),
|
|||
|
ESP_ERR_OTA_VALIDATE_FAILED => Err(Error::from_kind(ErrorKind::InvalidImage)),
|
|||
|
ESP_ERR_NOT_FOUND => panic!("OTA data partition not found"),
|
|||
|
ESP_ERR_FLASH_OP_TIMEOUT => Err(Error::from_kind(ErrorKind::FlashTimeout)),
|
|||
|
ESP_ERR_FLASH_OP_FAIL => Err(Error::from_kind(ErrorKind::FlashFailed)),
|
|||
|
code => panic!("Unexpected esp_ota_set_boot_partition code: {}", code),
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// Restarts the CPU. If [`set_as_boot_partition`](CompletedOtaUpdate::set_as_boot_partition) was
|
|||
|
/// called and completed successfully, the CPU will boot into the newly written app.
|
|||
|
///
|
|||
|
/// After successful restart, CPU reset reason will be SW_CPU_RESET. Peripherals
|
|||
|
/// (except for WiFi, BT, UART0, SPI1, and legacy timers) are not reset.
|
|||
|
pub fn restart(self) -> ! {
|
|||
|
unsafe { esp_restart() }
|
|||
|
}
|
|||
|
|
|||
|
/// Returns a raw pointer to the partition that the new app was written to.
|
|||
|
pub fn raw_partition(&self) -> *const esp_partition_t {
|
|||
|
self.partition
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// Call this function to indicate that the running app is working well.
|
|||
|
///
|
|||
|
/// Should be called (at least) the first time a new app starts up after
|
|||
|
/// being flashed.
|
|||
|
pub fn mark_app_valid() {
|
|||
|
match unsafe { esp_ota_mark_app_valid_cancel_rollback() } {
|
|||
|
ESP_OK => (),
|
|||
|
code => panic!(
|
|||
|
"Unexpected esp_ota_mark_app_valid_cancel_rollback code: {}",
|
|||
|
code
|
|||
|
),
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// Call this function to roll back to the previously workable app with reboot.
|
|||
|
///
|
|||
|
/// If rolling back failed, it returns an error, otherwise this function never returns,
|
|||
|
/// as the CPU is rebooting.
|
|||
|
pub fn rollback_and_reboot() -> Result<core::convert::Infallible> {
|
|||
|
match unsafe { esp_ota_mark_app_invalid_rollback_and_reboot() } {
|
|||
|
ESP_FAIL => Err(Error::from_kind(ErrorKind::RollbackFailed)),
|
|||
|
ESP_ERR_OTA_ROLLBACK_FAILED => Err(Error::from_kind(ErrorKind::RollbackFailedNoApps)),
|
|||
|
code => panic!(
|
|||
|
"Unexpected esp_ota_mark_app_invalid_rollback_and_reboot code: {}",
|
|||
|
code
|
|||
|
),
|
|||
|
}
|
|||
|
}
|