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        IntoTransport, ReceiveError, SMP_TRANSFER_BUFFER_SIZE,
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::new_from_transport(SerialTransport::new(serial))
341    }
342
343    /// Creates a Zephyr MCUmgr SMP client based on a USB serial port identified by VID:PID.
344    ///
345    /// Useful for programming many devices in rapid succession, as Windows usually
346    /// gives each one a different COMxx identifier.
347    ///
348    /// # Arguments
349    ///
350    /// * `identifier` - A regex that identifies the device.
351    /// * `baud_rate` - The baud rate the port should operate at.
352    /// * `timeout` - The communication timeout.
353    ///
354    /// # Identifier examples
355    ///
356    /// - `1234:89AB` - Vendor ID 1234, Product ID 89AB. Will fail if product has multiple serial ports.
357    /// - `1234:89AB:12` - Vendor ID 1234, Product ID 89AB, Interface 12.
358    /// - `1234:.*:[2-3]` - Vendor ID 1234, any Product Id, Interface 2 or 3.
359    ///
360    pub fn new_from_usb_serial(
361        identifier: impl AsRef<str>,
362        baud_rate: u32,
363        timeout: Duration,
364    ) -> Result<Self, UsbSerialError> {
365        let identifier = identifier.as_ref();
366
367        let ports = serialport::available_ports()?
368            .into_iter()
369            .filter_map(|port| {
370                if let serialport::SerialPortType::UsbPort(port_info) = port.port_type {
371                    if let Some(interface) = port_info.interface {
372                        Some(UsbSerialPortInfo {
373                            identifier: format!(
374                                "{:04x}:{:04x}:{}",
375                                port_info.vid, port_info.pid, interface
376                            ),
377                            port_name: port.port_name,
378                            port_info,
379                        })
380                    } else {
381                        Some(UsbSerialPortInfo {
382                            identifier: format!("{:04x}:{:04x}", port_info.vid, port_info.pid),
383                            port_name: port.port_name,
384                            port_info,
385                        })
386                    }
387                } else {
388                    None
389                }
390            })
391            .collect::<Vec<_>>();
392
393        if identifier.is_empty() {
394            return Err(UsbSerialError::IdentifierEmpty {
395                ports: UsbSerialPorts(ports),
396            });
397        }
398
399        let port_regex = regex::RegexBuilder::new(identifier)
400            .case_insensitive(true)
401            .unicode(true)
402            .build()?;
403
404        let matches = ports
405            .iter()
406            .filter(|port| {
407                if let Some(m) = port_regex.find(&port.identifier) {
408                    // Only accept if the regex matches at the beginning of the string
409                    m.start() == 0
410                } else {
411                    false
412                }
413            })
414            .cloned()
415            .collect::<Vec<_>>();
416
417        if matches.len() > 1 {
418            return Err(UsbSerialError::MultipleMatchingPorts {
419                identifier: identifier.to_string(),
420                ports: UsbSerialPorts(matches),
421            });
422        }
423
424        let port_name = match matches.into_iter().next() {
425            Some(port) => port.port_name,
426            None => {
427                return Err(UsbSerialError::NoMatchingPort {
428                    identifier: identifier.to_string(),
429                    available: UsbSerialPorts(ports),
430                });
431            }
432        };
433
434        let serial = serialport::new(port_name, baud_rate)
435            .timeout(timeout)
436            .open()?;
437
438        Ok(Self::new_from_serial(serial))
439    }
440
441    /// Creates a Zephyr MCUmgr SMP client based on a BLE connection.
442    ///
443    /// # Arguments
444    ///
445    /// * `identifier` - An OS dependent identifier for BLE devices.
446    /// * `timeout` - The communication timeout.
447    ///
448    #[cfg(feature = "ble")]
449    pub fn new_from_ble(
450        identifier: Option<BleIdentifier>,
451        timeout: Duration,
452    ) -> Result<Self, BleError> {
453        Self::new_from_ble_with_scan_callback(identifier, timeout, || {})
454    }
455
456    /// Creates a Zephyr MCUmgr SMP client based on a BLE connection.
457    ///
458    /// Additionally, notifies the caller when a full BLE discovery scan has started.
459    ///
460    /// # Arguments
461    ///
462    /// * `identifier` - An OS dependent identifier for BLE devices.
463    /// * `timeout` - The communication timeout.
464    /// * `on_start_scanning` - A callback that gets executed if a full BLE discovery scan was started
465    ///
466    #[cfg(feature = "ble")]
467    pub fn new_from_ble_with_scan_callback(
468        identifier: Option<BleIdentifier>,
469        timeout: Duration,
470        on_start_scanning: impl FnOnce(),
471    ) -> Result<Self, BleError> {
472        let scan_timeout = Duration::from_secs(3);
473        let connect_timeout = Duration::from_secs(5).max(timeout);
474        let connection = crate::transport::ble::connect_to_device(
475            identifier,
476            scan_timeout,
477            connect_timeout,
478            on_start_scanning,
479        )?;
480
481        let transport = crate::transport::ble::BleTransport::from_connection(connection, timeout)?;
482        Ok(Self::new_from_transport(transport))
483    }
484
485    /// Creates a Zephyr MCUmgr SMP client from a generic [`Transport`](crate::transport::Transport).
486    ///
487    /// # Arguments
488    ///
489    /// * `transport` - The transport the client should communicate over
490    ///
491    pub fn new_from_transport<T: IntoTransport>(transport: T) -> Self {
492        Self {
493            connection: Connection::new(transport),
494            smp_frame_size: ZEPHYR_DEFAULT_SMP_FRAME_SIZE.into(),
495        }
496    }
497
498    /// Creates a Zephyr MCUmgr SMP client based on a UDP socket.
499    ///
500    /// # Arguments
501    ///
502    /// * `addr` - The remote UDP endpoint.
503    /// * `timeout` - The communication timeout.
504    ///
505    /// # Example
506    ///
507    /// ```no_run
508    /// # use mcumgr_toolkit::MCUmgrClient;
509    /// # use std::time::Duration;
510    /// # use std::net::SocketAddr;
511    /// # fn main() {
512    /// let addr: SocketAddr = "192.168.1.1:1337".parse().unwrap();
513    /// let mut client = MCUmgrClient::new_from_udp(addr, Duration::from_millis(1000)).unwrap();
514    /// # }
515    /// ```
516    ///
517    /// Alternatively, you can use [`to_socket_addrs`](https://doc.rust-lang.org/std/net/trait.ToSocketAddrs.html#tymethod.to_socket_addrs)
518    /// to resolve hostnames:
519    ///
520    /// ```no_run
521    /// # use mcumgr_toolkit::MCUmgrClient;
522    /// # use std::time::Duration;
523    /// # use std::net::ToSocketAddrs;
524    /// # fn main() {
525    /// let addr = "mydevice.local:1337".to_socket_addrs().unwrap().next().unwrap();
526    /// let mut client = MCUmgrClient::new_from_udp(addr, Duration::from_millis(1000)).unwrap();
527    /// # }
528    /// ```
529    pub fn new_from_udp(addr: impl Into<SocketAddr>, timeout: Duration) -> Result<Self, UdpError> {
530        let addr = addr.into();
531        log::debug!("Connecting to {addr} ...");
532        Ok(Self::new_from_transport(UdpTransport::new(addr, timeout)?))
533    }
534
535    /// Configures the maximum SMP frame size that we can send to the device.
536    ///
537    /// Must not exceed [`MCUMGR_TRANSPORT_NETBUF_SIZE`](https://github.com/zephyrproject-rtos/zephyr/blob/v4.2.1/subsys/mgmt/mcumgr/transport/Kconfig#L40),
538    /// otherwise we might crash the device.
539    pub fn set_frame_size(&self, smp_frame_size: usize) {
540        self.smp_frame_size
541            .store(smp_frame_size, std::sync::atomic::Ordering::SeqCst);
542    }
543
544    /// Configures the maximum SMP frame size that we can send to the device automatically
545    /// 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)
546    /// from the device.
547    pub fn use_auto_frame_size(&self) -> Result<(), MCUmgrClientError> {
548        let mcumgr_params = self
549            .connection
550            .execute_command(&commands::os::MCUmgrParameters)?;
551
552        let frame_size = (mcumgr_params.buf_size as usize)
553            .min(SMP_TRANSFER_BUFFER_SIZE)
554            .min(self.connection.max_transport_frame_size());
555
556        log::debug!("Using frame size {}.", frame_size);
557
558        self.smp_frame_size
559            .store(frame_size, std::sync::atomic::Ordering::SeqCst);
560
561        Ok(())
562    }
563
564    /// Changes the communication timeout.
565    ///
566    /// When the device does not respond to packets within the set
567    /// duration, an error will be raised.
568    pub fn set_timeout(&self, timeout: Duration) -> Result<(), MCUmgrClientError> {
569        self.connection
570            .set_timeout(timeout)
571            .map_err(MCUmgrClientError::SetTimeoutFailed)
572    }
573
574    /// Changes the retry amount.
575    ///
576    /// When the device encounters a transport error, it will retry
577    /// this many times until giving up.
578    pub fn set_retries(&self, retries: u8) {
579        self.connection.set_retries(retries)
580    }
581
582    /// Checks if the device is alive and responding.
583    ///
584    /// Runs a simple echo with random data and checks if the response matches.
585    ///
586    /// # Return
587    ///
588    /// An error if the device is not alive and responding.
589    pub fn check_connection(&self) -> Result<(), MCUmgrClientError> {
590        let random_message = rand::distr::Alphanumeric.sample_string(&mut rand::rng(), 16);
591        let response = self.os_echo(&random_message)?;
592        if random_message == response {
593            Ok(())
594        } else {
595            Err(
596                ExecuteError::ReceiveFailed(crate::transport::ReceiveError::UnexpectedResponse)
597                    .into(),
598            )
599        }
600    }
601
602    /// High-level firmware update routine.
603    ///
604    /// # Arguments
605    ///
606    /// * `firmware` - The firmware image data.
607    /// * `checksum` - SHA256 of the firmware image. Optional.
608    /// * `params` - Configurable parameters.
609    /// * `progress` - A callback that receives progress updates.
610    ///
611    pub fn firmware_update(
612        &self,
613        firmware: impl AsRef<[u8]>,
614        checksum: Option<[u8; 32]>,
615        params: FirmwareUpdateParams,
616        progress: Option<&mut FirmwareUpdateProgressCallback>,
617    ) -> Result<(), FirmwareUpdateError> {
618        firmware_update::firmware_update(self, firmware, checksum, params, progress)
619    }
620
621    /// Sends a message to the device and expects the same message back as response.
622    ///
623    /// This can be used as a sanity check for whether the device is connected and responsive.
624    pub fn os_echo(&self, msg: impl AsRef<str>) -> Result<String, MCUmgrClientError> {
625        self.connection
626            .execute_command(&commands::os::Echo { d: msg.as_ref() })
627            .map(|resp| resp.r)
628            .map_err(Into::into)
629    }
630
631    /// Queries live task statistics
632    ///
633    /// # Note
634    ///
635    /// Converts `stkuse` and `stksiz` to bytes.
636    /// Zephyr originally reports them as number of 4 byte words.
637    ///
638    /// # Return
639    ///
640    /// A map of task names with their respective statistics
641    pub fn os_task_statistics(
642        &self,
643    ) -> Result<HashMap<String, commands::os::TaskStatisticsEntry>, MCUmgrClientError> {
644        self.connection
645            .execute_command(&commands::os::TaskStatistics)
646            .map(|resp| {
647                let mut tasks = resp.tasks;
648                for stats in tasks.values_mut() {
649                    stats.stkuse = stats.stkuse.map(|val| val * 4);
650                    stats.stksiz = stats.stksiz.map(|val| val * 4);
651                }
652                tasks
653            })
654            .map_err(Into::into)
655    }
656
657    /// Queries live memory pool statistics
658    ///
659    /// # Return
660    ///
661    /// A map of memory pool names with their respective statistics
662    pub fn os_memory_pool_statistics(
663        &self,
664    ) -> Result<HashMap<String, commands::os::MemoryPoolStatisticsEntry>, MCUmgrClientError> {
665        self.connection
666            .execute_command(&commands::os::MemoryPoolStatistics)
667            .map(|resp| resp.pools)
668            .map_err(Into::into)
669    }
670
671    /// Sets the RTC of the device to the given datetime.
672    pub fn os_set_datetime(
673        &self,
674        datetime: chrono::NaiveDateTime,
675    ) -> Result<(), MCUmgrClientError> {
676        self.connection
677            .execute_command(&commands::os::DateTimeSet { datetime })
678            .map(Into::into)
679            .map_err(Into::into)
680    }
681
682    /// Retrieves the device RTC's datetime.
683    pub fn os_get_datetime(&self) -> Result<chrono::NaiveDateTime, MCUmgrClientError> {
684        self.connection
685            .execute_command(&commands::os::DateTimeGet)
686            .map(|val| val.datetime)
687            .map_err(Into::into)
688    }
689
690    /// Issues a system reset.
691    ///
692    /// # Arguments
693    ///
694    /// * `force` - Issues a force reset.
695    /// * `boot_mode` - Overwrites the boot mode.
696    ///
697    /// Known `boot_mode` values:
698    /// * `0` - Normal system boot
699    /// * `1` - Bootloader recovery mode
700    ///
701    /// 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.
702    ///
703    pub fn os_system_reset(
704        &self,
705        force: bool,
706        boot_mode: Option<u8>,
707    ) -> Result<(), MCUmgrClientError> {
708        self.connection
709            .execute_command(&commands::os::SystemReset { force, boot_mode })
710            .map(Into::into)
711            .map_err(Into::into)
712    }
713
714    /// Fetch parameters from the MCUmgr library
715    pub fn os_mcumgr_parameters(
716        &self,
717    ) -> Result<commands::os::MCUmgrParametersResponse, MCUmgrClientError> {
718        self.connection
719            .execute_command(&commands::os::MCUmgrParameters)
720            .map_err(Into::into)
721    }
722
723    /// Fetch information on the running image
724    ///
725    /// Similar to Linux's `uname` command.
726    ///
727    /// # Arguments
728    ///
729    /// * `format` - Format specifier for the returned response
730    ///
731    /// For more information about the format specifier fields, see
732    /// the [SMP documentation](https://docs.zephyrproject.org/latest/services/device_mgmt/smp_groups/smp_group_0.html#os-application-info-request).
733    ///
734    pub fn os_application_info(&self, format: Option<&str>) -> Result<String, MCUmgrClientError> {
735        self.connection
736            .execute_command(&commands::os::ApplicationInfo { format })
737            .map(|resp| resp.output)
738            .map_err(Into::into)
739    }
740
741    /// Fetch information on the device's bootloader
742    pub fn os_bootloader_info(&self) -> Result<BootloaderInfo, MCUmgrClientError> {
743        Ok(
744            match self
745                .connection
746                .execute_command(&commands::os::BootloaderInfo)?
747                .bootloader
748                .as_str()
749            {
750                "MCUboot" => {
751                    let mode_data = self
752                        .connection
753                        .execute_command(&commands::os::BootloaderInfoMcubootMode {})?;
754                    BootloaderInfo::MCUboot {
755                        mode: mode_data.mode,
756                        no_downgrade: mode_data.no_downgrade,
757                    }
758                }
759                name => BootloaderInfo::Unknown {
760                    name: name.to_string(),
761                },
762            },
763        )
764    }
765
766    /// Obtain a list of images with their current state.
767    pub fn image_get_state(&self) -> Result<Vec<commands::image::ImageState>, MCUmgrClientError> {
768        self.connection
769            .execute_command(&commands::image::GetImageState)
770            .map(|val| val.images)
771            .map_err(Into::into)
772    }
773
774    /// Modify the current image state
775    ///
776    /// # Arguments
777    ///
778    /// * `hash` - the hash id of the image. See [`mcuboot::get_image_info`](crate::mcuboot::get_image_info).
779    /// * `confirm` - mark the given image as 'confirmed'
780    ///
781    /// If `confirm` is `false`, perform a test boot with the given image and revert upon hard reset.
782    ///
783    /// If `confirm` is `true`, boot to the given image and mark it as `confirmed`. If `hash` is omitted,
784    /// confirm the currently running image.
785    ///
786    /// Note that `hash` will not be the same as the SHA256 of the whole firmware image,
787    /// it is the field in the MCUboot TLV section that contains a hash of the data
788    /// which is used for signature verification purposes.
789    pub fn image_set_state(
790        &self,
791        hash: Option<&[u8]>,
792        confirm: bool,
793    ) -> Result<Vec<commands::image::ImageState>, MCUmgrClientError> {
794        self.connection
795            .execute_command(&commands::image::SetImageState { hash, confirm })
796            .map(|val| val.images)
797            .map_err(Into::into)
798    }
799
800    /// Upload a firmware image to an image slot.
801    ///
802    /// # Note
803    ///
804    /// This only uploads the image to a slot on the device, it has to be activated
805    /// through [`image_set_state`](Self::image_set_state) for an actual update to happen.
806    ///
807    /// For a full firmware update algorithm in a single step, see [`firmware_update`](Self::firmware_update).
808    ///
809    /// # Arguments
810    ///
811    /// * `data` - The firmware image data
812    /// * `image` - Selects target image on the device. Defaults to `0`.
813    /// * `checksum` - The SHA256 checksum of the image. If missing, will be computed from the image data.
814    /// * `upgrade_only` - If true, allow firmware upgrades only and reject downgrades.
815    /// * `progress` - A callback that receives a pair of (transferred, total) bytes and returns false on error.
816    ///
817    pub fn image_upload(
818        &self,
819        data: impl AsRef<[u8]>,
820        image: Option<u32>,
821        checksum: Option<[u8; 32]>,
822        upgrade_only: bool,
823        mut progress: Option<&mut dyn FnMut(u64, u64) -> bool>,
824    ) -> Result<(), MCUmgrClientError> {
825        let first_chunk_size_max = image_upload_max_data_chunk_size(
826            self.smp_frame_size
827                .load(std::sync::atomic::Ordering::SeqCst),
828            true,
829        )
830        .map_err(MCUmgrClientError::FrameSizeTooSmall)?;
831        let other_chunk_size_max = image_upload_max_data_chunk_size(
832            self.smp_frame_size
833                .load(std::sync::atomic::Ordering::SeqCst),
834            false,
835        )
836        .map_err(MCUmgrClientError::FrameSizeTooSmall)?;
837        log::debug!("Max chunk size: {first_chunk_size_max}, {other_chunk_size_max}");
838
839        let data = data.as_ref();
840
841        let actual_checksum: [u8; 32] = Sha256::digest(data).into();
842        if let Some(checksum) = checksum {
843            if actual_checksum != checksum {
844                return Err(MCUmgrClientError::ChecksumMismatch);
845            }
846        }
847
848        let mut offset = 0;
849        let size = data.len();
850
851        let mut checksum_matched = None;
852
853        while offset < size {
854            let upload_response = if offset == 0 {
855                let current_chunk_size = (size - offset).min(first_chunk_size_max);
856                let chunk_data = &data[offset..offset + current_chunk_size];
857
858                let result = self
859                    .connection
860                    .execute_command(&commands::image::ImageUpload {
861                        image,
862                        len: Some(size as u64),
863                        off: offset as u64,
864                        sha: Some(&actual_checksum),
865                        data: chunk_data,
866                        upgrade: Some(upgrade_only),
867                    });
868
869                if let Err(ExecuteError::ReceiveFailed(ReceiveError::Timeout)) = &result {
870                    log::warn!(
871                        "Timed out during transfer of first chunk. Consider enabling CONFIG_IMG_ERASE_PROGRESSIVELY."
872                    )
873                }
874
875                result?
876            } else {
877                let current_chunk_size = (size - offset).min(other_chunk_size_max);
878                let chunk_data = &data[offset..offset + current_chunk_size];
879
880                self.connection
881                    .execute_command(&commands::image::ImageUpload {
882                        image: None,
883                        len: None,
884                        off: offset as u64,
885                        sha: None,
886                        data: chunk_data,
887                        upgrade: None,
888                    })?
889            };
890
891            offset = upload_response
892                .off
893                .try_into()
894                .map_err(|_| MCUmgrClientError::UnexpectedOffset)?;
895
896            if offset > size {
897                return Err(MCUmgrClientError::UnexpectedOffset);
898            }
899
900            if let Some(progress) = &mut progress {
901                if !progress(offset as u64, size as u64) {
902                    return Err(MCUmgrClientError::ProgressCallbackError);
903                };
904            }
905
906            if let Some(is_match) = upload_response.r#match {
907                checksum_matched = Some(is_match);
908            }
909        }
910
911        if let Some(checksum_matched) = checksum_matched {
912            if !checksum_matched {
913                return Err(MCUmgrClientError::ChecksumMismatchOnDevice);
914            }
915        } else {
916            log::warn!("Device did not perform image checksum verification");
917        }
918
919        Ok(())
920    }
921
922    /// Erase image slot on target device.
923    ///
924    /// # Arguments
925    ///
926    /// * `slot` - The slot ID of the image to erase. Slot `1` if omitted.
927    ///
928    pub fn image_erase(&self, slot: Option<u32>) -> Result<(), MCUmgrClientError> {
929        self.connection
930            .execute_command(&commands::image::ImageErase { slot })
931            .map(Into::into)
932            .map_err(Into::into)
933    }
934
935    /// Obtain a list of available image slots.
936    pub fn image_slot_info(
937        &self,
938    ) -> Result<Vec<commands::image::SlotInfoImage>, MCUmgrClientError> {
939        self.connection
940            .execute_command(&commands::image::SlotInfo)
941            .map(|val| val.images)
942            .map_err(Into::into)
943    }
944
945    /// Query the current values of a given stats group
946    ///
947    /// # Arguments
948    ///
949    /// * `name` - The name of the group. See [`stats_list_groups`](Self::stats_list_groups).
950    ///
951    pub fn stats_get_group_data(
952        &self,
953        name: impl AsRef<str>,
954    ) -> Result<HashMap<String, u64>, MCUmgrClientError> {
955        self.connection
956            .execute_command(&commands::stats::GroupData {
957                name: name.as_ref(),
958            })
959            .map(|val| val.fields)
960            .map_err(Into::into)
961    }
962
963    /// Query the list of available stats groups
964    pub fn stats_list_groups(&self) -> Result<Vec<String>, MCUmgrClientError> {
965        self.connection
966            .execute_command(&commands::stats::ListGroups)
967            .map(|val| val.stat_list)
968            .map_err(Into::into)
969    }
970
971    /// Read a setting from the device.
972    ///
973    /// # Arguments
974    ///
975    /// * `name` - The name of the setting.
976    ///
977    /// # Return
978    ///
979    /// The value of the setting, as raw bytes.
980    ///
981    /// Note that the underlying data type cannot be specified through this and must be known by the client.
982    ///
983    pub fn settings_read(&self, name: impl AsRef<str>) -> Result<Vec<u8>, MCUmgrClientError> {
984        let name = name.as_ref();
985
986        self.settings_read_ext(name, None).map(|val| val.val)
987    }
988
989    /// Read a setting from the device.
990    ///
991    /// Extended version.
992    ///
993    /// # Arguments
994    ///
995    /// * `name` - The name of the setting.
996    /// * `max_size` - Optional maximum size of data to return.
997    ///
998    pub fn settings_read_ext(
999        &self,
1000        name: impl AsRef<str>,
1001        max_size: Option<u32>,
1002    ) -> Result<commands::settings::ReadSettingResponse, MCUmgrClientError> {
1003        let name = name.as_ref();
1004
1005        self.connection
1006            .execute_command(&commands::settings::ReadSetting { name, max_size })
1007            .map_err(Into::into)
1008    }
1009
1010    /// Write a setting to the device.
1011    ///
1012    /// # Arguments
1013    ///
1014    /// * `name` - The name of the setting.
1015    /// * `value` - The value of the setting.
1016    ///
1017    pub fn settings_write(
1018        &self,
1019        name: impl AsRef<str>,
1020        value: &[u8],
1021    ) -> Result<(), MCUmgrClientError> {
1022        let name = name.as_ref();
1023
1024        self.connection
1025            .execute_command(&commands::settings::WriteSetting { name, val: value })
1026            .map(Into::into)
1027            .map_err(Into::into)
1028    }
1029
1030    /// Delete a setting from the device.
1031    ///
1032    /// # Arguments
1033    ///
1034    /// * `name` - The name of the setting.
1035    ///
1036    pub fn settings_delete(&self, name: impl AsRef<str>) -> Result<(), MCUmgrClientError> {
1037        let name = name.as_ref();
1038
1039        self.connection
1040            .execute_command(&commands::settings::DeleteSetting { name })
1041            .map(Into::into)
1042            .map_err(Into::into)
1043    }
1044
1045    /// Commit all modified settings on the device.
1046    ///
1047    pub fn settings_commit(&self) -> Result<(), MCUmgrClientError> {
1048        self.connection
1049            .execute_command(&commands::settings::CommitSettings)
1050            .map(Into::into)
1051            .map_err(Into::into)
1052    }
1053
1054    /// Load settings from persistent storage.
1055    ///
1056    pub fn settings_load(&self) -> Result<(), MCUmgrClientError> {
1057        self.connection
1058            .execute_command(&commands::settings::LoadSettings)
1059            .map(Into::into)
1060            .map_err(Into::into)
1061    }
1062
1063    /// Save settings to persistent storage.
1064    ///
1065    /// # Arguments
1066    ///
1067    /// * `name` - Only persist the subtree with the given name.
1068    ///
1069    pub fn settings_save(&self, name: Option<impl AsRef<str>>) -> Result<(), MCUmgrClientError> {
1070        let name = name.as_ref().map(|val| val.as_ref());
1071
1072        self.connection
1073            .execute_command(&commands::settings::SaveSettings { name })
1074            .map(Into::into)
1075            .map_err(Into::into)
1076    }
1077
1078    /// Load a file from the device.
1079    ///
1080    /// # Arguments
1081    ///
1082    /// * `name` - The full path of the file on the device.
1083    /// * `writer` - A [`Write`] object that the file content will be written to.
1084    /// * `progress` - A callback that receives a pair of (transferred, total) bytes.
1085    ///
1086    /// # Performance
1087    ///
1088    /// Downloading files with Zephyr's default parameters is slow.
1089    /// You want to increase [`MCUMGR_TRANSPORT_NETBUF_SIZE`](https://github.com/zephyrproject-rtos/zephyr/blob/v4.2.1/subsys/mgmt/mcumgr/transport/Kconfig#L40)
1090    /// to maybe `4096` or larger.
1091    pub fn fs_file_download<T: Write>(
1092        &self,
1093        name: impl AsRef<str>,
1094        mut writer: T,
1095        mut progress: Option<&mut dyn FnMut(u64, u64) -> bool>,
1096    ) -> Result<(), MCUmgrClientError> {
1097        let name = name.as_ref();
1098        let response = self
1099            .connection
1100            .execute_command(&commands::fs::FileDownload { name, off: 0 })?;
1101
1102        let file_len = response.len.ok_or(MCUmgrClientError::MissingSize)?;
1103        if response.off != 0 {
1104            return Err(MCUmgrClientError::UnexpectedOffset);
1105        }
1106
1107        let mut offset = 0;
1108
1109        if let Some(progress) = &mut progress {
1110            if !progress(offset, file_len) {
1111                return Err(MCUmgrClientError::ProgressCallbackError);
1112            };
1113        }
1114
1115        writer
1116            .write_all(&response.data)
1117            .map_err(MCUmgrClientError::WriterError)?;
1118        offset += response.data.len() as u64;
1119
1120        if let Some(progress) = &mut progress {
1121            if !progress(offset, file_len) {
1122                return Err(MCUmgrClientError::ProgressCallbackError);
1123            };
1124        }
1125
1126        while offset < file_len {
1127            let response = self
1128                .connection
1129                .execute_command(&commands::fs::FileDownload { name, off: offset })?;
1130
1131            if response.off != offset {
1132                return Err(MCUmgrClientError::UnexpectedOffset);
1133            }
1134
1135            writer
1136                .write_all(&response.data)
1137                .map_err(MCUmgrClientError::WriterError)?;
1138            offset += response.data.len() as u64;
1139
1140            if let Some(progress) = &mut progress {
1141                if !progress(offset, file_len) {
1142                    return Err(MCUmgrClientError::ProgressCallbackError);
1143                };
1144            }
1145        }
1146
1147        if offset != file_len {
1148            return Err(MCUmgrClientError::SizeMismatch);
1149        }
1150
1151        Ok(())
1152    }
1153
1154    /// Write a file to the device.
1155    ///
1156    /// # Arguments
1157    ///
1158    /// * `name` - The full path of the file on the device.
1159    /// * `reader` - A [`Read`] object that contains the file content.
1160    /// * `size` - The file size.
1161    /// * `progress` - A callback that receives a pair of (transferred, total) bytes and returns false on error.
1162    ///
1163    /// # Performance
1164    ///
1165    /// Uploading files with Zephyr's default parameters is slow.
1166    /// You want to increase [`MCUMGR_TRANSPORT_NETBUF_SIZE`](https://github.com/zephyrproject-rtos/zephyr/blob/v4.2.1/subsys/mgmt/mcumgr/transport/Kconfig#L40)
1167    /// to maybe `4096` and then enable larger chunking through either [`MCUmgrClient::set_frame_size`]
1168    /// or [`MCUmgrClient::use_auto_frame_size`].
1169    pub fn fs_file_upload<T: Read>(
1170        &self,
1171        name: impl AsRef<str>,
1172        mut reader: T,
1173        size: u64,
1174        mut progress: Option<&mut dyn FnMut(u64, u64) -> bool>,
1175    ) -> Result<(), MCUmgrClientError> {
1176        let name = name.as_ref();
1177
1178        let chunk_size_max = file_upload_max_data_chunk_size(
1179            self.smp_frame_size
1180                .load(std::sync::atomic::Ordering::SeqCst),
1181            name,
1182        )
1183        .map_err(MCUmgrClientError::FrameSizeTooSmall)?;
1184        let mut data_buffer = vec![0u8; chunk_size_max].into_boxed_slice();
1185
1186        let mut offset = 0;
1187
1188        while offset < size {
1189            let current_chunk_size = (size - offset).min(data_buffer.len() as u64) as usize;
1190
1191            let chunk_buffer = &mut data_buffer[..current_chunk_size];
1192            reader
1193                .read_exact(chunk_buffer)
1194                .map_err(MCUmgrClientError::ReaderError)?;
1195
1196            self.connection.execute_command(&commands::fs::FileUpload {
1197                off: offset,
1198                data: chunk_buffer,
1199                name,
1200                len: if offset == 0 { Some(size) } else { None },
1201            })?;
1202
1203            offset += chunk_buffer.len() as u64;
1204
1205            if let Some(progress) = &mut progress {
1206                if !progress(offset, size) {
1207                    return Err(MCUmgrClientError::ProgressCallbackError);
1208                };
1209            }
1210        }
1211
1212        Ok(())
1213    }
1214
1215    /// Queries the file status
1216    pub fn fs_file_status(
1217        &self,
1218        name: impl AsRef<str>,
1219    ) -> Result<commands::fs::FileStatusResponse, MCUmgrClientError> {
1220        self.connection
1221            .execute_command(&commands::fs::FileStatus {
1222                name: name.as_ref(),
1223            })
1224            .map_err(Into::into)
1225    }
1226
1227    /// Computes the hash/checksum of a file
1228    ///
1229    /// For available algorithms, see [`fs_supported_checksum_types()`](MCUmgrClient::fs_supported_checksum_types).
1230    ///
1231    /// # Arguments
1232    ///
1233    /// * `name` - The absolute path of the file on the device
1234    /// * `algorithm` - The hash/checksum algorithm to use, or default if None
1235    /// * `offset` - How many bytes of the file to skip
1236    /// * `length` - How many bytes to read after `offset`. None for the entire file.
1237    ///
1238    pub fn fs_file_checksum(
1239        &self,
1240        name: impl AsRef<str>,
1241        algorithm: Option<impl AsRef<str>>,
1242        offset: u64,
1243        length: Option<u64>,
1244    ) -> Result<commands::fs::FileChecksumResponse, MCUmgrClientError> {
1245        self.connection
1246            .execute_command(&commands::fs::FileChecksum {
1247                name: name.as_ref(),
1248                r#type: algorithm.as_ref().map(AsRef::as_ref),
1249                off: offset,
1250                len: length,
1251            })
1252            .map_err(Into::into)
1253    }
1254
1255    /// Queries which hash/checksum algorithms are available on the target
1256    pub fn fs_supported_checksum_types(
1257        &self,
1258    ) -> Result<HashMap<String, commands::fs::FileChecksumProperties>, MCUmgrClientError> {
1259        self.connection
1260            .execute_command(&commands::fs::SupportedFileChecksumTypes)
1261            .map(|val| val.types)
1262            .map_err(Into::into)
1263    }
1264
1265    /// Close all device files MCUmgr has currently open
1266    pub fn fs_file_close(&self) -> Result<(), MCUmgrClientError> {
1267        self.connection
1268            .execute_command(&commands::fs::FileClose)
1269            .map(Into::into)
1270            .map_err(Into::into)
1271    }
1272
1273    /// Run a shell command.
1274    ///
1275    /// # Arguments
1276    ///
1277    /// * `argv` - The shell command to be executed.
1278    /// * `use_retries` - Retry request a certain amount of times if a transport error occurs.
1279    ///   Be aware that this might cause the command to be executed multiple times.
1280    ///
1281    /// # Return
1282    ///
1283    /// A tuple of (returncode, stdout) produced by the command execution.
1284    pub fn shell_execute(
1285        &self,
1286        argv: &[String],
1287        use_retries: bool,
1288    ) -> Result<(i32, String), MCUmgrClientError> {
1289        let command = commands::shell::ShellCommandLineExecute { argv };
1290
1291        if use_retries {
1292            self.connection.execute_command(&command)
1293        } else {
1294            self.connection.execute_command_without_retries(&command)
1295        }
1296        .map(|ret| (ret.ret, ret.o))
1297        .map_err(Into::into)
1298    }
1299
1300    /// Query how many MCUmgr groups are supported by the device.
1301    ///
1302    /// # Return
1303    ///
1304    /// The number of MCUmgr groups the device supports.
1305    ///
1306    pub fn enum_get_group_count(&self) -> Result<u16, MCUmgrClientError> {
1307        self.connection
1308            .execute_command(&commands::r#enum::GroupCount)
1309            .map(|ret| ret.count)
1310            .map_err(Into::into)
1311    }
1312
1313    /// Query all available group IDs in a single command.
1314    ///
1315    /// Note that this might fail if the amount of groups is too large for the
1316    /// SMP frame.
1317    /// But given that the Zephyr implementation contains less than 10 groups,
1318    /// this is currently highly unlikely.
1319    ///
1320    /// If it does fail, use [`enum_iter_group_ids`](Self::enum_iter_group_ids) to iterate
1321    /// through the available group IDs one by one.
1322    ///
1323    /// # Return
1324    ///
1325    /// A list of all MCUmgr group IDs the device supports.
1326    ///
1327    pub fn enum_get_group_ids(&self) -> Result<Vec<u16>, MCUmgrClientError> {
1328        self.connection
1329            .execute_command(&commands::r#enum::ListGroups)
1330            .map(|ret| ret.groups)
1331            .map_err(Into::into)
1332    }
1333
1334    /// Query a single group ID from the device.
1335    ///
1336    /// # Arguments
1337    ///
1338    /// * `index` - The index in the list of group IDs.
1339    ///   Must be smaller than [`enum_get_group_count`](Self::enum_get_group_count).
1340    ///
1341    /// # Return
1342    ///
1343    /// The group ID of the group with the given index
1344    ///
1345    pub fn enum_get_group_id(&self, index: u16) -> Result<u16, MCUmgrClientError> {
1346        self.connection
1347            .execute_command(&commands::r#enum::GroupId { index: Some(index) })
1348            .map(|ret| ret.group)
1349            .map_err(Into::into)
1350    }
1351
1352    /// Iterate through all supported MCUmgr Groups.
1353    ///
1354    /// Same as [`enum_get_group_ids`](Self::enum_get_group_ids), but does not
1355    /// require large message sizes if the number of groups is large. The tradeoff is
1356    /// that this function is much slower.
1357    pub fn enum_iter_group_ids(&self) -> impl Iterator<Item = Result<u16, MCUmgrClientError>> {
1358        let mut i = 0;
1359        let mut num_elements = None;
1360
1361        std::iter::from_fn(move || -> Option<Result<u16, MCUmgrClientError>> {
1362            let mut num_elements_err = None;
1363            let num_elements =
1364                *num_elements.get_or_insert_with(|| match self.enum_get_group_count() {
1365                    Ok(n) => n,
1366                    Err(e) => {
1367                        num_elements_err = Some(e);
1368                        0
1369                    }
1370                });
1371            if let Some(err) = num_elements_err {
1372                return Some(Err(err));
1373            }
1374
1375            if i >= num_elements {
1376                None
1377            } else {
1378                Some(match self.enum_get_group_id(i) {
1379                    Ok(group_id) => {
1380                        i += 1;
1381                        Ok(group_id)
1382                    }
1383                    Err(e) => {
1384                        i = num_elements;
1385                        Err(e)
1386                    }
1387                })
1388            }
1389        })
1390    }
1391
1392    /// Query details from all available groups.
1393    ///
1394    /// # Arguments
1395    ///
1396    /// * `groups` - The group IDs to fetch details for. If omitted, fetch all groups.
1397    ///
1398    /// # Return
1399    ///
1400    /// A list of details about all MCUmgr group IDs the device supports.
1401    ///
1402    pub fn enum_get_group_details(
1403        &self,
1404        groups: Option<&[u16]>,
1405    ) -> Result<Vec<commands::r#enum::GroupDetailsEntry>, MCUmgrClientError> {
1406        self.connection
1407            .execute_command(&commands::r#enum::GroupDetails { groups })
1408            .map(|ret| ret.groups)
1409            .map_err(Into::into)
1410    }
1411
1412    /// Erase the `storage_partition` flash partition.
1413    pub fn zephyr_erase_storage(&self) -> Result<(), MCUmgrClientError> {
1414        self.connection
1415            .execute_command(&commands::zephyr::EraseStorage)
1416            .map(Into::into)
1417            .map_err(Into::into)
1418    }
1419
1420    /// Execute a raw [`commands::McuMgrCommand`].
1421    ///
1422    /// Only returns if no error happened, so the
1423    /// user does not need to check for an `rc` or `err`
1424    /// field in the response.
1425    pub fn raw_command<T: commands::McuMgrCommand>(
1426        &self,
1427        command: &T,
1428    ) -> Result<T::Response, MCUmgrClientError> {
1429        self.connection.execute_command(command).map_err(Into::into)
1430    }
1431}