PlantCtrl/rust/src/espota.rs

267 lines
11 KiB
Rust
Raw Normal View History

2024-02-15 23:00:05 +01:00
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
),
}
}