Skip to main content

mcumgr_toolkit/client/
firmware_update.rs

1use std::fmt::Display;
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(Clone, Debug, 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 step of the firmware update that is currently being performed
76#[derive(Clone, Debug)]
77pub enum FirmwareUpdateStep {
78    /// Querying which bootloader the device is running
79    DetectingBootloader,
80    /// The bootloader was found
81    BootloaderFound(BootloaderType),
82    /// Extracting meta information from the new firmware image
83    ParsingFirmwareImage,
84    /// Querying the current firmware state of the device
85    QueryingDeviceState,
86    /// A summary of what update exactly we will perform now
87    UpdateInfo {
88        /// The current version with the current ID hash, if available
89        current_version: Option<(String, Option<[u8; 32]>)>,
90        /// The new version with the new ID hash
91        new_version: (String, [u8; 32]),
92    },
93    /// Uploading the new firmware to the device
94    UploadingFirmware,
95    /// Marking the new firmware to be swapped to active during next boot
96    ActivatingFirmware,
97    /// Triggering a system reboot so that the bootloader switches to the new image
98    TriggeringReboot,
99}
100
101impl Display for FirmwareUpdateStep {
102    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103        match self {
104            Self::DetectingBootloader => f.write_str("Detecting bootloader ..."),
105            Self::BootloaderFound(bootloader_type) => {
106                write!(f, "Found bootloader: {bootloader_type}")
107            }
108            Self::ParsingFirmwareImage => f.write_str("Parsing firmware image ..."),
109            Self::QueryingDeviceState => f.write_str("Querying device state ..."),
110            Self::UpdateInfo {
111                current_version,
112                new_version,
113            } => {
114                f.write_str("Update: ")?;
115
116                if let Some((version_str, version_hash)) = &current_version {
117                    f.write_str(version_str)?;
118
119                    if let Some(version_hash) = version_hash {
120                        write!(f, "-{}", hex::encode(&version_hash[..SHOWN_HASH_DIGITS]))?;
121                    }
122                } else {
123                    f.write_str("Empty")?;
124                };
125
126                write!(
127                    f,
128                    " -> {}-{}",
129                    new_version.0,
130                    hex::encode(&new_version.1[..SHOWN_HASH_DIGITS])
131                )
132            }
133            Self::UploadingFirmware => f.write_str("Uploading new firmware ..."),
134            Self::ActivatingFirmware => f.write_str("Activating new firmware ..."),
135            Self::TriggeringReboot => f.write_str("Triggering device reboot ..."),
136        }
137    }
138}
139
140/// The progress callback type of [`MCUmgrClient::firmware_update`].
141///
142/// # Arguments
143///
144/// * `FirmwareUpdateStep` - The current step that is being executed
145/// * `Option<(u64, u64)>` - The (current, total) progress of the current step, if available.
146///
147/// # Return
148///
149/// `false` on error; this will cancel the update
150///
151pub type FirmwareUpdateProgressCallback<'a> =
152    dyn FnMut(FirmwareUpdateStep, Option<(u64, u64)>) -> bool + 'a;
153
154const SHOWN_HASH_DIGITS: usize = 4;
155
156/// High-level firmware update routine
157///
158/// # Arguments
159///
160/// * `client` - The MCUmgr client.
161/// * `firmware` - The firmware image data.
162/// * `checksum` - SHA256 of the firmware image. Optional.
163/// * `params` - Configurable parameters.
164/// * `progress` - A callback that receives progress updates.
165///
166pub(crate) fn firmware_update(
167    client: &MCUmgrClient,
168    firmware: impl AsRef<[u8]>,
169    checksum: Option<[u8; 32]>,
170    params: FirmwareUpdateParams,
171    mut progress: Option<&mut FirmwareUpdateProgressCallback>,
172) -> Result<(), FirmwareUpdateError> {
173    // Might become a params member in the future
174    let target_image: Option<u32> = Default::default();
175    let actual_target_image = target_image.unwrap_or(0);
176
177    let firmware = firmware.as_ref();
178
179    let has_progress = progress.is_some();
180    let mut progress = |state: FirmwareUpdateStep, prog| {
181        if let Some(progress) = &mut progress {
182            if !progress(state, prog) {
183                return Err(FirmwareUpdateError::ProgressCallbackError);
184            }
185        }
186        Ok(())
187    };
188
189    let bootloader_type = if let Some(bootloader_type) = params.bootloader_type {
190        bootloader_type
191    } else {
192        progress(FirmwareUpdateStep::DetectingBootloader, None)?;
193
194        let bootloader_type = client
195            .os_bootloader_info()
196            .map_err(FirmwareUpdateError::BootloaderDetectionFailed)?
197            .get_bootloader_type()
198            .map_err(FirmwareUpdateError::BootloaderNotSupported)?;
199
200        progress(FirmwareUpdateStep::BootloaderFound(bootloader_type), None)?;
201
202        bootloader_type
203    };
204
205    progress(FirmwareUpdateStep::ParsingFirmwareImage, None)?;
206    let (image_version, image_id_hash) = match bootloader_type {
207        BootloaderType::MCUboot => {
208            let info = mcuboot::get_image_info(std::io::Cursor::new(firmware))?;
209            (info.version, info.hash)
210        }
211    };
212
213    progress(FirmwareUpdateStep::QueryingDeviceState, None)?;
214    let image_state = client
215        .image_get_state()
216        .map_err(FirmwareUpdateError::GetStateFailed)?;
217
218    let active_image = image_state
219        .iter()
220        .find(|img| img.image == actual_target_image && img.active)
221        .or_else(|| {
222            image_state
223                .iter()
224                .find(|img| img.image == actual_target_image && img.slot == 0)
225        });
226
227    progress(
228        FirmwareUpdateStep::UpdateInfo {
229            current_version: active_image.map(|img| (img.version.clone(), img.hash)),
230            new_version: (image_version.to_string(), image_id_hash),
231        },
232        None,
233    )?;
234
235    if active_image.and_then(|img| img.hash) == Some(image_id_hash) {
236        return Err(FirmwareUpdateError::AlreadyInstalled);
237    }
238
239    progress(FirmwareUpdateStep::UploadingFirmware, None)?;
240    let mut upload_progress_cb = |current, total| {
241        progress(
242            FirmwareUpdateStep::UploadingFirmware,
243            Some((current, total)),
244        )
245        .is_ok()
246    };
247
248    client
249        .image_upload(
250            firmware,
251            target_image,
252            checksum,
253            params.upgrade_only,
254            has_progress.then_some(&mut upload_progress_cb),
255        )
256        .map_err(|err| {
257            if let ImageUploadError::ProgressCallbackError = err {
258                // Users expect this error when the progress callback errors
259                FirmwareUpdateError::ProgressCallbackError
260            } else {
261                FirmwareUpdateError::ImageUploadFailed(err)
262            }
263        })?;
264
265    progress(FirmwareUpdateStep::ActivatingFirmware, None)?;
266    let set_state_result = client.image_set_state(Some(image_id_hash), params.force_confirm);
267    if let Err(set_state_error) = set_state_result {
268        let mut image_already_active = false;
269
270        // Special case: if the command isn't supported, we are most likely in
271        // the MCUmgr recovery shell, which writes directly to the active slot
272        // and does not support swapping.
273        // Sanity check that the image is on the first position already to avoid false
274        // positives of this exception.
275        if bootloader_type == BootloaderType::MCUboot && set_state_error.command_not_supported() {
276            progress(FirmwareUpdateStep::QueryingDeviceState, None)?;
277            let image_state = client
278                .image_get_state()
279                .map_err(FirmwareUpdateError::GetStateFailed)?;
280            if image_state.iter().any(|img| {
281                img.image == actual_target_image && img.slot == 0 && img.hash == Some(image_id_hash)
282            }) {
283                image_already_active = true;
284            }
285        }
286
287        if !image_already_active {
288            return Err(FirmwareUpdateError::SetStateFailed(set_state_error));
289        }
290    }
291
292    if !params.skip_reboot {
293        progress(FirmwareUpdateStep::TriggeringReboot, None)?;
294        client
295            .os_system_reset(false, None)
296            .map_err(FirmwareUpdateError::RebootFailed)?;
297    }
298
299    Ok(())
300}