Skip to main content

mcumgr_toolkit/
client.rs

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