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    /// Returns the configured frame size, reduced by the space the
567    /// transport additionally occupies in the device's receive buffer.
568    fn usable_frame_size(&self) -> usize {
569        self.smp_frame_size
570            .load(std::sync::atomic::Ordering::SeqCst)
571            .saturating_sub(self.connection.device_rx_buffer_overhead())
572    }
573
574    /// Changes the communication timeout.
575    ///
576    /// When the device does not respond to packets within the set
577    /// duration, an error will be raised.
578    pub fn set_timeout(&self, timeout: Duration) -> Result<(), MCUmgrClientError> {
579        self.connection
580            .set_timeout(timeout)
581            .map_err(MCUmgrClientError::SetTimeoutFailed)
582    }
583
584    /// Changes the retry amount.
585    ///
586    /// When the device encounters a transport error, it will retry
587    /// this many times until giving up.
588    pub fn set_retries(&self, retries: u8) {
589        self.connection.set_retries(retries)
590    }
591
592    /// Checks if the device is alive and responding.
593    ///
594    /// Runs a simple echo with random data and checks if the response matches.
595    ///
596    /// # Return
597    ///
598    /// An error if the device is not alive and responding.
599    pub fn check_connection(&self) -> Result<(), MCUmgrClientError> {
600        let random_message = rand::distr::Alphanumeric.sample_string(&mut rand::rng(), 16);
601        let response = self.os_echo(&random_message)?;
602        if random_message == response {
603            Ok(())
604        } else {
605            Err(
606                ExecuteError::ReceiveFailed(crate::transport::ReceiveError::UnexpectedResponse)
607                    .into(),
608            )
609        }
610    }
611
612    /// High-level firmware update routine.
613    ///
614    /// # Arguments
615    ///
616    /// * `firmware` - The firmware image data.
617    /// * `checksum` - SHA256 of the firmware image. Optional.
618    /// * `params` - Configurable parameters.
619    /// * `progress` - A callback that receives progress updates.
620    ///
621    pub fn firmware_update(
622        &self,
623        firmware: impl AsRef<[u8]>,
624        checksum: Option<[u8; 32]>,
625        params: FirmwareUpdateParams,
626        progress: Option<&mut FirmwareUpdateProgressCallback>,
627    ) -> Result<(), FirmwareUpdateError> {
628        firmware_update::firmware_update(self, firmware, checksum, params, progress)
629    }
630
631    /// Sends a message to the device and expects the same message back as response.
632    ///
633    /// This can be used as a sanity check for whether the device is connected and responsive.
634    pub fn os_echo(&self, msg: impl AsRef<str>) -> Result<String, MCUmgrClientError> {
635        self.connection
636            .execute_command(&commands::os::Echo { d: msg.as_ref() })
637            .map(|resp| resp.r)
638            .map_err(Into::into)
639    }
640
641    /// Queries live task statistics
642    ///
643    /// # Note
644    ///
645    /// Converts `stkuse` and `stksiz` to bytes.
646    /// Zephyr originally reports them as number of 4 byte words.
647    ///
648    /// # Return
649    ///
650    /// A map of task names with their respective statistics
651    pub fn os_task_statistics(
652        &self,
653    ) -> Result<HashMap<String, commands::os::TaskStatisticsEntry>, MCUmgrClientError> {
654        self.connection
655            .execute_command(&commands::os::TaskStatistics)
656            .map(|resp| {
657                let mut tasks = resp.tasks;
658                for stats in tasks.values_mut() {
659                    stats.stkuse = stats.stkuse.map(|val| val * 4);
660                    stats.stksiz = stats.stksiz.map(|val| val * 4);
661                }
662                tasks
663            })
664            .map_err(Into::into)
665    }
666
667    /// Queries live memory pool statistics
668    ///
669    /// # Return
670    ///
671    /// A map of memory pool names with their respective statistics
672    pub fn os_memory_pool_statistics(
673        &self,
674    ) -> Result<HashMap<String, commands::os::MemoryPoolStatisticsEntry>, MCUmgrClientError> {
675        self.connection
676            .execute_command(&commands::os::MemoryPoolStatistics)
677            .map(|resp| resp.pools)
678            .map_err(Into::into)
679    }
680
681    /// Sets the RTC of the device to the given datetime.
682    pub fn os_set_datetime(
683        &self,
684        datetime: chrono::NaiveDateTime,
685    ) -> Result<(), MCUmgrClientError> {
686        self.connection
687            .execute_command(&commands::os::DateTimeSet { datetime })
688            .map(Into::into)
689            .map_err(Into::into)
690    }
691
692    /// Retrieves the device RTC's datetime.
693    pub fn os_get_datetime(&self) -> Result<chrono::NaiveDateTime, MCUmgrClientError> {
694        self.connection
695            .execute_command(&commands::os::DateTimeGet)
696            .map(|val| val.datetime)
697            .map_err(Into::into)
698    }
699
700    /// Issues a system reset.
701    ///
702    /// # Arguments
703    ///
704    /// * `force` - Issues a force reset.
705    /// * `boot_mode` - Overwrites the boot mode.
706    ///
707    /// Known `boot_mode` values:
708    /// * `0` - Normal system boot
709    /// * `1` - Bootloader recovery mode
710    ///
711    /// 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.
712    ///
713    pub fn os_system_reset(
714        &self,
715        force: bool,
716        boot_mode: Option<u8>,
717    ) -> Result<(), MCUmgrClientError> {
718        self.connection
719            .execute_command(&commands::os::SystemReset { force, boot_mode })
720            .map(Into::into)
721            .map_err(Into::into)
722    }
723
724    /// Fetch parameters from the MCUmgr library
725    pub fn os_mcumgr_parameters(
726        &self,
727    ) -> Result<commands::os::MCUmgrParametersResponse, MCUmgrClientError> {
728        self.connection
729            .execute_command(&commands::os::MCUmgrParameters)
730            .map_err(Into::into)
731    }
732
733    /// Fetch information on the running image
734    ///
735    /// Similar to Linux's `uname` command.
736    ///
737    /// # Arguments
738    ///
739    /// * `format` - Format specifier for the returned response
740    ///
741    /// For more information about the format specifier fields, see
742    /// the [SMP documentation](https://docs.zephyrproject.org/latest/services/device_mgmt/smp_groups/smp_group_0.html#os-application-info-request).
743    ///
744    pub fn os_application_info(&self, format: Option<&str>) -> Result<String, MCUmgrClientError> {
745        self.connection
746            .execute_command(&commands::os::ApplicationInfo { format })
747            .map(|resp| resp.output)
748            .map_err(Into::into)
749    }
750
751    /// Fetch information on the device's bootloader
752    pub fn os_bootloader_info(&self) -> Result<BootloaderInfo, MCUmgrClientError> {
753        Ok(
754            match self
755                .connection
756                .execute_command(&commands::os::BootloaderInfo)?
757                .bootloader
758                .as_str()
759            {
760                "MCUboot" => {
761                    let mode_data = self
762                        .connection
763                        .execute_command(&commands::os::BootloaderInfoMcubootMode {})?;
764                    BootloaderInfo::MCUboot {
765                        mode: mode_data.mode,
766                        no_downgrade: mode_data.no_downgrade,
767                    }
768                }
769                name => BootloaderInfo::Unknown {
770                    name: name.to_string(),
771                },
772            },
773        )
774    }
775
776    /// Obtain a list of images with their current state.
777    pub fn image_get_state(&self) -> Result<Vec<commands::image::ImageState>, MCUmgrClientError> {
778        self.connection
779            .execute_command(&commands::image::GetImageState)
780            .map(|val| val.images)
781            .map_err(Into::into)
782    }
783
784    /// Modify the current image state
785    ///
786    /// # Arguments
787    ///
788    /// * `hash` - the hash id of the image. See [`mcuboot::get_image_info`](crate::mcuboot::get_image_info).
789    /// * `confirm` - mark the given image as 'confirmed'
790    ///
791    /// If `confirm` is `false`, perform a test boot with the given image and revert upon hard reset.
792    ///
793    /// If `confirm` is `true`, boot to the given image and mark it as `confirmed`. If `hash` is omitted,
794    /// confirm the currently running image.
795    ///
796    /// Note that `hash` will not be the same as the SHA256 of the whole firmware image,
797    /// it is the field in the MCUboot TLV section that contains a hash of the data
798    /// which is used for signature verification purposes.
799    pub fn image_set_state(
800        &self,
801        hash: Option<&[u8]>,
802        confirm: bool,
803    ) -> Result<Vec<commands::image::ImageState>, MCUmgrClientError> {
804        self.connection
805            .execute_command(&commands::image::SetImageState { hash, confirm })
806            .map(|val| val.images)
807            .map_err(Into::into)
808    }
809
810    /// Upload a firmware image to an image slot.
811    ///
812    /// # Note
813    ///
814    /// This only uploads the image to a slot on the device, it has to be activated
815    /// through [`image_set_state`](Self::image_set_state) for an actual update to happen.
816    ///
817    /// For a full firmware update algorithm in a single step, see [`firmware_update`](Self::firmware_update).
818    ///
819    /// # Arguments
820    ///
821    /// * `data` - The firmware image data
822    /// * `image` - Selects target image on the device. Defaults to `0`.
823    /// * `checksum` - The SHA256 checksum of the image. If missing, will be computed from the image data.
824    /// * `upgrade_only` - If true, allow firmware upgrades only and reject downgrades.
825    /// * `progress` - A callback that receives a pair of (transferred, total) bytes and returns false on error.
826    ///
827    pub fn image_upload(
828        &self,
829        data: impl AsRef<[u8]>,
830        image: Option<u32>,
831        checksum: Option<[u8; 32]>,
832        upgrade_only: bool,
833        mut progress: Option<&mut dyn FnMut(u64, u64) -> bool>,
834    ) -> Result<(), MCUmgrClientError> {
835        let first_chunk_size_max = image_upload_max_data_chunk_size(self.usable_frame_size(), true)
836            .map_err(MCUmgrClientError::FrameSizeTooSmall)?;
837        let other_chunk_size_max =
838            image_upload_max_data_chunk_size(self.usable_frame_size(), false)
839                .map_err(MCUmgrClientError::FrameSizeTooSmall)?;
840        log::debug!("Max chunk size: {first_chunk_size_max}, {other_chunk_size_max}");
841
842        let data = data.as_ref();
843
844        let actual_checksum: [u8; 32] = Sha256::digest(data).into();
845        if let Some(checksum) = checksum {
846            if actual_checksum != checksum {
847                return Err(MCUmgrClientError::ChecksumMismatch);
848            }
849        }
850
851        let mut offset = 0;
852        let size = data.len();
853
854        let mut checksum_matched = None;
855
856        loop {
857            let upload_response = if offset == 0 {
858                let current_chunk_size = (size - offset).min(first_chunk_size_max);
859                let chunk_data = &data[offset..offset + current_chunk_size];
860
861                let result = self
862                    .connection
863                    .execute_command(&commands::image::ImageUpload {
864                        image,
865                        len: Some(size as u64),
866                        off: offset as u64,
867                        sha: Some(&actual_checksum),
868                        data: chunk_data,
869                        upgrade: Some(upgrade_only),
870                    });
871
872                if let Err(ExecuteError::ReceiveFailed(ReceiveError::Timeout)) = &result {
873                    log::warn!(
874                        "Timed out during transfer of first chunk. Consider enabling CONFIG_IMG_ERASE_PROGRESSIVELY."
875                    )
876                }
877
878                result?
879            } else {
880                let current_chunk_size = (size - offset).min(other_chunk_size_max);
881                let chunk_data = &data[offset..offset + current_chunk_size];
882
883                self.connection
884                    .execute_command(&commands::image::ImageUpload {
885                        image: None,
886                        len: None,
887                        off: offset as u64,
888                        sha: None,
889                        data: chunk_data,
890                        upgrade: None,
891                    })?
892            };
893
894            offset = upload_response
895                .off
896                .try_into()
897                .map_err(|_| MCUmgrClientError::UnexpectedOffset)?;
898
899            if offset > size {
900                return Err(MCUmgrClientError::UnexpectedOffset);
901            }
902
903            if let Some(progress) = &mut progress {
904                if !progress(offset as u64, size as u64) {
905                    return Err(MCUmgrClientError::ProgressCallbackError);
906                };
907            }
908
909            if let Some(is_match) = upload_response.r#match {
910                checksum_matched = Some(is_match);
911            }
912
913            if offset >= size {
914                break;
915            }
916        }
917
918        if let Some(checksum_matched) = checksum_matched {
919            if !checksum_matched {
920                return Err(MCUmgrClientError::ChecksumMismatchOnDevice);
921            }
922        } else {
923            log::warn!("Device did not perform image checksum verification");
924        }
925
926        Ok(())
927    }
928
929    /// Erase image slot on target device.
930    ///
931    /// # Arguments
932    ///
933    /// * `slot` - The slot ID of the image to erase. Slot `1` if omitted.
934    ///
935    pub fn image_erase(&self, slot: Option<u32>) -> Result<(), MCUmgrClientError> {
936        self.connection
937            .execute_command(&commands::image::ImageErase { slot })
938            .map(Into::into)
939            .map_err(Into::into)
940    }
941
942    /// Obtain a list of available image slots.
943    pub fn image_slot_info(
944        &self,
945    ) -> Result<Vec<commands::image::SlotInfoImage>, MCUmgrClientError> {
946        self.connection
947            .execute_command(&commands::image::SlotInfo)
948            .map(|val| val.images)
949            .map_err(Into::into)
950    }
951
952    /// Query the current values of a given stats group
953    ///
954    /// # Arguments
955    ///
956    /// * `name` - The name of the group. See [`stats_list_groups`](Self::stats_list_groups).
957    ///
958    pub fn stats_get_group_data(
959        &self,
960        name: impl AsRef<str>,
961    ) -> Result<HashMap<String, u64>, MCUmgrClientError> {
962        self.connection
963            .execute_command(&commands::stats::GroupData {
964                name: name.as_ref(),
965            })
966            .map(|val| val.fields)
967            .map_err(Into::into)
968    }
969
970    /// Query the list of available stats groups
971    pub fn stats_list_groups(&self) -> Result<Vec<String>, MCUmgrClientError> {
972        self.connection
973            .execute_command(&commands::stats::ListGroups)
974            .map(|val| val.stat_list)
975            .map_err(Into::into)
976    }
977
978    /// Read a setting from the device.
979    ///
980    /// # Arguments
981    ///
982    /// * `name` - The name of the setting.
983    ///
984    /// # Return
985    ///
986    /// The value of the setting, as raw bytes.
987    ///
988    /// Note that the underlying data type cannot be specified through this and must be known by the client.
989    ///
990    pub fn settings_read(&self, name: impl AsRef<str>) -> Result<Vec<u8>, MCUmgrClientError> {
991        let name = name.as_ref();
992
993        self.settings_read_ext(name, None).map(|val| val.val)
994    }
995
996    /// Read a setting from the device.
997    ///
998    /// Extended version.
999    ///
1000    /// # Arguments
1001    ///
1002    /// * `name` - The name of the setting.
1003    /// * `max_size` - Optional maximum size of data to return.
1004    ///
1005    pub fn settings_read_ext(
1006        &self,
1007        name: impl AsRef<str>,
1008        max_size: Option<u32>,
1009    ) -> Result<commands::settings::ReadSettingResponse, MCUmgrClientError> {
1010        let name = name.as_ref();
1011
1012        self.connection
1013            .execute_command(&commands::settings::ReadSetting { name, max_size })
1014            .map_err(Into::into)
1015    }
1016
1017    /// Write a setting to the device.
1018    ///
1019    /// # Arguments
1020    ///
1021    /// * `name` - The name of the setting.
1022    /// * `value` - The value of the setting.
1023    ///
1024    pub fn settings_write(
1025        &self,
1026        name: impl AsRef<str>,
1027        value: &[u8],
1028    ) -> Result<(), MCUmgrClientError> {
1029        let name = name.as_ref();
1030
1031        self.connection
1032            .execute_command(&commands::settings::WriteSetting { name, val: value })
1033            .map(Into::into)
1034            .map_err(Into::into)
1035    }
1036
1037    /// Delete a setting from the device.
1038    ///
1039    /// # Arguments
1040    ///
1041    /// * `name` - The name of the setting.
1042    ///
1043    pub fn settings_delete(&self, name: impl AsRef<str>) -> Result<(), MCUmgrClientError> {
1044        let name = name.as_ref();
1045
1046        self.connection
1047            .execute_command(&commands::settings::DeleteSetting { name })
1048            .map(Into::into)
1049            .map_err(Into::into)
1050    }
1051
1052    /// Commit all modified settings on the device.
1053    ///
1054    pub fn settings_commit(&self) -> Result<(), MCUmgrClientError> {
1055        self.connection
1056            .execute_command(&commands::settings::CommitSettings)
1057            .map(Into::into)
1058            .map_err(Into::into)
1059    }
1060
1061    /// Load settings from persistent storage.
1062    ///
1063    pub fn settings_load(&self) -> Result<(), MCUmgrClientError> {
1064        self.connection
1065            .execute_command(&commands::settings::LoadSettings)
1066            .map(Into::into)
1067            .map_err(Into::into)
1068    }
1069
1070    /// Save settings to persistent storage.
1071    ///
1072    /// # Arguments
1073    ///
1074    /// * `name` - Only persist the subtree with the given name.
1075    ///
1076    pub fn settings_save(&self, name: Option<impl AsRef<str>>) -> Result<(), MCUmgrClientError> {
1077        let name = name.as_ref().map(|val| val.as_ref());
1078
1079        self.connection
1080            .execute_command(&commands::settings::SaveSettings { name })
1081            .map(Into::into)
1082            .map_err(Into::into)
1083    }
1084
1085    /// Load a file from the device.
1086    ///
1087    /// # Arguments
1088    ///
1089    /// * `name` - The full path of the file on the device.
1090    /// * `writer` - A [`Write`] object that the file content will be written to.
1091    /// * `progress` - A callback that receives a pair of (transferred, total) bytes.
1092    ///
1093    /// # Performance
1094    ///
1095    /// Downloading files with Zephyr's default parameters is slow.
1096    /// You want to increase [`MCUMGR_TRANSPORT_NETBUF_SIZE`](https://github.com/zephyrproject-rtos/zephyr/blob/v4.2.1/subsys/mgmt/mcumgr/transport/Kconfig#L40)
1097    /// to maybe `4096` or larger.
1098    pub fn fs_file_download<T: Write>(
1099        &self,
1100        name: impl AsRef<str>,
1101        mut writer: T,
1102        mut progress: Option<&mut dyn FnMut(u64, u64) -> bool>,
1103    ) -> Result<(), MCUmgrClientError> {
1104        let name = name.as_ref();
1105        let response = self
1106            .connection
1107            .execute_command(&commands::fs::FileDownload { name, off: 0 })?;
1108
1109        let file_len = response.len.ok_or(MCUmgrClientError::MissingSize)?;
1110        if response.off != 0 {
1111            return Err(MCUmgrClientError::UnexpectedOffset);
1112        }
1113
1114        let mut offset = 0;
1115
1116        if let Some(progress) = &mut progress {
1117            if !progress(offset, file_len) {
1118                return Err(MCUmgrClientError::ProgressCallbackError);
1119            };
1120        }
1121
1122        writer
1123            .write_all(&response.data)
1124            .map_err(MCUmgrClientError::WriterError)?;
1125        offset += response.data.len() as u64;
1126
1127        if let Some(progress) = &mut progress {
1128            if !progress(offset, file_len) {
1129                return Err(MCUmgrClientError::ProgressCallbackError);
1130            };
1131        }
1132
1133        while offset < file_len {
1134            let response = self
1135                .connection
1136                .execute_command(&commands::fs::FileDownload { name, off: offset })?;
1137
1138            if response.off != offset {
1139                return Err(MCUmgrClientError::UnexpectedOffset);
1140            }
1141
1142            if response.data.is_empty() {
1143                return Err(MCUmgrClientError::SizeMismatch);
1144            }
1145
1146            writer
1147                .write_all(&response.data)
1148                .map_err(MCUmgrClientError::WriterError)?;
1149            offset += response.data.len() as u64;
1150
1151            if let Some(progress) = &mut progress {
1152                if !progress(offset, file_len) {
1153                    return Err(MCUmgrClientError::ProgressCallbackError);
1154                };
1155            }
1156        }
1157
1158        if offset != file_len {
1159            return Err(MCUmgrClientError::SizeMismatch);
1160        }
1161
1162        Ok(())
1163    }
1164
1165    /// Write a file to the device.
1166    ///
1167    /// # Arguments
1168    ///
1169    /// * `name` - The full path of the file on the device.
1170    /// * `reader` - A [`Read`] object that contains the file content.
1171    /// * `size` - The file size.
1172    /// * `progress` - A callback that receives a pair of (transferred, total) bytes and returns false on error.
1173    ///
1174    /// # Performance
1175    ///
1176    /// Uploading files with Zephyr's default parameters is slow.
1177    /// You want to increase [`MCUMGR_TRANSPORT_NETBUF_SIZE`](https://github.com/zephyrproject-rtos/zephyr/blob/v4.2.1/subsys/mgmt/mcumgr/transport/Kconfig#L40)
1178    /// to maybe `4096` and then enable larger chunking through either [`MCUmgrClient::set_frame_size`]
1179    /// or [`MCUmgrClient::use_auto_frame_size`].
1180    pub fn fs_file_upload<T: Read>(
1181        &self,
1182        name: impl AsRef<str>,
1183        mut reader: T,
1184        size: u64,
1185        mut progress: Option<&mut dyn FnMut(u64, u64) -> bool>,
1186    ) -> Result<(), MCUmgrClientError> {
1187        let name = name.as_ref();
1188
1189        let chunk_size_max = file_upload_max_data_chunk_size(self.usable_frame_size(), name)
1190            .map_err(MCUmgrClientError::FrameSizeTooSmall)?;
1191        let mut data_buffer = vec![0u8; chunk_size_max].into_boxed_slice();
1192
1193        let mut offset = 0;
1194
1195        loop {
1196            let current_chunk_size = (size - offset).min(data_buffer.len() as u64) as usize;
1197
1198            let chunk_buffer = &mut data_buffer[..current_chunk_size];
1199            reader
1200                .read_exact(chunk_buffer)
1201                .map_err(MCUmgrClientError::ReaderError)?;
1202
1203            let upload_response = self.connection.execute_command(&commands::fs::FileUpload {
1204                off: offset,
1205                data: chunk_buffer,
1206                name,
1207                len: if offset == 0 { Some(size) } else { None },
1208            })?;
1209
1210            offset += chunk_buffer.len() as u64;
1211
1212            if offset != upload_response.off {
1213                return Err(MCUmgrClientError::UnexpectedOffset);
1214            }
1215
1216            if let Some(progress) = &mut progress {
1217                if !progress(offset, size) {
1218                    return Err(MCUmgrClientError::ProgressCallbackError);
1219                };
1220            }
1221
1222            if offset >= size {
1223                break;
1224            }
1225        }
1226
1227        Ok(())
1228    }
1229
1230    /// Queries the file status
1231    pub fn fs_file_status(
1232        &self,
1233        name: impl AsRef<str>,
1234    ) -> Result<commands::fs::FileStatusResponse, MCUmgrClientError> {
1235        self.connection
1236            .execute_command(&commands::fs::FileStatus {
1237                name: name.as_ref(),
1238            })
1239            .map_err(Into::into)
1240    }
1241
1242    /// Computes the hash/checksum of a file
1243    ///
1244    /// For available algorithms, see [`fs_supported_checksum_types()`](MCUmgrClient::fs_supported_checksum_types).
1245    ///
1246    /// # Arguments
1247    ///
1248    /// * `name` - The absolute path of the file on the device
1249    /// * `algorithm` - The hash/checksum algorithm to use, or default if None
1250    /// * `offset` - How many bytes of the file to skip
1251    /// * `length` - How many bytes to read after `offset`. None for the entire file.
1252    ///
1253    pub fn fs_file_checksum(
1254        &self,
1255        name: impl AsRef<str>,
1256        algorithm: Option<impl AsRef<str>>,
1257        offset: u64,
1258        length: Option<u64>,
1259    ) -> Result<commands::fs::FileChecksumResponse, MCUmgrClientError> {
1260        self.connection
1261            .execute_command(&commands::fs::FileChecksum {
1262                name: name.as_ref(),
1263                r#type: algorithm.as_ref().map(AsRef::as_ref),
1264                off: offset,
1265                len: length,
1266            })
1267            .map_err(Into::into)
1268    }
1269
1270    /// Queries which hash/checksum algorithms are available on the target
1271    pub fn fs_supported_checksum_types(
1272        &self,
1273    ) -> Result<HashMap<String, commands::fs::FileChecksumProperties>, MCUmgrClientError> {
1274        self.connection
1275            .execute_command(&commands::fs::SupportedFileChecksumTypes)
1276            .map(|val| val.types)
1277            .map_err(Into::into)
1278    }
1279
1280    /// Close all device files MCUmgr has currently open
1281    pub fn fs_file_close(&self) -> Result<(), MCUmgrClientError> {
1282        self.connection
1283            .execute_command(&commands::fs::FileClose)
1284            .map(Into::into)
1285            .map_err(Into::into)
1286    }
1287
1288    /// Run a shell command.
1289    ///
1290    /// # Arguments
1291    ///
1292    /// * `argv` - The shell command to be executed.
1293    /// * `use_retries` - Retry request a certain amount of times if a transport error occurs.
1294    ///   Be aware that this might cause the command to be executed multiple times.
1295    ///
1296    /// # Return
1297    ///
1298    /// A tuple of (returncode, stdout) produced by the command execution.
1299    pub fn shell_execute(
1300        &self,
1301        argv: &[String],
1302        use_retries: bool,
1303    ) -> Result<(i32, String), MCUmgrClientError> {
1304        let command = commands::shell::ShellCommandLineExecute { argv };
1305
1306        if use_retries {
1307            self.connection.execute_command(&command)
1308        } else {
1309            self.connection.execute_command_without_retries(&command)
1310        }
1311        .map(|ret| (ret.ret, ret.o))
1312        .map_err(Into::into)
1313    }
1314
1315    /// Query how many MCUmgr groups are supported by the device.
1316    ///
1317    /// # Return
1318    ///
1319    /// The number of MCUmgr groups the device supports.
1320    ///
1321    pub fn enum_get_group_count(&self) -> Result<u16, MCUmgrClientError> {
1322        self.connection
1323            .execute_command(&commands::r#enum::GroupCount)
1324            .map(|ret| ret.count)
1325            .map_err(Into::into)
1326    }
1327
1328    /// Query all available group IDs in a single command.
1329    ///
1330    /// Note that this might fail if the amount of groups is too large for the
1331    /// SMP frame.
1332    /// But given that the Zephyr implementation contains less than 10 groups,
1333    /// this is currently highly unlikely.
1334    ///
1335    /// If it does fail, use [`enum_iter_group_ids`](Self::enum_iter_group_ids) to iterate
1336    /// through the available group IDs one by one.
1337    ///
1338    /// # Return
1339    ///
1340    /// A list of all MCUmgr group IDs the device supports.
1341    ///
1342    pub fn enum_get_group_ids(&self) -> Result<Vec<u16>, MCUmgrClientError> {
1343        self.connection
1344            .execute_command(&commands::r#enum::ListGroups)
1345            .map(|ret| ret.groups)
1346            .map_err(Into::into)
1347    }
1348
1349    /// Query a single group ID from the device.
1350    ///
1351    /// # Arguments
1352    ///
1353    /// * `index` - The index in the list of group IDs.
1354    ///   Must be smaller than [`enum_get_group_count`](Self::enum_get_group_count).
1355    ///
1356    /// # Return
1357    ///
1358    /// The group ID of the group with the given index
1359    ///
1360    pub fn enum_get_group_id(&self, index: u16) -> Result<u16, MCUmgrClientError> {
1361        self.connection
1362            .execute_command(&commands::r#enum::GroupId { index: Some(index) })
1363            .map(|ret| ret.group)
1364            .map_err(Into::into)
1365    }
1366
1367    /// Iterate through all supported MCUmgr Groups.
1368    ///
1369    /// Same as [`enum_get_group_ids`](Self::enum_get_group_ids), but does not
1370    /// require large message sizes if the number of groups is large. The tradeoff is
1371    /// that this function is much slower.
1372    pub fn enum_iter_group_ids(&self) -> impl Iterator<Item = Result<u16, MCUmgrClientError>> {
1373        let mut i = 0;
1374        let mut num_elements = None;
1375
1376        std::iter::from_fn(move || -> Option<Result<u16, MCUmgrClientError>> {
1377            let mut num_elements_err = None;
1378            let num_elements =
1379                *num_elements.get_or_insert_with(|| match self.enum_get_group_count() {
1380                    Ok(n) => n,
1381                    Err(e) => {
1382                        num_elements_err = Some(e);
1383                        0
1384                    }
1385                });
1386            if let Some(err) = num_elements_err {
1387                return Some(Err(err));
1388            }
1389
1390            if i >= num_elements {
1391                None
1392            } else {
1393                Some(match self.enum_get_group_id(i) {
1394                    Ok(group_id) => {
1395                        i += 1;
1396                        Ok(group_id)
1397                    }
1398                    Err(e) => {
1399                        i = num_elements;
1400                        Err(e)
1401                    }
1402                })
1403            }
1404        })
1405    }
1406
1407    /// Query details from all available groups.
1408    ///
1409    /// # Arguments
1410    ///
1411    /// * `groups` - The group IDs to fetch details for. If omitted, fetch all groups.
1412    ///
1413    /// # Return
1414    ///
1415    /// A list of details about all MCUmgr group IDs the device supports.
1416    ///
1417    pub fn enum_get_group_details(
1418        &self,
1419        groups: Option<&[u16]>,
1420    ) -> Result<Vec<commands::r#enum::GroupDetailsEntry>, MCUmgrClientError> {
1421        self.connection
1422            .execute_command(&commands::r#enum::GroupDetails { groups })
1423            .map(|ret| ret.groups)
1424            .map_err(Into::into)
1425    }
1426
1427    /// Erase the `storage_partition` flash partition.
1428    pub fn zephyr_erase_storage(&self) -> Result<(), MCUmgrClientError> {
1429        self.connection
1430            .execute_command(&commands::zephyr::EraseStorage)
1431            .map(Into::into)
1432            .map_err(Into::into)
1433    }
1434
1435    /// Execute a raw [`commands::McuMgrCommand`].
1436    ///
1437    /// Only returns if no error happened, so the
1438    /// user does not need to check for an `rc` or `err`
1439    /// field in the response.
1440    pub fn raw_command<T: commands::McuMgrCommand>(
1441        &self,
1442        command: &T,
1443    ) -> Result<T::Response, MCUmgrClientError> {
1444        self.connection.execute_command(command).map_err(Into::into)
1445    }
1446}