use std::fmt::Display;
use miette::Diagnostic;
use thiserror::Error;
use crate::{
MCUmgrClient,
bootloader::BootloaderType,
client::{
MCUmgrClientError,
image_run_state::{self, ImageRunState},
},
mcuboot,
};
#[non_exhaustive]
#[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,
#[error("An image is already pending")]
#[diagnostic(code(mcumgr_toolkit::firmware_update::image_already_pending))]
#[diagnostic(help(
"Please reboot the system to reach a stable state before retrying the update."
))]
ImageAlreadyPending,
#[error("An image is currently being tested")]
#[diagnostic(code(mcumgr_toolkit::firmware_update::image_currently_tested))]
#[diagnostic(help("Please bring the system to a stable state before attempting an update."))]
ImageCurrentlyTested,
#[error("The device state is inconsistent")]
#[diagnostic(code(mcumgr_toolkit::firmware_update::inconsistent_device_state))]
InconsistentDeviceState,
}
#[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 maybe_target_image: Option<u32> = Default::default();
let target_image: u32 = maybe_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 mut image_state = client
.image_get_state()
.map_err(FirmwareUpdateError::GetStateFailed)?;
let mut run_state = image_run_state::analyze(&image_state, target_image);
let active_image = match run_state {
ImageRunState::Stable(image_state) => Some(image_state),
ImageRunState::Pending { current, .. } => current,
ImageRunState::Testing { current, .. } => Some(current),
ImageRunState::Unknown(image_state) => image_state,
ImageRunState::Inconsistent => return Err(FirmwareUpdateError::InconsistentDeviceState),
};
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,
)?;
match run_state {
ImageRunState::Stable(current) => {
if current.hash.as_ref() == Some(&image_id_hash) {
return Err(FirmwareUpdateError::AlreadyInstalled);
}
}
ImageRunState::Pending { .. } => {
return Err(FirmwareUpdateError::ImageAlreadyPending);
}
ImageRunState::Testing { .. } => {
return Err(FirmwareUpdateError::ImageCurrentlyTested);
}
ImageRunState::Unknown(None) => {
}
ImageRunState::Unknown(Some(_)) => {
}
ImageRunState::Inconsistent => {
return Err(FirmwareUpdateError::InconsistentDeviceState);
}
}
progress(FirmwareUpdateStep::UploadingFirmware, None)?;
let mut upload_progress_cb = |current, total| {
progress(
FirmwareUpdateStep::UploadingFirmware,
Some((current, total)),
)
.is_ok()
};
client
.image_upload(
firmware,
maybe_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::QueryingDeviceState, None)?;
image_state = client
.image_get_state()
.map_err(FirmwareUpdateError::GetStateFailed)?;
run_state = image_run_state::analyze(&image_state, target_image);
let needs_set_state = match run_state {
ImageRunState::Stable(current) => {
if let Some(hash) = ¤t.hash {
hash != &image_id_hash
} else {
false
}
}
ImageRunState::Pending { next, .. } => {
if let Some(hash) = &next.hash
&& hash != &image_id_hash
{
return Err(FirmwareUpdateError::ImageAlreadyPending);
}
false
}
ImageRunState::Testing { .. } => {
return Err(FirmwareUpdateError::ImageCurrentlyTested);
}
ImageRunState::Unknown(Some(guessed)) => {
if let Some(hash) = &guessed.hash
&& hash != &image_id_hash
{
return Err(FirmwareUpdateError::InconsistentDeviceState);
}
false
}
ImageRunState::Unknown(None) => {
return Err(FirmwareUpdateError::InconsistentDeviceState);
}
ImageRunState::Inconsistent => {
return Err(FirmwareUpdateError::InconsistentDeviceState);
}
};
if needs_set_state {
progress(FirmwareUpdateStep::ActivatingFirmware, None)?;
client
.image_set_state(Some(&image_id_hash), params.force_confirm)
.map_err(FirmwareUpdateError::SetStateFailed)?;
}
if !params.skip_reboot {
progress(FirmwareUpdateStep::TriggeringReboot, None)?;
client
.os_system_reset(false, None)
.map_err(FirmwareUpdateError::RebootFailed)?;
}
Ok(())
}