PlantCtrl/rust/src/espota.rs
2024-02-15 23:00:05 +01:00

267 lines
11 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 doesnt 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
),
}
}