use std::fmt::Display;
use miette::Diagnostic;
use thiserror::Error;
use crate::{MCUmgrClient, bootloader::BootloaderType, client::MCUmgrClientError, mcuboot};
#[derive(Error, Debug, Diagnostic)]
pub enum FirmwareUpdateError {
#[error("Progress callback returned an error")]
#[diagnostic(code(mcumgr_toolkit::firmware_update::progress_cb_error))]
ProgressCallbackError,
#[error("Failed to detect bootloader")]
#[diagnostic(code(mcumgr_toolkit::firmware_update::detect_bootloader))]
#[diagnostic(help("try to specify the bootloader type manually"))]
BootloaderDetectionFailed(#[source] MCUmgrClientError),
#[error("Bootloader '{0}' not supported")]
#[diagnostic(code(mcumgr_toolkit::firmware_update::unknown_bootloader))]
BootloaderNotSupported(String),
#[error("Firmware is not a valid MCUboot image")]
#[diagnostic(code(mcumgr_toolkit::firmware_update::mcuboot_image))]
InvalidMcuBootFirmwareImage(#[from] mcuboot::ImageParseError),
#[error("Failed to fetch image state from device")]
#[diagnostic(code(mcumgr_toolkit::firmware_update::get_image_state))]
GetStateFailed(#[source] MCUmgrClientError),
#[error("Failed to upload firmware image to device")]
#[diagnostic(code(mcumgr_toolkit::firmware_update::image_upload))]
ImageUploadFailed(#[source] MCUmgrClientError),
#[error("Failed to activate new firmware image")]
#[diagnostic(code(mcumgr_toolkit::firmware_update::set_image_state))]
SetStateFailed(#[source] MCUmgrClientError),
#[error("Failed to trigger device reboot")]
#[diagnostic(code(mcumgr_toolkit::firmware_update::reboot))]
RebootFailed(#[source] MCUmgrClientError),
#[error("The device is already running the given firmware")]
#[diagnostic(code(mcumgr_toolkit::firmware_update::already_installed))]
AlreadyInstalled,
}
#[derive(Clone, Debug, Default)]
pub struct FirmwareUpdateParams {
pub bootloader_type: Option<BootloaderType>,
pub skip_reboot: bool,
pub force_confirm: bool,
pub upgrade_only: bool,
}
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
pub enum FirmwareUpdateStep {
DetectingBootloader,
BootloaderFound(BootloaderType),
ParsingFirmwareImage,
QueryingDeviceState,
UpdateInfo {
current_version: Option<(String, Option<Vec<u8>>)>,
new_version: (String, Vec<u8>),
},
UploadingFirmware,
ActivatingFirmware,
TriggeringReboot,
}
impl Display for FirmwareUpdateStep {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::DetectingBootloader => f.write_str("Detecting bootloader ..."),
Self::BootloaderFound(bootloader_type) => {
write!(f, "Found bootloader: {bootloader_type}")
}
Self::ParsingFirmwareImage => f.write_str("Parsing firmware image ..."),
Self::QueryingDeviceState => f.write_str("Querying device state ..."),
Self::UpdateInfo {
current_version,
new_version,
} => {
f.write_str("Update: ")?;
if let Some((version_str, version_hash)) = ¤t_version {
f.write_str(version_str)?;
if let Some(version_hash) = version_hash {
write!(
f,
"-{}",
hex::encode(&version_hash[..SHOWN_HASH_DIGITS.min(version_hash.len())])
)?;
}
} else {
f.write_str("Empty")?;
};
write!(
f,
" -> {}-{}",
new_version.0,
hex::encode(&new_version.1[..SHOWN_HASH_DIGITS.min(new_version.1.len())])
)
}
Self::UploadingFirmware => f.write_str("Uploading new firmware ..."),
Self::ActivatingFirmware => f.write_str("Activating new firmware ..."),
Self::TriggeringReboot => f.write_str("Triggering device reboot ..."),
}
}
}
pub type FirmwareUpdateProgressCallback<'a> =
dyn FnMut(FirmwareUpdateStep, Option<(u64, u64)>) -> bool + 'a;
const SHOWN_HASH_DIGITS: usize = 4;
pub(crate) fn firmware_update(
client: &MCUmgrClient,
firmware: impl AsRef<[u8]>,
checksum: Option<[u8; 32]>,
params: FirmwareUpdateParams,
mut progress: Option<&mut FirmwareUpdateProgressCallback>,
) -> Result<(), FirmwareUpdateError> {
let target_image: Option<u32> = Default::default();
let actual_target_image = target_image.unwrap_or(0);
let firmware = firmware.as_ref();
let has_progress = progress.is_some();
let mut progress = |state: FirmwareUpdateStep, prog| {
if let Some(progress) = &mut progress {
if !progress(state, prog) {
return Err(FirmwareUpdateError::ProgressCallbackError);
}
}
Ok(())
};
let bootloader_type = if let Some(bootloader_type) = params.bootloader_type {
bootloader_type
} else {
progress(FirmwareUpdateStep::DetectingBootloader, None)?;
let bootloader_type = client
.os_bootloader_info()
.map_err(FirmwareUpdateError::BootloaderDetectionFailed)?
.get_bootloader_type()
.map_err(FirmwareUpdateError::BootloaderNotSupported)?;
progress(FirmwareUpdateStep::BootloaderFound(bootloader_type), None)?;
bootloader_type
};
progress(FirmwareUpdateStep::ParsingFirmwareImage, None)?;
let (image_version, image_id_hash) = match bootloader_type {
BootloaderType::MCUboot => {
let info = mcuboot::get_image_info(std::io::Cursor::new(firmware))?;
(info.version, Vec::<u8>::from(info.hash))
}
};
progress(FirmwareUpdateStep::QueryingDeviceState, None)?;
let image_state = client
.image_get_state()
.map_err(FirmwareUpdateError::GetStateFailed)?;
let active_image = image_state
.iter()
.find(|img| img.image == actual_target_image && img.active)
.or_else(|| {
image_state
.iter()
.find(|img| img.image == actual_target_image && img.slot == 0)
});
progress(
FirmwareUpdateStep::UpdateInfo {
current_version: active_image.map(|img| (img.version.clone(), img.hash.clone())),
new_version: (image_version.to_string(), image_id_hash.clone()),
},
None,
)?;
if active_image.and_then(|img| img.hash.as_ref()) == Some(&image_id_hash) {
return Err(FirmwareUpdateError::AlreadyInstalled);
}
progress(FirmwareUpdateStep::UploadingFirmware, None)?;
let mut upload_progress_cb = |current, total| {
progress(
FirmwareUpdateStep::UploadingFirmware,
Some((current, total)),
)
.is_ok()
};
client
.image_upload(
firmware,
target_image,
checksum,
params.upgrade_only,
has_progress.then_some(&mut upload_progress_cb),
)
.map_err(|err| {
if let MCUmgrClientError::ProgressCallbackError = err {
FirmwareUpdateError::ProgressCallbackError
} else {
FirmwareUpdateError::ImageUploadFailed(err)
}
})?;
progress(FirmwareUpdateStep::ActivatingFirmware, None)?;
let set_state_result = client.image_set_state(Some(&image_id_hash), params.force_confirm);
if let Err(set_state_error) = set_state_result {
let mut image_already_active = false;
if bootloader_type == BootloaderType::MCUboot && set_state_error.command_not_supported() {
progress(FirmwareUpdateStep::QueryingDeviceState, None)?;
let image_state = client
.image_get_state()
.map_err(FirmwareUpdateError::GetStateFailed)?;
if image_state.iter().any(|img| {
img.image == actual_target_image
&& img.slot == 0
&& img.hash.as_ref() == Some(&image_id_hash)
}) {
image_already_active = true;
}
}
if !image_already_active {
return Err(FirmwareUpdateError::SetStateFailed(set_state_error));
}
}
if !params.skip_reboot {
progress(FirmwareUpdateStep::TriggeringReboot, None)?;
client
.os_system_reset(false, None)
.map_err(FirmwareUpdateError::RebootFailed)?;
}
Ok(())
}