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