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