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