Skip to main content

mcumgr_toolkit/
client.rs

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