Skip to main content

mcumgr_toolkit/
client.rs

1/// High-level firmware update routine
2mod firmware_update;
3
4pub use firmware_update::{
5    FirmwareUpdateError, FirmwareUpdateParams, FirmwareUpdateProgressCallback,
6};
7
8use std::{
9    collections::HashMap,
10    io::{self, Read, Write},
11    sync::atomic::AtomicUsize,
12    time::Duration,
13};
14
15use miette::Diagnostic;
16use rand::distr::SampleString;
17use serde::Serialize;
18use sha2::{Digest, Sha256};
19use thiserror::Error;
20
21use crate::{
22    bootloader::BootloaderInfo,
23    commands::{
24        self, fs::file_upload_max_data_chunk_size, image::image_upload_max_data_chunk_size,
25    },
26    connection::{Connection, ExecuteError},
27    transport::serial::{ConfigurableTimeout, SerialTransport},
28};
29
30/// The default SMP frame size of Zephyr.
31///
32/// Matches Zephyr default value of [MCUMGR_TRANSPORT_NETBUF_SIZE](https://github.com/zephyrproject-rtos/zephyr/blob/v4.2.1/subsys/mgmt/mcumgr/transport/Kconfig#L40).
33const ZEPHYR_DEFAULT_SMP_FRAME_SIZE: usize = 384;
34
35/// A high-level client for Zephyr's MCUmgr SMP protocol.
36///
37/// This struct is the central entry point of this crate.
38pub struct MCUmgrClient {
39    connection: Connection,
40    smp_frame_size: AtomicUsize,
41}
42
43/// Possible error values of [`MCUmgrClient::fs_file_download`].
44#[derive(Error, Debug, Diagnostic)]
45pub enum FileDownloadError {
46    /// The command failed in the SMP protocol layer.
47    #[error("Command execution failed")]
48    #[diagnostic(code(mcumgr_toolkit::client::file_download::execute))]
49    ExecuteError(#[from] ExecuteError),
50    /// A device response contained an unexpected offset value.
51    #[error("Received offset does not match requested offset")]
52    #[diagnostic(code(mcumgr_toolkit::client::file_download::offset_mismatch))]
53    UnexpectedOffset,
54    /// The writer returned an error.
55    #[error("Writer returned an error")]
56    #[diagnostic(code(mcumgr_toolkit::client::file_download::writer))]
57    WriterError(#[from] io::Error),
58    /// The received data does not match the reported file size.
59    #[error("Received data does not match reported size")]
60    #[diagnostic(code(mcumgr_toolkit::client::file_download::size_mismatch))]
61    SizeMismatch,
62    /// The received data unexpectedly did not report the file size.
63    #[error("Received data is missing file size information")]
64    #[diagnostic(code(mcumgr_toolkit::client::file_download::missing_size))]
65    MissingSize,
66    /// The progress callback returned an error.
67    #[error("Progress callback returned an error")]
68    #[diagnostic(code(mcumgr_toolkit::client::file_download::progress_cb_error))]
69    ProgressCallbackError,
70}
71
72/// Possible error values of [`MCUmgrClient::fs_file_upload`].
73#[derive(Error, Debug, Diagnostic)]
74pub enum FileUploadError {
75    /// The command failed in the SMP protocol layer.
76    #[error("Command execution failed")]
77    #[diagnostic(code(mcumgr_toolkit::client::file_upload::execute))]
78    ExecuteError(#[from] ExecuteError),
79    /// The reader returned an error.
80    #[error("Reader returned an error")]
81    #[diagnostic(code(mcumgr_toolkit::client::file_upload::reader))]
82    ReaderError(#[from] io::Error),
83    /// The progress callback returned an error.
84    #[error("Progress callback returned an error")]
85    #[diagnostic(code(mcumgr_toolkit::client::file_upload::progress_cb_error))]
86    ProgressCallbackError,
87    /// The current SMP frame size is too small for this command.
88    #[error("SMP frame size too small for this command")]
89    #[diagnostic(code(mcumgr_toolkit::client::file_upload::framesize_too_small))]
90    FrameSizeTooSmall(#[source] io::Error),
91}
92
93/// Possible error values of [`MCUmgrClient::image_upload`].
94#[derive(Error, Debug, Diagnostic)]
95pub enum ImageUploadError {
96    /// The command failed in the SMP protocol layer.
97    #[error("Command execution failed")]
98    #[diagnostic(code(mcumgr_toolkit::client::image_upload::execute))]
99    ExecuteError(#[from] ExecuteError),
100    /// The progress callback returned an error.
101    #[error("Progress callback returned an error")]
102    #[diagnostic(code(mcumgr_toolkit::client::image_upload::progress_cb_error))]
103    ProgressCallbackError,
104    /// The current SMP frame size is too small for this command.
105    #[error("SMP frame size too small for this command")]
106    #[diagnostic(code(mcumgr_toolkit::client::image_upload::framesize_too_small))]
107    FrameSizeTooSmall(#[source] io::Error),
108    /// A device response contained an unexpected offset value.
109    #[error("Received offset out of expected range")]
110    #[diagnostic(code(mcumgr_toolkit::client::image_upload::invalid_offset))]
111    UnexpectedOffset,
112    /// The device reported a checksum mismatch
113    #[error("Device reported checksum mismatch")]
114    #[diagnostic(code(mcumgr_toolkit::client::image_upload::checksum_mismatch_on_device))]
115    ChecksumMismatchOnDevice,
116    /// The firmware image does not match the given checksum
117    #[error("Firmware image does not match given checksum")]
118    #[diagnostic(code(mcumgr_toolkit::client::image_upload::checksum_mismatch))]
119    ChecksumMismatch,
120}
121
122/// Information about a serial port
123#[derive(Debug, Serialize, Clone, Eq, PartialEq)]
124pub struct UsbSerialPortInfo {
125    /// The identifier that the regex will match against
126    pub identifier: String,
127    /// The name of the port
128    pub port_name: String,
129    /// Information about the port
130    pub port_info: serialport::UsbPortInfo,
131}
132
133/// A list of available serial ports
134///
135/// Used for pretty error messages.
136#[derive(Serialize, Clone, Eq, PartialEq)]
137#[serde(transparent)]
138pub struct UsbSerialPorts(pub Vec<UsbSerialPortInfo>);
139impl std::fmt::Display for UsbSerialPorts {
140    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141        if self.0.is_empty() {
142            writeln!(f)?;
143            write!(f, " - None -")?;
144            return Ok(());
145        }
146
147        for UsbSerialPortInfo {
148            identifier,
149            port_name,
150            port_info,
151        } in &self.0
152        {
153            writeln!(f)?;
154            write!(f, " - {identifier}")?;
155
156            let mut print_port_string = true;
157            let port_string = format!("({port_name})");
158
159            if port_info.manufacturer.is_some() || port_info.product.is_some() {
160                write!(f, " -")?;
161                if let Some(manufacturer) = &port_info.manufacturer {
162                    let mut print_manufacturer = true;
163
164                    if let Some(product) = &port_info.product {
165                        if product.starts_with(manufacturer) {
166                            print_manufacturer = false;
167                        }
168                    }
169
170                    if print_manufacturer {
171                        write!(f, " {manufacturer}")?;
172                    }
173                }
174                if let Some(product) = &port_info.product {
175                    write!(f, " {product}")?;
176
177                    if product.ends_with(&port_string) {
178                        print_port_string = false;
179                    }
180                }
181            }
182
183            if print_port_string {
184                write!(f, " {port_string}")?;
185            }
186        }
187        Ok(())
188    }
189}
190impl std::fmt::Debug for UsbSerialPorts {
191    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
192        std::fmt::Debug::fmt(&self.0, f)
193    }
194}
195
196/// Possible error values of [`MCUmgrClient::new_from_usb_serial`].
197#[derive(Error, Debug, Diagnostic)]
198pub enum UsbSerialError {
199    /// Serialport error
200    #[error("Serialport returned an error")]
201    #[diagnostic(code(mcumgr_toolkit::usb_serial::serialport_error))]
202    SerialPortError(#[from] serialport::Error),
203    /// No port matched the given identifier
204    #[error("No serial port matched the identifier '{identifier}'\nAvailable ports:\n{available}")]
205    #[diagnostic(code(mcumgr_toolkit::usb_serial::no_matches))]
206    NoMatchingPort {
207        /// The original identifier provided by the user
208        identifier: String,
209        /// A list of available ports
210        available: UsbSerialPorts,
211    },
212    /// More than one port matched the given identifier
213    #[error("Multiple serial ports matched the identifier '{identifier}'\n{ports}")]
214    #[diagnostic(code(mcumgr_toolkit::usb_serial::multiple_matches))]
215    MultipleMatchingPorts {
216        /// The original identifier provided by the user
217        identifier: String,
218        /// The matching ports
219        ports: UsbSerialPorts,
220    },
221    /// Returned when the identifier was empty;
222    /// can be used to query all available ports
223    #[error("An empty identifier was provided")]
224    #[diagnostic(code(mcumgr_toolkit::usb_serial::empty_identifier))]
225    IdentifierEmpty {
226        /// A list of available ports
227        ports: UsbSerialPorts,
228    },
229    /// The given identifier was not a valid RegEx
230    #[error("The given identifier was not a valid RegEx")]
231    #[diagnostic(code(mcumgr_toolkit::usb_serial::regex_error))]
232    RegexError(#[from] regex::Error),
233}
234
235impl MCUmgrClient {
236    /// Creates a Zephyr MCUmgr SMP client based on a configured and opened serial port.
237    ///
238    /// ```no_run
239    /// # use mcumgr_toolkit::MCUmgrClient;
240    /// # fn main() {
241    /// let serial = serialport::new("COM42", 115200)
242    ///     .timeout(std::time::Duration::from_millis(10000))
243    ///     .open()
244    ///     .unwrap();
245    ///
246    /// let mut client = MCUmgrClient::new_from_serial(serial);
247    /// # }
248    /// ```
249    pub fn new_from_serial<T: Send + Read + Write + ConfigurableTimeout + 'static>(
250        serial: T,
251    ) -> Self {
252        Self {
253            connection: Connection::new(SerialTransport::new(serial)),
254            smp_frame_size: ZEPHYR_DEFAULT_SMP_FRAME_SIZE.into(),
255        }
256    }
257
258    /// Creates a Zephyr MCUmgr SMP client based on a USB serial port identified by VID:PID.
259    ///
260    /// Useful for programming many devices in rapid succession, as Windows usually
261    /// gives each one a different COMxx identifier.
262    ///
263    /// # Arguments
264    ///
265    /// * `identifier` - A regex that identifies the device.
266    /// * `baud_rate` - The baud rate the port should operate at.
267    /// * `timeout` - The communication timeout.
268    ///
269    /// # Identifier examples
270    ///
271    /// - `1234:89AB` - Vendor ID 1234, Product ID 89AB. Will fail if product has multiple serial ports.
272    /// - `1234:89AB:12` - Vendor ID 1234, Product ID 89AB, Interface 12.
273    /// - `1234:.*:[2-3]` - Vendor ID 1234, any Product Id, Interface 2 or 3.
274    ///
275    pub fn new_from_usb_serial(
276        identifier: impl AsRef<str>,
277        baud_rate: u32,
278        timeout: Duration,
279    ) -> Result<Self, UsbSerialError> {
280        let identifier = identifier.as_ref();
281
282        let ports = serialport::available_ports()?
283            .into_iter()
284            .filter_map(|port| {
285                if let serialport::SerialPortType::UsbPort(port_info) = port.port_type {
286                    if let Some(interface) = port_info.interface {
287                        Some(UsbSerialPortInfo {
288                            identifier: format!(
289                                "{:04x}:{:04x}:{}",
290                                port_info.vid, port_info.pid, interface
291                            ),
292                            port_name: port.port_name,
293                            port_info,
294                        })
295                    } else {
296                        Some(UsbSerialPortInfo {
297                            identifier: format!("{:04x}:{:04x}", port_info.vid, port_info.pid),
298                            port_name: port.port_name,
299                            port_info,
300                        })
301                    }
302                } else {
303                    None
304                }
305            })
306            .collect::<Vec<_>>();
307
308        if identifier.is_empty() {
309            return Err(UsbSerialError::IdentifierEmpty {
310                ports: UsbSerialPorts(ports),
311            });
312        }
313
314        let port_regex = regex::RegexBuilder::new(identifier)
315            .case_insensitive(true)
316            .unicode(true)
317            .build()?;
318
319        let matches = ports
320            .iter()
321            .filter(|port| {
322                if let Some(m) = port_regex.find(&port.identifier) {
323                    // Only accept if the regex matches at the beginning of the string
324                    m.start() == 0
325                } else {
326                    false
327                }
328            })
329            .cloned()
330            .collect::<Vec<_>>();
331
332        if matches.len() > 1 {
333            return Err(UsbSerialError::MultipleMatchingPorts {
334                identifier: identifier.to_string(),
335                ports: UsbSerialPorts(matches),
336            });
337        }
338
339        let port_name = match matches.into_iter().next() {
340            Some(port) => port.port_name,
341            None => {
342                return Err(UsbSerialError::NoMatchingPort {
343                    identifier: identifier.to_string(),
344                    available: UsbSerialPorts(ports),
345                });
346            }
347        };
348
349        let serial = serialport::new(port_name, baud_rate)
350            .timeout(timeout)
351            .open()?;
352
353        Ok(Self::new_from_serial(serial))
354    }
355
356    /// Configures the maximum SMP frame size that we can send to the device.
357    ///
358    /// Must not exceed [`MCUMGR_TRANSPORT_NETBUF_SIZE`](https://github.com/zephyrproject-rtos/zephyr/blob/v4.2.1/subsys/mgmt/mcumgr/transport/Kconfig#L40),
359    /// otherwise we might crash the device.
360    pub fn set_frame_size(&self, smp_frame_size: usize) {
361        self.smp_frame_size
362            .store(smp_frame_size, std::sync::atomic::Ordering::SeqCst);
363    }
364
365    /// Configures the maximum SMP frame size that we can send to the device automatically
366    /// by reading the value of [`MCUMGR_TRANSPORT_NETBUF_SIZE`](https://github.com/zephyrproject-rtos/zephyr/blob/v4.2.1/subsys/mgmt/mcumgr/transport/Kconfig#L40)
367    /// from the device.
368    pub fn use_auto_frame_size(&self) -> Result<(), ExecuteError> {
369        let mcumgr_params = self
370            .connection
371            .execute_command(&commands::os::MCUmgrParameters)?;
372
373        log::debug!("Using frame size {}.", mcumgr_params.buf_size);
374
375        self.smp_frame_size.store(
376            mcumgr_params.buf_size as usize,
377            std::sync::atomic::Ordering::SeqCst,
378        );
379
380        Ok(())
381    }
382
383    /// Changes the communication timeout.
384    ///
385    /// When the device does not respond to packets within the set
386    /// duration, an error will be raised.
387    pub fn set_timeout(&self, timeout: Duration) -> Result<(), miette::Report> {
388        self.connection.set_timeout(timeout)
389    }
390
391    /// Checks if the device is alive and responding.
392    ///
393    /// Runs a simple echo with random data and checks if the response matches.
394    ///
395    /// # Return
396    ///
397    /// An error if the device is not alive and responding.
398    pub fn check_connection(&self) -> Result<(), ExecuteError> {
399        let random_message = rand::distr::Alphanumeric.sample_string(&mut rand::rng(), 16);
400        let response = self.os_echo(&random_message)?;
401        if random_message == response {
402            Ok(())
403        } else {
404            Err(ExecuteError::ReceiveFailed(
405                crate::transport::ReceiveError::UnexpectedResponse,
406            ))
407        }
408    }
409
410    /// High-level firmware update routine.
411    ///
412    /// # Arguments
413    ///
414    /// * `firmware` - The firmware image data.
415    /// * `checksum` - SHA256 of the firmware image. Optional.
416    /// * `params` - Configurable parameters.
417    /// * `progress` - A callback that receives progress updates.
418    ///
419    pub fn firmware_update(
420        &self,
421        firmware: impl AsRef<[u8]>,
422        checksum: Option<[u8; 32]>,
423        params: FirmwareUpdateParams,
424        progress: Option<&mut FirmwareUpdateProgressCallback>,
425    ) -> Result<(), FirmwareUpdateError> {
426        firmware_update::firmware_update(self, firmware, checksum, params, progress)
427    }
428
429    /// Sends a message to the device and expects the same message back as response.
430    ///
431    /// This can be used as a sanity check for whether the device is connected and responsive.
432    pub fn os_echo(&self, msg: impl AsRef<str>) -> Result<String, ExecuteError> {
433        self.connection
434            .execute_command(&commands::os::Echo { d: msg.as_ref() })
435            .map(|resp| resp.r)
436    }
437
438    /// Queries live task statistics
439    ///
440    /// # Note
441    ///
442    /// Converts `stkuse` and `stksiz` to bytes.
443    /// Zephyr originally reports them as number of 4 byte words.
444    ///
445    /// # Return
446    ///
447    /// A map of task names with their respective statistics
448    pub fn os_task_statistics(
449        &self,
450    ) -> Result<HashMap<String, commands::os::TaskStatisticsEntry>, ExecuteError> {
451        self.connection
452            .execute_command(&commands::os::TaskStatistics)
453            .map(|resp| {
454                let mut tasks = resp.tasks;
455                for (_, stats) in tasks.iter_mut() {
456                    stats.stkuse = stats.stkuse.map(|val| val * 4);
457                    stats.stksiz = stats.stksiz.map(|val| val * 4);
458                }
459                tasks
460            })
461    }
462
463    /// Sets the RTC of the device to the given datetime.
464    pub fn os_set_datetime(&self, datetime: chrono::NaiveDateTime) -> Result<(), ExecuteError> {
465        self.connection
466            .execute_command(&commands::os::DateTimeSet { datetime })
467            .map(Into::into)
468    }
469
470    /// Retrieves the device RTC's datetime.
471    pub fn os_get_datetime(&self) -> Result<chrono::NaiveDateTime, ExecuteError> {
472        self.connection
473            .execute_command(&commands::os::DateTimeGet)
474            .map(|val| val.datetime)
475    }
476
477    /// Issues a system reset.
478    ///
479    /// # Arguments
480    ///
481    /// * `force` - Issues a force reset.
482    /// * `boot_mode` - Overwrites the boot mode.
483    ///
484    /// Known `boot_mode` values:
485    /// * `0` - Normal system boot
486    /// * `1` - Bootloader recovery mode
487    ///
488    /// Note that `boot_mode` only works if [`MCUMGR_GRP_OS_RESET_BOOT_MODE`](https://docs.zephyrproject.org/latest/kconfig.html#CONFIG_MCUMGR_GRP_OS_RESET_BOOT_MODE) is enabled.
489    ///
490    pub fn os_system_reset(&self, force: bool, boot_mode: Option<u8>) -> Result<(), ExecuteError> {
491        self.connection
492            .execute_command(&commands::os::SystemReset { force, boot_mode })
493            .map(Into::into)
494    }
495
496    /// Fetch parameters from the MCUmgr library
497    pub fn os_mcumgr_parameters(
498        &self,
499    ) -> Result<commands::os::MCUmgrParametersResponse, ExecuteError> {
500        self.connection
501            .execute_command(&commands::os::MCUmgrParameters)
502    }
503
504    /// Fetch information on the running image
505    ///
506    /// Similar to Linux's `uname` command.
507    ///
508    /// # Arguments
509    ///
510    /// * `format` - Format specifier for the returned response
511    ///
512    /// For more information about the format specifier fields, see
513    /// the [SMP documentation](https://docs.zephyrproject.org/latest/services/device_mgmt/smp_groups/smp_group_0.html#os-application-info-request).
514    ///
515    pub fn os_application_info(&self, format: Option<&str>) -> Result<String, ExecuteError> {
516        self.connection
517            .execute_command(&commands::os::ApplicationInfo { format })
518            .map(|resp| resp.output)
519    }
520
521    /// Fetch information on the device's bootloader
522    pub fn os_bootloader_info(&self) -> Result<BootloaderInfo, ExecuteError> {
523        Ok(
524            match self
525                .connection
526                .execute_command(&commands::os::BootloaderInfo)?
527                .bootloader
528                .as_str()
529            {
530                "MCUboot" => {
531                    let mode_data = self
532                        .connection
533                        .execute_command(&commands::os::BootloaderInfoMcubootMode {})?;
534                    BootloaderInfo::MCUboot {
535                        mode: mode_data.mode,
536                        no_downgrade: mode_data.no_downgrade,
537                    }
538                }
539                name => BootloaderInfo::Unknown {
540                    name: name.to_string(),
541                },
542            },
543        )
544    }
545
546    /// Obtain a list of images with their current state.
547    pub fn image_get_state(&self) -> Result<Vec<commands::image::ImageState>, ExecuteError> {
548        self.connection
549            .execute_command(&commands::image::GetImageState)
550            .map(|val| val.images)
551    }
552
553    /// Modify the current image state
554    ///
555    /// # Arguments
556    ///
557    /// * `hash` - the SHA256 id of the image.
558    /// * `confirm` - mark the given image as 'confirmed'
559    ///
560    /// If `confirm` is `false`, perform a test boot with the given image and revert upon hard reset.
561    ///
562    /// If `confirm` is `true`, boot to the given image and mark it as `confirmed`. If `hash` is omitted,
563    /// confirm the currently running image.
564    ///
565    /// Note that `hash` will not be the same as the SHA256 of the whole firmware image,
566    /// it is the field in the MCUboot TLV section that contains a hash of the data
567    /// which is used for signature verification purposes.
568    pub fn image_set_state(
569        &self,
570        hash: Option<[u8; 32]>,
571        confirm: bool,
572    ) -> Result<Vec<commands::image::ImageState>, ExecuteError> {
573        self.connection
574            .execute_command(&commands::image::SetImageState {
575                hash: hash.as_ref(),
576                confirm,
577            })
578            .map(|val| val.images)
579    }
580
581    /// Upload a firmware image to an image slot.
582    ///
583    /// # Arguments
584    ///
585    /// * `data` - The firmware image data
586    /// * `image` - Selects target image on the device. Defaults to `0`.
587    /// * `checksum` - The SHA256 checksum of the image. If missing, will be computed from the image data.
588    /// * `upgrade_only` - If true, allow firmware upgrades only and reject downgrades.
589    /// * `progress` - A callback that receives a pair of (transferred, total) bytes and returns false on error.
590    ///
591    pub fn image_upload(
592        &self,
593        data: impl AsRef<[u8]>,
594        image: Option<u32>,
595        checksum: Option<[u8; 32]>,
596        upgrade_only: bool,
597        mut progress: Option<&mut dyn FnMut(u64, u64) -> bool>,
598    ) -> Result<(), ImageUploadError> {
599        let chunk_size_max = image_upload_max_data_chunk_size(
600            self.smp_frame_size
601                .load(std::sync::atomic::Ordering::SeqCst),
602        )
603        .map_err(ImageUploadError::FrameSizeTooSmall)?;
604
605        let data = data.as_ref();
606
607        let actual_checksum: [u8; 32] = Sha256::digest(data).into();
608        if let Some(checksum) = checksum {
609            if actual_checksum != checksum {
610                return Err(ImageUploadError::ChecksumMismatch);
611            }
612        }
613
614        let mut offset = 0;
615        let size = data.len();
616
617        let mut checksum_matched = None;
618
619        while offset < size {
620            let current_chunk_size = (size - offset).min(chunk_size_max);
621            let chunk_data = &data[offset..offset + current_chunk_size];
622
623            let upload_response = if offset == 0 {
624                self.connection
625                    .execute_command(&commands::image::ImageUpload {
626                        image,
627                        len: Some(size as u64),
628                        off: offset as u64,
629                        sha: Some(&actual_checksum),
630                        data: chunk_data,
631                        upgrade: Some(upgrade_only),
632                    })?
633            } else {
634                self.connection
635                    .execute_command(&commands::image::ImageUpload {
636                        image: None,
637                        len: None,
638                        off: offset as u64,
639                        sha: None,
640                        data: chunk_data,
641                        upgrade: None,
642                    })?
643            };
644
645            offset = upload_response
646                .off
647                .try_into()
648                .map_err(|_| ImageUploadError::UnexpectedOffset)?;
649
650            if offset > size {
651                return Err(ImageUploadError::UnexpectedOffset);
652            }
653
654            if let Some(progress) = &mut progress {
655                if !progress(offset as u64, size as u64) {
656                    return Err(ImageUploadError::ProgressCallbackError);
657                };
658            }
659
660            if let Some(is_match) = upload_response.r#match {
661                checksum_matched = Some(is_match);
662            }
663        }
664
665        if let Some(checksum_matched) = checksum_matched {
666            if !checksum_matched {
667                return Err(ImageUploadError::ChecksumMismatchOnDevice);
668            }
669        } else {
670            log::warn!("Device did not perform image checksum verification");
671        }
672
673        Ok(())
674    }
675
676    /// Erase image slot on target device.
677    ///
678    /// # Arguments
679    ///
680    /// * `slot` - The slot ID of the image to erase. Slot `1` if omitted.
681    ///
682    pub fn image_erase(&self, slot: Option<u32>) -> Result<(), ExecuteError> {
683        self.connection
684            .execute_command(&commands::image::ImageErase { slot })
685            .map(Into::into)
686    }
687
688    /// Obtain a list of available image slots.
689    pub fn image_slot_info(&self) -> Result<Vec<commands::image::SlotInfoImage>, ExecuteError> {
690        self.connection
691            .execute_command(&commands::image::SlotInfo)
692            .map(|val| val.images)
693    }
694
695    /// Load a file from the device.
696    ///
697    /// # Arguments
698    ///
699    /// * `name` - The full path of the file on the device.
700    /// * `writer` - A [`Write`] object that the file content will be written to.
701    /// * `progress` - A callback that receives a pair of (transferred, total) bytes.
702    ///
703    /// # Performance
704    ///
705    /// Downloading files with Zephyr's default parameters is slow.
706    /// You want to increase [`MCUMGR_TRANSPORT_NETBUF_SIZE`](https://github.com/zephyrproject-rtos/zephyr/blob/v4.2.1/subsys/mgmt/mcumgr/transport/Kconfig#L40)
707    /// to maybe `4096` or larger.
708    pub fn fs_file_download<T: Write>(
709        &self,
710        name: impl AsRef<str>,
711        mut writer: T,
712        mut progress: Option<&mut dyn FnMut(u64, u64) -> bool>,
713    ) -> Result<(), FileDownloadError> {
714        let name = name.as_ref();
715        let response = self
716            .connection
717            .execute_command(&commands::fs::FileDownload { name, off: 0 })?;
718
719        let file_len = response.len.ok_or(FileDownloadError::MissingSize)?;
720        if response.off != 0 {
721            return Err(FileDownloadError::UnexpectedOffset);
722        }
723
724        let mut offset = 0;
725
726        if let Some(progress) = &mut progress {
727            if !progress(offset, file_len) {
728                return Err(FileDownloadError::ProgressCallbackError);
729            };
730        }
731
732        writer.write_all(&response.data)?;
733        offset += response.data.len() as u64;
734
735        if let Some(progress) = &mut progress {
736            if !progress(offset, file_len) {
737                return Err(FileDownloadError::ProgressCallbackError);
738            };
739        }
740
741        while offset < file_len {
742            let response = self
743                .connection
744                .execute_command(&commands::fs::FileDownload { name, off: offset })?;
745
746            if response.off != offset {
747                return Err(FileDownloadError::UnexpectedOffset);
748            }
749
750            writer.write_all(&response.data)?;
751            offset += response.data.len() as u64;
752
753            if let Some(progress) = &mut progress {
754                if !progress(offset, file_len) {
755                    return Err(FileDownloadError::ProgressCallbackError);
756                };
757            }
758        }
759
760        if offset != file_len {
761            return Err(FileDownloadError::SizeMismatch);
762        }
763
764        Ok(())
765    }
766
767    /// Write a file to the device.
768    ///
769    /// # Arguments
770    ///
771    /// * `name` - The full path of the file on the device.
772    /// * `reader` - A [`Read`] object that contains the file content.
773    /// * `size` - The file size.
774    /// * `progress` - A callback that receives a pair of (transferred, total) bytes and returns false on error.
775    ///
776    /// # Performance
777    ///
778    /// Uploading files with Zephyr's default parameters is slow.
779    /// You want to increase [`MCUMGR_TRANSPORT_NETBUF_SIZE`](https://github.com/zephyrproject-rtos/zephyr/blob/v4.2.1/subsys/mgmt/mcumgr/transport/Kconfig#L40)
780    /// to maybe `4096` and then enable larger chunking through either [`MCUmgrClient::set_frame_size`]
781    /// or [`MCUmgrClient::use_auto_frame_size`].
782    pub fn fs_file_upload<T: Read>(
783        &self,
784        name: impl AsRef<str>,
785        mut reader: T,
786        size: u64,
787        mut progress: Option<&mut dyn FnMut(u64, u64) -> bool>,
788    ) -> Result<(), FileUploadError> {
789        let name = name.as_ref();
790
791        let chunk_size_max = file_upload_max_data_chunk_size(
792            self.smp_frame_size
793                .load(std::sync::atomic::Ordering::SeqCst),
794            name,
795        )
796        .map_err(FileUploadError::FrameSizeTooSmall)?;
797        let mut data_buffer = vec![0u8; chunk_size_max].into_boxed_slice();
798
799        let mut offset = 0;
800
801        while offset < size {
802            let current_chunk_size = (size - offset).min(data_buffer.len() as u64) as usize;
803
804            let chunk_buffer = &mut data_buffer[..current_chunk_size];
805            reader.read_exact(chunk_buffer)?;
806
807            self.connection.execute_command(&commands::fs::FileUpload {
808                off: offset,
809                data: chunk_buffer,
810                name,
811                len: if offset == 0 { Some(size) } else { None },
812            })?;
813
814            offset += chunk_buffer.len() as u64;
815
816            if let Some(progress) = &mut progress {
817                if !progress(offset, size) {
818                    return Err(FileUploadError::ProgressCallbackError);
819                };
820            }
821        }
822
823        Ok(())
824    }
825
826    /// Queries the file status
827    pub fn fs_file_status(
828        &self,
829        name: impl AsRef<str>,
830    ) -> Result<commands::fs::FileStatusResponse, ExecuteError> {
831        self.connection.execute_command(&commands::fs::FileStatus {
832            name: name.as_ref(),
833        })
834    }
835
836    /// Computes the hash/checksum of a file
837    ///
838    /// For available algorithms, see [`fs_supported_checksum_types()`](MCUmgrClient::fs_supported_checksum_types).
839    ///
840    /// # Arguments
841    ///
842    /// * `name` - The absolute path of the file on the device
843    /// * `algorithm` - The hash/checksum algorithm to use, or default if None
844    /// * `offset` - How many bytes of the file to skip
845    /// * `length` - How many bytes to read after `offset`. None for the entire file.
846    ///
847    pub fn fs_file_checksum(
848        &self,
849        name: impl AsRef<str>,
850        algorithm: Option<impl AsRef<str>>,
851        offset: u64,
852        length: Option<u64>,
853    ) -> Result<commands::fs::FileChecksumResponse, ExecuteError> {
854        self.connection
855            .execute_command(&commands::fs::FileChecksum {
856                name: name.as_ref(),
857                r#type: algorithm.as_ref().map(AsRef::as_ref),
858                off: offset,
859                len: length,
860            })
861    }
862
863    /// Queries which hash/checksum algorithms are available on the target
864    pub fn fs_supported_checksum_types(
865        &self,
866    ) -> Result<HashMap<String, commands::fs::FileChecksumProperties>, ExecuteError> {
867        self.connection
868            .execute_command(&commands::fs::SupportedFileChecksumTypes)
869            .map(|val| val.types)
870    }
871
872    /// Close all device files MCUmgr has currently open
873    pub fn fs_file_close(&self) -> Result<(), ExecuteError> {
874        self.connection
875            .execute_command(&commands::fs::FileClose)
876            .map(Into::into)
877    }
878
879    /// Run a shell command.
880    ///
881    /// # Arguments
882    ///
883    /// * `argv` - The shell command to be executed.
884    ///
885    /// # Return
886    ///
887    /// A tuple of (returncode, stdout) produced by the command execution.
888    pub fn shell_execute(&self, argv: &[String]) -> Result<(i32, String), ExecuteError> {
889        self.connection
890            .execute_command(&commands::shell::ShellCommandLineExecute { argv })
891            .map(|ret| (ret.ret, ret.o))
892    }
893
894    /// Erase the `storage_partition` flash partition.
895    pub fn zephyr_erase_storage(&self) -> Result<(), ExecuteError> {
896        self.connection
897            .execute_command(&commands::zephyr::EraseStorage)
898            .map(Into::into)
899    }
900
901    /// Execute a raw [`commands::McuMgrCommand`].
902    ///
903    /// Only returns if no error happened, so the
904    /// user does not need to check for an `rc` or `err`
905    /// field in the response.
906    pub fn raw_command<T: commands::McuMgrCommand>(
907        &self,
908        command: &T,
909    ) -> Result<T::Response, ExecuteError> {
910        self.connection.execute_command(command)
911    }
912}