mcumgr_toolkit/client/
firmware_update.rs1use 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#[derive(Error, Debug, Diagnostic)]
13pub enum FirmwareUpdateError {
14 #[error("Progress callback returned an error")]
16 #[diagnostic(code(mcumgr_toolkit::firmware_update::progress_cb_error))]
17 ProgressCallbackError,
18 #[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 #[error("Bootloader '{0}' not supported")]
25 #[diagnostic(code(mcumgr_toolkit::firmware_update::unknown_bootloader))]
26 BootloaderNotSupported(String),
27 #[error("Firmware is not a valid MCUboot image")]
29 #[diagnostic(code(mcumgr_toolkit::firmware_update::mcuboot_image))]
30 InvalidMcuBootFirmwareImage(#[from] mcuboot::ImageParseError),
31 #[error("Failed to fetch image state from device")]
33 #[diagnostic(code(mcumgr_toolkit::firmware_update::get_image_state))]
34 GetStateFailed(#[source] ExecuteError),
35 #[error("Failed to upload firmware image to device")]
37 #[diagnostic(code(mcumgr_toolkit::firmware_update::image_upload))]
38 ImageUploadFailed(#[from] ImageUploadError),
39 #[error("Failed to activate new firmware image")]
41 #[diagnostic(code(mcumgr_toolkit::firmware_update::set_image_state))]
42 SetStateFailed(#[source] ExecuteError),
43 #[error("Failed to trigger device reboot")]
45 #[diagnostic(code(mcumgr_toolkit::firmware_update::reboot))]
46 RebootFailed(#[source] ExecuteError),
47 #[error("The device is already running the given firmware")]
49 #[diagnostic(code(mcumgr_toolkit::firmware_update::already_installed))]
50 AlreadyInstalled,
51}
52
53#[derive(Default)]
55pub struct FirmwareUpdateParams {
56 pub bootloader_type: Option<BootloaderType>,
61 pub skip_reboot: bool,
65 pub force_confirm: bool,
69 pub upgrade_only: bool,
73}
74
75pub type FirmwareUpdateProgressCallback<'a> = dyn FnMut(&str, Option<(u64, u64)>) -> bool + 'a;
87
88const SHOWN_HASH_DIGITS: usize = 4;
89
90pub(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 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 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 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}