Skip to main content

mcumgr_toolkit/client/
firmware_update.rs

1use std::borrow::Cow;
2
3use miette::Diagnostic;
4use thiserror::Error;
5
6use crate::{
7    MCUmgrClient, bootloader::BootloaderType, client::ImageUploadError, connection::ExecuteError,
8    mcuboot,
9};
10
11/// Possible error values of [`MCUmgrClient::firmware_update`].
12#[derive(Error, Debug, Diagnostic)]
13pub enum FirmwareUpdateError {
14    /// The progress callback returned an error.
15    #[error("Progress callback returned an error")]
16    #[diagnostic(code(mcumgr_toolkit::firmware_update::progress_cb_error))]
17    ProgressCallbackError,
18    /// An error occurred while trying to detect the bootloader.
19    #[error("Failed to detect bootloader")]
20    #[diagnostic(code(mcumgr_toolkit::firmware_update::detect_bootloader))]
21    #[diagnostic(help("try to specify the bootloader type manually"))]
22    BootloaderDetectionFailed(#[source] ExecuteError),
23    /// The device contains a bootloader that is not supported.
24    #[error("Bootloader '{0}' not supported")]
25    #[diagnostic(code(mcumgr_toolkit::firmware_update::unknown_bootloader))]
26    BootloaderNotSupported(String),
27    /// Failed to parse the firmware image as MCUboot firmware.
28    #[error("Firmware is not a valid MCUboot image")]
29    #[diagnostic(code(mcumgr_toolkit::firmware_update::mcuboot_image))]
30    InvalidMcuBootFirmwareImage(#[from] mcuboot::ImageParseError),
31    /// Fetching the image state returned an error.
32    #[error("Failed to fetch image state from device")]
33    #[diagnostic(code(mcumgr_toolkit::firmware_update::get_image_state))]
34    GetStateFailed(#[source] ExecuteError),
35    /// Uploading the firmware image returned an error.
36    #[error("Failed to upload firmware image to device")]
37    #[diagnostic(code(mcumgr_toolkit::firmware_update::image_upload))]
38    ImageUploadFailed(#[from] ImageUploadError),
39    /// Writing the new image state to the device failed
40    #[error("Failed to activate new firmware image")]
41    #[diagnostic(code(mcumgr_toolkit::firmware_update::set_image_state))]
42    SetStateFailed(#[source] ExecuteError),
43    /// Performing device reset failed
44    #[error("Failed to trigger device reboot")]
45    #[diagnostic(code(mcumgr_toolkit::firmware_update::reboot))]
46    RebootFailed(#[source] ExecuteError),
47    /// The given firmware is already installed on the device
48    #[error("The device is already running the given firmware")]
49    #[diagnostic(code(mcumgr_toolkit::firmware_update::already_installed))]
50    AlreadyInstalled,
51}
52
53/// Configurable parameters for [`MCUmgrClient::firmware_update`].
54#[derive(Default)]
55pub struct FirmwareUpdateParams {
56    /// Default: `None`
57    ///
58    /// The bootloader type.
59    /// Auto-detect bootloader if `None`.
60    pub bootloader_type: Option<BootloaderType>,
61    /// Default: `false`
62    ///
63    /// Do not reboot device after the update.
64    pub skip_reboot: bool,
65    /// Default: `false`
66    ///
67    /// Skip test boot and confirm directly.
68    pub force_confirm: bool,
69    /// Default: `false`
70    ///
71    /// Prevent firmware downgrades.
72    pub upgrade_only: bool,
73}
74
75/// The progress callback type of [`MCUmgrClient::firmware_update`].
76///
77/// # Arguments
78///
79/// * `&str` - Human readable description of the current step
80/// * `Option<(u64, u64)>` - The (current, total) progress of the current step, if available.
81///
82/// # Return
83///
84/// `false` on error; this will cancel the update
85///
86pub type FirmwareUpdateProgressCallback<'a> = dyn FnMut(&str, Option<(u64, u64)>) -> bool + 'a;
87
88const SHOWN_HASH_DIGITS: usize = 4;
89
90/// High-level firmware update routine
91///
92/// # Arguments
93///
94/// * `client` - The MCUmgr client.
95/// * `firmware` - The firmware image data.
96/// * `checksum` - SHA256 of the firmware image. Optional.
97/// * `params` - Configurable parameters.
98/// * `progress` - A callback that receives progress updates.
99///
100pub(crate) fn firmware_update(
101    client: &MCUmgrClient,
102    firmware: impl AsRef<[u8]>,
103    checksum: Option<[u8; 32]>,
104    params: FirmwareUpdateParams,
105    mut progress: Option<&mut FirmwareUpdateProgressCallback>,
106) -> Result<(), FirmwareUpdateError> {
107    // Might become a params member in the future
108    let target_image: Option<u32> = Default::default();
109    let actual_target_image = target_image.unwrap_or(0);
110
111    let firmware = firmware.as_ref();
112
113    let has_progress = progress.is_some();
114    let mut progress = |msg: Cow<str>, prog| {
115        if let Some(progress) = &mut progress {
116            if !progress(msg.as_ref(), prog) {
117                return Err(FirmwareUpdateError::ProgressCallbackError);
118            }
119        }
120        Ok(())
121    };
122
123    let bootloader_type = if let Some(bootloader_type) = params.bootloader_type {
124        bootloader_type
125    } else {
126        progress("Detecting bootloader ...".into(), None)?;
127
128        let bootloader_type = client
129            .os_bootloader_info()
130            .map_err(FirmwareUpdateError::BootloaderDetectionFailed)?
131            .get_bootloader_type()
132            .map_err(FirmwareUpdateError::BootloaderNotSupported)?;
133
134        progress(format!("Found bootloader: {bootloader_type}").into(), None)?;
135
136        bootloader_type
137    };
138
139    progress("Parsing firmware image ...".into(), None)?;
140    let (image_version, image_id_hash) = match bootloader_type {
141        BootloaderType::MCUboot => {
142            let info = mcuboot::get_image_info(std::io::Cursor::new(firmware))?;
143            (info.version, info.hash)
144        }
145    };
146
147    let new_image_string = format!(
148        "{}-{}",
149        image_version,
150        hex::encode(&image_id_hash[..SHOWN_HASH_DIGITS])
151    );
152
153    progress("Querying device state ...".into(), None)?;
154    let image_state = client
155        .image_get_state()
156        .map_err(FirmwareUpdateError::GetStateFailed)?;
157
158    let active_image = image_state
159        .iter()
160        .find(|img| img.image == actual_target_image && img.active)
161        .or_else(|| {
162            image_state
163                .iter()
164                .find(|img| img.image == actual_target_image && img.slot == 0)
165        });
166
167    let active_image_string = if let Some(active_image) = &active_image {
168        if let Some(active_hash) = active_image.hash {
169            format!(
170                "{}-{}",
171                active_image.version,
172                hex::encode(&active_hash[..SHOWN_HASH_DIGITS]),
173            )
174        } else {
175            active_image.version.clone()
176        }
177    } else {
178        "Empty".to_string()
179    };
180
181    progress(
182        format!("Update: {} -> {}", active_image_string, new_image_string).into(),
183        None,
184    )?;
185
186    if active_image.and_then(|img| img.hash) == Some(image_id_hash) {
187        return Err(FirmwareUpdateError::AlreadyInstalled);
188    }
189
190    progress("Uploading new firmware ...".into(), None)?;
191    let mut upload_progress_cb = |current, total| {
192        progress("Uploading new firmware ...".into(), Some((current, total))).is_ok()
193    };
194
195    client
196        .image_upload(
197            firmware,
198            target_image,
199            checksum,
200            params.upgrade_only,
201            has_progress.then_some(&mut upload_progress_cb),
202        )
203        .map_err(|err| {
204            if let ImageUploadError::ProgressCallbackError = err {
205                // Users expect this error when the progress callback errors
206                FirmwareUpdateError::ProgressCallbackError
207            } else {
208                FirmwareUpdateError::ImageUploadFailed(err)
209            }
210        })?;
211
212    progress("Activating new firmware ...".into(), None)?;
213    let set_state_result = client.image_set_state(Some(image_id_hash), params.force_confirm);
214    if let Err(set_state_error) = set_state_result {
215        let mut image_already_active = false;
216
217        // Special case: if the command isn't supported, we are most likely in
218        // the MCUmgr recovery shell, which writes directly to the active slot
219        // and does not support swapping.
220        // Sanity check that the image is on the first position already to avoid false
221        // positives of this exception.
222        if bootloader_type == BootloaderType::MCUboot && set_state_error.command_not_supported() {
223            progress("Querying device state ...".into(), None)?;
224            let image_state = client
225                .image_get_state()
226                .map_err(FirmwareUpdateError::GetStateFailed)?;
227            if image_state.iter().any(|img| {
228                img.image == actual_target_image && img.slot == 0 && img.hash == Some(image_id_hash)
229            }) {
230                image_already_active = true;
231            }
232        }
233
234        if !image_already_active {
235            return Err(FirmwareUpdateError::SetStateFailed(set_state_error));
236        }
237    }
238
239    if !params.skip_reboot {
240        progress("Triggering device reboot ...".into(), None)?;
241        client
242            .os_system_reset(false, None)
243            .map_err(FirmwareUpdateError::RebootFailed)?;
244    }
245
246    Ok(())
247}