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,
8    bootloader::BootloaderType,
9    client::{
10        MCUmgrClientError,
11        image_run_state::{self, ImageRunState},
12    },
13    mcuboot,
14};
15
16/// Possible error values of [`MCUmgrClient::firmware_update`].
17#[non_exhaustive]
18#[derive(Error, Debug, Diagnostic)]
19pub enum FirmwareUpdateError {
20    /// The progress callback returned an error.
21    #[error("Progress callback returned an error")]
22    #[diagnostic(code(mcumgr_toolkit::firmware_update::progress_cb_error))]
23    ProgressCallbackError,
24    /// An error occurred while trying to detect the bootloader.
25    #[error("Failed to detect bootloader")]
26    #[diagnostic(code(mcumgr_toolkit::firmware_update::detect_bootloader))]
27    #[diagnostic(help("try to specify the bootloader type manually"))]
28    BootloaderDetectionFailed(#[source] MCUmgrClientError),
29    /// The device contains a bootloader that is not supported.
30    #[error("Bootloader '{0}' not supported")]
31    #[diagnostic(code(mcumgr_toolkit::firmware_update::unknown_bootloader))]
32    BootloaderNotSupported(String),
33    /// Failed to parse the firmware image as MCUboot firmware.
34    #[error("Firmware is not a valid MCUboot image")]
35    #[diagnostic(code(mcumgr_toolkit::firmware_update::mcuboot_image))]
36    InvalidMcuBootFirmwareImage(#[from] mcuboot::ImageParseError),
37    /// Fetching the image state returned an error.
38    #[error("Failed to fetch image state from device")]
39    #[diagnostic(code(mcumgr_toolkit::firmware_update::get_image_state))]
40    GetStateFailed(#[source] MCUmgrClientError),
41    /// Uploading the firmware image returned an error.
42    #[error("Failed to upload firmware image to device")]
43    #[diagnostic(code(mcumgr_toolkit::firmware_update::image_upload))]
44    ImageUploadFailed(#[source] MCUmgrClientError),
45    /// Writing the new image state to the device failed
46    #[error("Failed to activate new firmware image")]
47    #[diagnostic(code(mcumgr_toolkit::firmware_update::set_image_state))]
48    SetStateFailed(#[source] MCUmgrClientError),
49    /// Performing device reset failed
50    #[error("Failed to trigger device reboot")]
51    #[diagnostic(code(mcumgr_toolkit::firmware_update::reboot))]
52    RebootFailed(#[source] MCUmgrClientError),
53    /// The given firmware is already installed on the device
54    #[error("The device is already running the given firmware")]
55    #[diagnostic(code(mcumgr_toolkit::firmware_update::already_installed))]
56    AlreadyInstalled,
57    /// There is already a pending image on the system
58    #[error("An image is already pending")]
59    #[diagnostic(code(mcumgr_toolkit::firmware_update::image_already_pending))]
60    #[diagnostic(help(
61        "Please reboot the system to reach a stable state before retrying the update."
62    ))]
63    ImageAlreadyPending,
64    /// The system is currently test-booting an image
65    #[error("An image is currently being tested")]
66    #[diagnostic(code(mcumgr_toolkit::firmware_update::image_currently_tested))]
67    #[diagnostic(help("Please bring the system to a stable state before attempting an update."))]
68    ImageCurrentlyTested,
69    /// The device state is inconsistent
70    #[error("The device state is inconsistent")]
71    #[diagnostic(code(mcumgr_toolkit::firmware_update::inconsistent_device_state))]
72    InconsistentDeviceState,
73}
74
75/// Configurable parameters for [`MCUmgrClient::firmware_update`].
76#[derive(Clone, Debug, Default)]
77pub struct FirmwareUpdateParams {
78    /// Default: `None`
79    ///
80    /// The bootloader type.
81    /// Auto-detect bootloader if `None`.
82    pub bootloader_type: Option<BootloaderType>,
83    /// Default: `false`
84    ///
85    /// Do not reboot device after the update.
86    pub skip_reboot: bool,
87    /// Default: `false`
88    ///
89    /// Skip test boot and confirm directly.
90    ///
91    /// Be aware that this is best effort and might not work
92    /// in all circumstances.
93    pub force_confirm: bool,
94    /// Default: `false`
95    ///
96    /// Prevent firmware downgrades.
97    pub upgrade_only: bool,
98}
99
100/// The step of the firmware update that is currently being performed
101#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
102pub enum FirmwareUpdateStep {
103    /// Querying which bootloader the device is running
104    DetectingBootloader,
105    /// The bootloader was found
106    BootloaderFound(BootloaderType),
107    /// Extracting meta information from the new firmware image
108    ParsingFirmwareImage,
109    /// Querying the current firmware state of the device
110    QueryingDeviceState,
111    /// A summary of what update exactly we will perform now
112    UpdateInfo {
113        /// The current version with the current ID hash, if available
114        current_version: Option<(String, Option<Vec<u8>>)>,
115        /// The new version with the new ID hash
116        new_version: (String, Vec<u8>),
117    },
118    /// Uploading the new firmware to the device
119    UploadingFirmware,
120    /// Marking the new firmware to be swapped to active during next boot
121    ActivatingFirmware,
122    /// Triggering a system reboot so that the bootloader switches to the new image
123    TriggeringReboot,
124}
125
126impl Display for FirmwareUpdateStep {
127    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128        match self {
129            Self::DetectingBootloader => f.write_str("Detecting bootloader ..."),
130            Self::BootloaderFound(bootloader_type) => {
131                write!(f, "Found bootloader: {bootloader_type}")
132            }
133            Self::ParsingFirmwareImage => f.write_str("Parsing firmware image ..."),
134            Self::QueryingDeviceState => f.write_str("Querying device state ..."),
135            Self::UpdateInfo {
136                current_version,
137                new_version,
138            } => {
139                f.write_str("Update: ")?;
140
141                if let Some((version_str, version_hash)) = &current_version {
142                    f.write_str(version_str)?;
143
144                    if let Some(version_hash) = version_hash {
145                        write!(
146                            f,
147                            "-{}",
148                            hex::encode(&version_hash[..SHOWN_HASH_DIGITS.min(version_hash.len())])
149                        )?;
150                    }
151                } else {
152                    f.write_str("Empty")?;
153                };
154
155                write!(
156                    f,
157                    " -> {}-{}",
158                    new_version.0,
159                    hex::encode(&new_version.1[..SHOWN_HASH_DIGITS.min(new_version.1.len())])
160                )
161            }
162            Self::UploadingFirmware => f.write_str("Uploading new firmware ..."),
163            Self::ActivatingFirmware => f.write_str("Activating new firmware ..."),
164            Self::TriggeringReboot => f.write_str("Triggering device reboot ..."),
165        }
166    }
167}
168
169/// The progress callback type of [`MCUmgrClient::firmware_update`].
170///
171/// # Arguments
172///
173/// * `FirmwareUpdateStep` - The current step that is being executed
174/// * `Option<(u64, u64)>` - The (current, total) progress of the current step, if available.
175///
176/// # Return
177///
178/// `false` on error; this will cancel the update
179///
180pub type FirmwareUpdateProgressCallback<'a> =
181    dyn FnMut(FirmwareUpdateStep, Option<(u64, u64)>) -> bool + 'a;
182
183const SHOWN_HASH_DIGITS: usize = 4;
184
185/// High-level firmware update routine
186///
187/// # Arguments
188///
189/// * `client` - The MCUmgr client.
190/// * `firmware` - The firmware image data.
191/// * `checksum` - SHA256 of the firmware image. Optional.
192/// * `params` - Configurable parameters.
193/// * `progress` - A callback that receives progress updates.
194///
195pub(crate) fn firmware_update(
196    client: &MCUmgrClient,
197    firmware: impl AsRef<[u8]>,
198    checksum: Option<[u8; 32]>,
199    params: FirmwareUpdateParams,
200    mut progress: Option<&mut FirmwareUpdateProgressCallback>,
201) -> Result<(), FirmwareUpdateError> {
202    // Might become a params member in the future
203    let maybe_target_image: Option<u32> = Default::default();
204
205    // We assume that the upload command uploads to image 0 when parameter is missing.
206    let target_image: u32 = maybe_target_image.unwrap_or(0);
207
208    let firmware = firmware.as_ref();
209
210    let has_progress = progress.is_some();
211    let mut progress = |state: FirmwareUpdateStep, prog| {
212        if let Some(progress) = &mut progress {
213            if !progress(state, prog) {
214                return Err(FirmwareUpdateError::ProgressCallbackError);
215            }
216        }
217        Ok(())
218    };
219
220    let bootloader_type = if let Some(bootloader_type) = params.bootloader_type {
221        bootloader_type
222    } else {
223        progress(FirmwareUpdateStep::DetectingBootloader, None)?;
224
225        let bootloader_type = client
226            .os_bootloader_info()
227            .map_err(FirmwareUpdateError::BootloaderDetectionFailed)?
228            .get_bootloader_type()
229            .map_err(FirmwareUpdateError::BootloaderNotSupported)?;
230
231        progress(FirmwareUpdateStep::BootloaderFound(bootloader_type), None)?;
232
233        bootloader_type
234    };
235
236    progress(FirmwareUpdateStep::ParsingFirmwareImage, None)?;
237    let (image_version, image_id_hash) = match bootloader_type {
238        BootloaderType::MCUboot => {
239            let info = mcuboot::get_image_info(std::io::Cursor::new(firmware))?;
240            (info.version, Vec::<u8>::from(info.hash))
241        }
242    };
243
244    progress(FirmwareUpdateStep::QueryingDeviceState, None)?;
245    let mut image_state = client
246        .image_get_state()
247        .map_err(FirmwareUpdateError::GetStateFailed)?;
248
249    let mut run_state = image_run_state::analyze(&image_state, target_image);
250    let active_image = match run_state {
251        ImageRunState::Stable(image_state) => Some(image_state),
252        ImageRunState::Pending { current, .. } => current,
253        ImageRunState::Testing { current, .. } => Some(current),
254        ImageRunState::Unknown(image_state) => image_state,
255        ImageRunState::Inconsistent => return Err(FirmwareUpdateError::InconsistentDeviceState),
256    };
257
258    progress(
259        FirmwareUpdateStep::UpdateInfo {
260            current_version: active_image.map(|img| (img.version.clone(), img.hash.clone())),
261            new_version: (image_version.to_string(), image_id_hash.clone()),
262        },
263        None,
264    )?;
265
266    match run_state {
267        ImageRunState::Stable(current) => {
268            if current.hash.as_ref() == Some(&image_id_hash) {
269                return Err(FirmwareUpdateError::AlreadyInstalled);
270            }
271        }
272
273        ImageRunState::Pending { .. } => {
274            return Err(FirmwareUpdateError::ImageAlreadyPending);
275        }
276
277        ImageRunState::Testing { .. } => {
278            return Err(FirmwareUpdateError::ImageCurrentlyTested);
279        }
280
281        ImageRunState::Unknown(None) => {
282            // Might be in MCUboot recovery mode with no
283            // images installed on the system, continue
284            // and try the update anyway
285        }
286
287        ImageRunState::Unknown(Some(_)) => {
288            // Might be in MCUboot recovery mode, continue
289            // and try the update anyway
290        }
291
292        ImageRunState::Inconsistent => {
293            return Err(FirmwareUpdateError::InconsistentDeviceState);
294        }
295    }
296
297    progress(FirmwareUpdateStep::UploadingFirmware, None)?;
298    let mut upload_progress_cb = |current, total| {
299        progress(
300            FirmwareUpdateStep::UploadingFirmware,
301            Some((current, total)),
302        )
303        .is_ok()
304    };
305
306    client
307        .image_upload(
308            firmware,
309            maybe_target_image,
310            checksum,
311            params.upgrade_only,
312            has_progress.then_some(&mut upload_progress_cb),
313        )
314        .map_err(|err| {
315            if let MCUmgrClientError::ProgressCallbackError = err {
316                // Users expect this error when the progress callback errors
317                FirmwareUpdateError::ProgressCallbackError
318            } else {
319                FirmwareUpdateError::ImageUploadFailed(err)
320            }
321        })?;
322
323    progress(FirmwareUpdateStep::QueryingDeviceState, None)?;
324    image_state = client
325        .image_get_state()
326        .map_err(FirmwareUpdateError::GetStateFailed)?;
327    run_state = image_run_state::analyze(&image_state, target_image);
328
329    let needs_set_state = match run_state {
330        ImageRunState::Stable(current) => {
331            // Issue set-state if another image is currently running;
332            // this is probably the most common case.
333            if let Some(hash) = &current.hash {
334                hash != &image_id_hash
335            } else {
336                // We are most likely in MCUboot with hashes disabled;
337                // it's highly likely we uploaded to the active image
338                // and will break the system if we set-state now.
339                false
340            }
341        }
342
343        ImageRunState::Pending { next, .. } => {
344            // This should never happen, we already checked earlier that
345            // we are not pending, so if we now pend for an image that
346            // is not our target image something went horribly wrong
347            if let Some(hash) = &next.hash
348                && hash != &image_id_hash
349            {
350                return Err(FirmwareUpdateError::ImageAlreadyPending);
351            }
352
353            // The target image is already pending
354            false
355        }
356
357        ImageRunState::Testing { .. } => {
358            // Already running in test mode.
359            // Do **not** mark as confirmed, as MCUboot/Zephyr behavior is somewhat wild
360            // around how the image behaves when set-state is issued while testing.
361
362            // Whatever is currently being tested, we cannot `set-state` because of MCUboot quirks,
363            // and we also cannot reboot because that would interrupt the testing and revert
364            // to the previous image.
365            //
366            // So the only possible way to react here is to error out.
367
368            return Err(FirmwareUpdateError::ImageCurrentlyTested);
369        }
370
371        ImageRunState::Unknown(Some(guessed)) => {
372            // There's a good chance we are currently in MCUboot without image info
373            // enabled. Attempt to set-state only when our heuristic thinks
374            // we aren't already active.
375
376            // We need to be careful with calling set-state in MCUboot, see
377            // https://github.com/mcu-tools/mcuboot/issues/2882.
378
379            if let Some(hash) = &guessed.hash
380                && hash != &image_id_hash
381            {
382                return Err(FirmwareUpdateError::InconsistentDeviceState);
383            }
384
385            // We guess the image is already active;
386            // set-state could make the situation worse in MCUboot.
387
388            false
389        }
390
391        ImageRunState::Unknown(None) => {
392            // We just uploaded an image, if we still get not even a guess
393            // something is seriously wrong
394            return Err(FirmwareUpdateError::InconsistentDeviceState);
395        }
396
397        ImageRunState::Inconsistent => {
398            return Err(FirmwareUpdateError::InconsistentDeviceState);
399        }
400    };
401
402    if needs_set_state {
403        progress(FirmwareUpdateStep::ActivatingFirmware, None)?;
404        client
405            .image_set_state(Some(&image_id_hash), params.force_confirm)
406            .map_err(FirmwareUpdateError::SetStateFailed)?;
407    }
408
409    if !params.skip_reboot {
410        progress(FirmwareUpdateStep::TriggeringReboot, None)?;
411        client
412            .os_system_reset(false, None)
413            .map_err(FirmwareUpdateError::RebootFailed)?;
414    }
415
416    Ok(())
417}