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