Skip to main content

mcumgr_toolkit/client/
firmware_update.rs

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