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