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#[non_exhaustive]
18#[derive(Error, Debug, Diagnostic)]
19pub enum FirmwareUpdateError {
20 #[error("Progress callback returned an error")]
22 #[diagnostic(code(mcumgr_toolkit::firmware_update::progress_cb_error))]
23 ProgressCallbackError,
24 #[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 #[error("Bootloader '{0}' not supported")]
31 #[diagnostic(code(mcumgr_toolkit::firmware_update::unknown_bootloader))]
32 BootloaderNotSupported(String),
33 #[error("Firmware is not a valid MCUboot image")]
35 #[diagnostic(code(mcumgr_toolkit::firmware_update::mcuboot_image))]
36 InvalidMcuBootFirmwareImage(#[from] mcuboot::ImageParseError),
37 #[error("Failed to fetch image state from device")]
39 #[diagnostic(code(mcumgr_toolkit::firmware_update::get_image_state))]
40 GetStateFailed(#[source] MCUmgrClientError),
41 #[error("Failed to upload firmware image to device")]
43 #[diagnostic(code(mcumgr_toolkit::firmware_update::image_upload))]
44 ImageUploadFailed(#[source] MCUmgrClientError),
45 #[error("Failed to activate new firmware image")]
47 #[diagnostic(code(mcumgr_toolkit::firmware_update::set_image_state))]
48 SetStateFailed(#[source] MCUmgrClientError),
49 #[error("Failed to trigger device reboot")]
51 #[diagnostic(code(mcumgr_toolkit::firmware_update::reboot))]
52 RebootFailed(#[source] MCUmgrClientError),
53 #[error("The device is already running the given firmware")]
55 #[diagnostic(code(mcumgr_toolkit::firmware_update::already_installed))]
56 AlreadyInstalled,
57 #[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 #[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 #[error("The device state is inconsistent")]
71 #[diagnostic(code(mcumgr_toolkit::firmware_update::inconsistent_device_state))]
72 InconsistentDeviceState,
73}
74
75#[derive(Clone, Debug, Default)]
77pub struct FirmwareUpdateParams {
78 pub bootloader_type: Option<BootloaderType>,
83 pub skip_reboot: bool,
87 pub force_confirm: bool,
94 pub upgrade_only: bool,
98}
99
100#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
102pub enum FirmwareUpdateStep {
103 DetectingBootloader,
105 BootloaderFound(BootloaderType),
107 ParsingFirmwareImage,
109 QueryingDeviceState,
111 UpdateInfo {
113 current_version: Option<(String, Option<Vec<u8>>)>,
115 new_version: (String, Vec<u8>),
117 },
118 UploadingFirmware,
120 ActivatingFirmware,
122 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)) = ¤t_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
169pub type FirmwareUpdateProgressCallback<'a> =
181 dyn FnMut(FirmwareUpdateStep, Option<(u64, u64)>) -> bool + 'a;
182
183const SHOWN_HASH_DIGITS: usize = 4;
184
185pub(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 let maybe_target_image: Option<u32> = Default::default();
204
205 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 }
286
287 ImageRunState::Unknown(Some(_)) => {
288 }
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 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 if let Some(hash) = ¤t.hash {
334 hash != &image_id_hash
335 } else {
336 false
340 }
341 }
342
343 ImageRunState::Pending { next, .. } => {
344 if let Some(hash) = &next.hash
348 && hash != &image_id_hash
349 {
350 return Err(FirmwareUpdateError::ImageAlreadyPending);
351 }
352
353 false
355 }
356
357 ImageRunState::Testing { .. } => {
358 return Err(FirmwareUpdateError::ImageCurrentlyTested);
369 }
370
371 ImageRunState::Unknown(Some(guessed)) => {
372 if let Some(hash) = &guessed.hash
380 && hash != &image_id_hash
381 {
382 return Err(FirmwareUpdateError::InconsistentDeviceState);
383 }
384
385 false
389 }
390
391 ImageRunState::Unknown(None) => {
392 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}