1mod 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 sync::atomic::AtomicUsize,
12 time::Duration,
13};
14
15use miette::Diagnostic;
16use rand::distr::SampleString;
17use serde::Serialize;
18use sha2::{Digest, Sha256};
19use thiserror::Error;
20
21use crate::{
22 bootloader::BootloaderInfo,
23 commands::{
24 self, fs::file_upload_max_data_chunk_size, image::image_upload_max_data_chunk_size,
25 },
26 connection::{Connection, ExecuteError},
27 transport::serial::{ConfigurableTimeout, SerialTransport},
28};
29
30const ZEPHYR_DEFAULT_SMP_FRAME_SIZE: usize = 384;
34
35pub struct MCUmgrClient {
39 connection: Connection,
40 smp_frame_size: AtomicUsize,
41}
42
43#[derive(Error, Debug, Diagnostic)]
45pub enum MCUmgrClientError {
46 #[error("Command execution failed")]
48 #[diagnostic(code(mcumgr_toolkit::client::execute))]
49 ExecuteError(#[from] ExecuteError),
50 #[error("Received an unexpected offset value")]
52 #[diagnostic(code(mcumgr_toolkit::client::unexpected_offset))]
53 UnexpectedOffset,
54 #[error("Writer returned an error")]
56 #[diagnostic(code(mcumgr_toolkit::client::writer))]
57 WriterError(#[source] io::Error),
58 #[error("Reader returned an error")]
60 #[diagnostic(code(mcumgr_toolkit::client::reader))]
61 ReaderError(#[source] io::Error),
62 #[error("Received data does not match reported size")]
64 #[diagnostic(code(mcumgr_toolkit::client::size_mismatch))]
65 SizeMismatch,
66 #[error("Received data is missing file size information")]
68 #[diagnostic(code(mcumgr_toolkit::client::missing_size))]
69 MissingSize,
70 #[error("Progress callback returned an error")]
72 #[diagnostic(code(mcumgr_toolkit::client::progress_cb_error))]
73 ProgressCallbackError,
74 #[error("SMP frame size too small for this command")]
76 #[diagnostic(code(mcumgr_toolkit::client::framesize_too_small))]
77 FrameSizeTooSmall(#[source] io::Error),
78 #[error("Device reported checksum mismatch")]
80 #[diagnostic(code(mcumgr_toolkit::client::checksum_mismatch_on_device))]
81 ChecksumMismatchOnDevice,
82 #[error("Firmware image does not match given checksum")]
84 #[diagnostic(code(mcumgr_toolkit::client::checksum_mismatch))]
85 ChecksumMismatch,
86 #[error("Failed to set the device timeout")]
88 #[diagnostic(code(mcumgr_toolkit::client::set_timeout))]
89 SetTimeoutFailed(#[source] Box<dyn std::error::Error + Send + Sync>),
90}
91
92impl MCUmgrClientError {
93 pub fn command_not_supported(&self) -> bool {
95 if let Self::ExecuteError(err) = self {
96 err.command_not_supported()
97 } else {
98 false
99 }
100 }
101}
102
103#[derive(Debug, Serialize, Clone, Eq, PartialEq)]
105pub struct UsbSerialPortInfo {
106 pub identifier: String,
108 pub port_name: String,
110 pub port_info: serialport::UsbPortInfo,
112}
113
114#[derive(Serialize, Clone, Eq, PartialEq)]
118#[serde(transparent)]
119pub struct UsbSerialPorts(pub Vec<UsbSerialPortInfo>);
120impl std::fmt::Display for UsbSerialPorts {
121 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122 if self.0.is_empty() {
123 writeln!(f)?;
124 write!(f, " - None -")?;
125 return Ok(());
126 }
127
128 for UsbSerialPortInfo {
129 identifier,
130 port_name,
131 port_info,
132 } in &self.0
133 {
134 writeln!(f)?;
135 write!(f, " - {identifier}")?;
136
137 let mut print_port_string = true;
138 let port_string = format!("({port_name})");
139
140 if port_info.manufacturer.is_some() || port_info.product.is_some() {
141 write!(f, " -")?;
142 if let Some(manufacturer) = &port_info.manufacturer {
143 let mut print_manufacturer = true;
144
145 if let Some(product) = &port_info.product {
146 if product.starts_with(manufacturer) {
147 print_manufacturer = false;
148 }
149 }
150
151 if print_manufacturer {
152 write!(f, " {manufacturer}")?;
153 }
154 }
155 if let Some(product) = &port_info.product {
156 write!(f, " {product}")?;
157
158 if product.ends_with(&port_string) {
159 print_port_string = false;
160 }
161 }
162 }
163
164 if print_port_string {
165 write!(f, " {port_string}")?;
166 }
167 }
168 Ok(())
169 }
170}
171impl std::fmt::Debug for UsbSerialPorts {
172 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173 std::fmt::Debug::fmt(&self.0, f)
174 }
175}
176
177#[derive(Error, Debug, Diagnostic)]
179pub enum UsbSerialError {
180 #[error("Serialport returned an error")]
182 #[diagnostic(code(mcumgr_toolkit::usb_serial::serialport_error))]
183 SerialPortError(#[from] serialport::Error),
184 #[error("No serial port matched the identifier '{identifier}'\nAvailable ports:\n{available}")]
186 #[diagnostic(code(mcumgr_toolkit::usb_serial::no_matches))]
187 NoMatchingPort {
188 identifier: String,
190 available: UsbSerialPorts,
192 },
193 #[error("Multiple serial ports matched the identifier '{identifier}'\n{ports}")]
195 #[diagnostic(code(mcumgr_toolkit::usb_serial::multiple_matches))]
196 MultipleMatchingPorts {
197 identifier: String,
199 ports: UsbSerialPorts,
201 },
202 #[error("An empty identifier was provided")]
205 #[diagnostic(code(mcumgr_toolkit::usb_serial::empty_identifier))]
206 IdentifierEmpty {
207 ports: UsbSerialPorts,
209 },
210 #[error("The given identifier was not a valid RegEx")]
212 #[diagnostic(code(mcumgr_toolkit::usb_serial::regex_error))]
213 RegexError(#[from] regex::Error),
214}
215
216impl MCUmgrClient {
217 pub fn new_from_serial<T: Send + Read + Write + ConfigurableTimeout + 'static>(
230 serial: T,
231 ) -> Self {
232 Self {
233 connection: Connection::new(SerialTransport::new(serial)),
234 smp_frame_size: ZEPHYR_DEFAULT_SMP_FRAME_SIZE.into(),
235 }
236 }
237
238 pub fn new_from_usb_serial(
256 identifier: impl AsRef<str>,
257 baud_rate: u32,
258 timeout: Duration,
259 ) -> Result<Self, UsbSerialError> {
260 let identifier = identifier.as_ref();
261
262 let ports = serialport::available_ports()?
263 .into_iter()
264 .filter_map(|port| {
265 if let serialport::SerialPortType::UsbPort(port_info) = port.port_type {
266 if let Some(interface) = port_info.interface {
267 Some(UsbSerialPortInfo {
268 identifier: format!(
269 "{:04x}:{:04x}:{}",
270 port_info.vid, port_info.pid, interface
271 ),
272 port_name: port.port_name,
273 port_info,
274 })
275 } else {
276 Some(UsbSerialPortInfo {
277 identifier: format!("{:04x}:{:04x}", port_info.vid, port_info.pid),
278 port_name: port.port_name,
279 port_info,
280 })
281 }
282 } else {
283 None
284 }
285 })
286 .collect::<Vec<_>>();
287
288 if identifier.is_empty() {
289 return Err(UsbSerialError::IdentifierEmpty {
290 ports: UsbSerialPorts(ports),
291 });
292 }
293
294 let port_regex = regex::RegexBuilder::new(identifier)
295 .case_insensitive(true)
296 .unicode(true)
297 .build()?;
298
299 let matches = ports
300 .iter()
301 .filter(|port| {
302 if let Some(m) = port_regex.find(&port.identifier) {
303 m.start() == 0
305 } else {
306 false
307 }
308 })
309 .cloned()
310 .collect::<Vec<_>>();
311
312 if matches.len() > 1 {
313 return Err(UsbSerialError::MultipleMatchingPorts {
314 identifier: identifier.to_string(),
315 ports: UsbSerialPorts(matches),
316 });
317 }
318
319 let port_name = match matches.into_iter().next() {
320 Some(port) => port.port_name,
321 None => {
322 return Err(UsbSerialError::NoMatchingPort {
323 identifier: identifier.to_string(),
324 available: UsbSerialPorts(ports),
325 });
326 }
327 };
328
329 let serial = serialport::new(port_name, baud_rate)
330 .timeout(timeout)
331 .open()?;
332
333 Ok(Self::new_from_serial(serial))
334 }
335
336 pub fn set_frame_size(&self, smp_frame_size: usize) {
341 self.smp_frame_size
342 .store(smp_frame_size, std::sync::atomic::Ordering::SeqCst);
343 }
344
345 pub fn use_auto_frame_size(&self) -> Result<(), MCUmgrClientError> {
349 let mcumgr_params = self
350 .connection
351 .execute_command(&commands::os::MCUmgrParameters)?;
352
353 log::debug!("Using frame size {}.", mcumgr_params.buf_size);
354
355 self.smp_frame_size.store(
356 mcumgr_params.buf_size as usize,
357 std::sync::atomic::Ordering::SeqCst,
358 );
359
360 Ok(())
361 }
362
363 pub fn set_timeout(&self, timeout: Duration) -> Result<(), MCUmgrClientError> {
368 self.connection
369 .set_timeout(timeout)
370 .map_err(MCUmgrClientError::SetTimeoutFailed)
371 }
372
373 pub fn set_retries(&self, retries: u8) {
378 self.connection.set_retries(retries)
379 }
380
381 pub fn check_connection(&self) -> Result<(), MCUmgrClientError> {
389 let random_message = rand::distr::Alphanumeric.sample_string(&mut rand::rng(), 16);
390 let response = self.os_echo(&random_message)?;
391 if random_message == response {
392 Ok(())
393 } else {
394 Err(
395 ExecuteError::ReceiveFailed(crate::transport::ReceiveError::UnexpectedResponse)
396 .into(),
397 )
398 }
399 }
400
401 pub fn firmware_update(
411 &self,
412 firmware: impl AsRef<[u8]>,
413 checksum: Option<[u8; 32]>,
414 params: FirmwareUpdateParams,
415 progress: Option<&mut FirmwareUpdateProgressCallback>,
416 ) -> Result<(), FirmwareUpdateError> {
417 firmware_update::firmware_update(self, firmware, checksum, params, progress)
418 }
419
420 pub fn os_echo(&self, msg: impl AsRef<str>) -> Result<String, MCUmgrClientError> {
424 self.connection
425 .execute_command(&commands::os::Echo { d: msg.as_ref() })
426 .map(|resp| resp.r)
427 .map_err(Into::into)
428 }
429
430 pub fn os_task_statistics(
441 &self,
442 ) -> Result<HashMap<String, commands::os::TaskStatisticsEntry>, MCUmgrClientError> {
443 self.connection
444 .execute_command(&commands::os::TaskStatistics)
445 .map(|resp| {
446 let mut tasks = resp.tasks;
447 for (_, stats) in tasks.iter_mut() {
448 stats.stkuse = stats.stkuse.map(|val| val * 4);
449 stats.stksiz = stats.stksiz.map(|val| val * 4);
450 }
451 tasks
452 })
453 .map_err(Into::into)
454 }
455
456 pub fn os_set_datetime(
458 &self,
459 datetime: chrono::NaiveDateTime,
460 ) -> Result<(), MCUmgrClientError> {
461 self.connection
462 .execute_command(&commands::os::DateTimeSet { datetime })
463 .map(Into::into)
464 .map_err(Into::into)
465 }
466
467 pub fn os_get_datetime(&self) -> Result<chrono::NaiveDateTime, MCUmgrClientError> {
469 self.connection
470 .execute_command(&commands::os::DateTimeGet)
471 .map(|val| val.datetime)
472 .map_err(Into::into)
473 }
474
475 pub fn os_system_reset(
489 &self,
490 force: bool,
491 boot_mode: Option<u8>,
492 ) -> Result<(), MCUmgrClientError> {
493 self.connection
494 .execute_command(&commands::os::SystemReset { force, boot_mode })
495 .map(Into::into)
496 .map_err(Into::into)
497 }
498
499 pub fn os_mcumgr_parameters(
501 &self,
502 ) -> Result<commands::os::MCUmgrParametersResponse, MCUmgrClientError> {
503 self.connection
504 .execute_command(&commands::os::MCUmgrParameters)
505 .map_err(Into::into)
506 }
507
508 pub fn os_application_info(&self, format: Option<&str>) -> Result<String, MCUmgrClientError> {
520 self.connection
521 .execute_command(&commands::os::ApplicationInfo { format })
522 .map(|resp| resp.output)
523 .map_err(Into::into)
524 }
525
526 pub fn os_bootloader_info(&self) -> Result<BootloaderInfo, MCUmgrClientError> {
528 Ok(
529 match self
530 .connection
531 .execute_command(&commands::os::BootloaderInfo)?
532 .bootloader
533 .as_str()
534 {
535 "MCUboot" => {
536 let mode_data = self
537 .connection
538 .execute_command(&commands::os::BootloaderInfoMcubootMode {})?;
539 BootloaderInfo::MCUboot {
540 mode: mode_data.mode,
541 no_downgrade: mode_data.no_downgrade,
542 }
543 }
544 name => BootloaderInfo::Unknown {
545 name: name.to_string(),
546 },
547 },
548 )
549 }
550
551 pub fn image_get_state(&self) -> Result<Vec<commands::image::ImageState>, MCUmgrClientError> {
553 self.connection
554 .execute_command(&commands::image::GetImageState)
555 .map(|val| val.images)
556 .map_err(Into::into)
557 }
558
559 pub fn image_set_state(
575 &self,
576 hash: Option<[u8; 32]>,
577 confirm: bool,
578 ) -> Result<Vec<commands::image::ImageState>, MCUmgrClientError> {
579 self.connection
580 .execute_command(&commands::image::SetImageState {
581 hash: hash.as_ref(),
582 confirm,
583 })
584 .map(|val| val.images)
585 .map_err(Into::into)
586 }
587
588 pub fn image_upload(
599 &self,
600 data: impl AsRef<[u8]>,
601 image: Option<u32>,
602 checksum: Option<[u8; 32]>,
603 upgrade_only: bool,
604 mut progress: Option<&mut dyn FnMut(u64, u64) -> bool>,
605 ) -> Result<(), MCUmgrClientError> {
606 let chunk_size_max = image_upload_max_data_chunk_size(
607 self.smp_frame_size
608 .load(std::sync::atomic::Ordering::SeqCst),
609 )
610 .map_err(MCUmgrClientError::FrameSizeTooSmall)?;
611
612 let data = data.as_ref();
613
614 let actual_checksum: [u8; 32] = Sha256::digest(data).into();
615 if let Some(checksum) = checksum {
616 if actual_checksum != checksum {
617 return Err(MCUmgrClientError::ChecksumMismatch);
618 }
619 }
620
621 let mut offset = 0;
622 let size = data.len();
623
624 let mut checksum_matched = None;
625
626 while offset < size {
627 let current_chunk_size = (size - offset).min(chunk_size_max);
628 let chunk_data = &data[offset..offset + current_chunk_size];
629
630 let upload_response = if offset == 0 {
631 self.connection
632 .execute_command(&commands::image::ImageUpload {
633 image,
634 len: Some(size as u64),
635 off: offset as u64,
636 sha: Some(&actual_checksum),
637 data: chunk_data,
638 upgrade: Some(upgrade_only),
639 })?
640 } else {
641 self.connection
642 .execute_command(&commands::image::ImageUpload {
643 image: None,
644 len: None,
645 off: offset as u64,
646 sha: None,
647 data: chunk_data,
648 upgrade: None,
649 })?
650 };
651
652 offset = upload_response
653 .off
654 .try_into()
655 .map_err(|_| MCUmgrClientError::UnexpectedOffset)?;
656
657 if offset > size {
658 return Err(MCUmgrClientError::UnexpectedOffset);
659 }
660
661 if let Some(progress) = &mut progress {
662 if !progress(offset as u64, size as u64) {
663 return Err(MCUmgrClientError::ProgressCallbackError);
664 };
665 }
666
667 if let Some(is_match) = upload_response.r#match {
668 checksum_matched = Some(is_match);
669 }
670 }
671
672 if let Some(checksum_matched) = checksum_matched {
673 if !checksum_matched {
674 return Err(MCUmgrClientError::ChecksumMismatchOnDevice);
675 }
676 } else {
677 log::warn!("Device did not perform image checksum verification");
678 }
679
680 Ok(())
681 }
682
683 pub fn image_erase(&self, slot: Option<u32>) -> Result<(), MCUmgrClientError> {
690 self.connection
691 .execute_command(&commands::image::ImageErase { slot })
692 .map(Into::into)
693 .map_err(Into::into)
694 }
695
696 pub fn image_slot_info(
698 &self,
699 ) -> Result<Vec<commands::image::SlotInfoImage>, MCUmgrClientError> {
700 self.connection
701 .execute_command(&commands::image::SlotInfo)
702 .map(|val| val.images)
703 .map_err(Into::into)
704 }
705
706 pub fn fs_file_download<T: Write>(
720 &self,
721 name: impl AsRef<str>,
722 mut writer: T,
723 mut progress: Option<&mut dyn FnMut(u64, u64) -> bool>,
724 ) -> Result<(), MCUmgrClientError> {
725 let name = name.as_ref();
726 let response = self
727 .connection
728 .execute_command(&commands::fs::FileDownload { name, off: 0 })?;
729
730 let file_len = response.len.ok_or(MCUmgrClientError::MissingSize)?;
731 if response.off != 0 {
732 return Err(MCUmgrClientError::UnexpectedOffset);
733 }
734
735 let mut offset = 0;
736
737 if let Some(progress) = &mut progress {
738 if !progress(offset, file_len) {
739 return Err(MCUmgrClientError::ProgressCallbackError);
740 };
741 }
742
743 writer
744 .write_all(&response.data)
745 .map_err(MCUmgrClientError::WriterError)?;
746 offset += response.data.len() as u64;
747
748 if let Some(progress) = &mut progress {
749 if !progress(offset, file_len) {
750 return Err(MCUmgrClientError::ProgressCallbackError);
751 };
752 }
753
754 while offset < file_len {
755 let response = self
756 .connection
757 .execute_command(&commands::fs::FileDownload { name, off: offset })?;
758
759 if response.off != offset {
760 return Err(MCUmgrClientError::UnexpectedOffset);
761 }
762
763 writer
764 .write_all(&response.data)
765 .map_err(MCUmgrClientError::WriterError)?;
766 offset += response.data.len() as u64;
767
768 if let Some(progress) = &mut progress {
769 if !progress(offset, file_len) {
770 return Err(MCUmgrClientError::ProgressCallbackError);
771 };
772 }
773 }
774
775 if offset != file_len {
776 return Err(MCUmgrClientError::SizeMismatch);
777 }
778
779 Ok(())
780 }
781
782 pub fn fs_file_upload<T: Read>(
798 &self,
799 name: impl AsRef<str>,
800 mut reader: T,
801 size: u64,
802 mut progress: Option<&mut dyn FnMut(u64, u64) -> bool>,
803 ) -> Result<(), MCUmgrClientError> {
804 let name = name.as_ref();
805
806 let chunk_size_max = file_upload_max_data_chunk_size(
807 self.smp_frame_size
808 .load(std::sync::atomic::Ordering::SeqCst),
809 name,
810 )
811 .map_err(MCUmgrClientError::FrameSizeTooSmall)?;
812 let mut data_buffer = vec![0u8; chunk_size_max].into_boxed_slice();
813
814 let mut offset = 0;
815
816 while offset < size {
817 let current_chunk_size = (size - offset).min(data_buffer.len() as u64) as usize;
818
819 let chunk_buffer = &mut data_buffer[..current_chunk_size];
820 reader
821 .read_exact(chunk_buffer)
822 .map_err(MCUmgrClientError::ReaderError)?;
823
824 self.connection.execute_command(&commands::fs::FileUpload {
825 off: offset,
826 data: chunk_buffer,
827 name,
828 len: if offset == 0 { Some(size) } else { None },
829 })?;
830
831 offset += chunk_buffer.len() as u64;
832
833 if let Some(progress) = &mut progress {
834 if !progress(offset, size) {
835 return Err(MCUmgrClientError::ProgressCallbackError);
836 };
837 }
838 }
839
840 Ok(())
841 }
842
843 pub fn fs_file_status(
845 &self,
846 name: impl AsRef<str>,
847 ) -> Result<commands::fs::FileStatusResponse, MCUmgrClientError> {
848 self.connection
849 .execute_command(&commands::fs::FileStatus {
850 name: name.as_ref(),
851 })
852 .map_err(Into::into)
853 }
854
855 pub fn fs_file_checksum(
867 &self,
868 name: impl AsRef<str>,
869 algorithm: Option<impl AsRef<str>>,
870 offset: u64,
871 length: Option<u64>,
872 ) -> Result<commands::fs::FileChecksumResponse, MCUmgrClientError> {
873 self.connection
874 .execute_command(&commands::fs::FileChecksum {
875 name: name.as_ref(),
876 r#type: algorithm.as_ref().map(AsRef::as_ref),
877 off: offset,
878 len: length,
879 })
880 .map_err(Into::into)
881 }
882
883 pub fn fs_supported_checksum_types(
885 &self,
886 ) -> Result<HashMap<String, commands::fs::FileChecksumProperties>, MCUmgrClientError> {
887 self.connection
888 .execute_command(&commands::fs::SupportedFileChecksumTypes)
889 .map(|val| val.types)
890 .map_err(Into::into)
891 }
892
893 pub fn fs_file_close(&self) -> Result<(), MCUmgrClientError> {
895 self.connection
896 .execute_command(&commands::fs::FileClose)
897 .map(Into::into)
898 .map_err(Into::into)
899 }
900
901 pub fn shell_execute(
913 &self,
914 argv: &[String],
915 use_retries: bool,
916 ) -> Result<(i32, String), MCUmgrClientError> {
917 let command = commands::shell::ShellCommandLineExecute { argv };
918
919 if use_retries {
920 self.connection.execute_command(&command)
921 } else {
922 self.connection.execute_command_without_retries(&command)
923 }
924 .map(|ret| (ret.ret, ret.o))
925 .map_err(Into::into)
926 }
927
928 pub fn zephyr_erase_storage(&self) -> Result<(), MCUmgrClientError> {
930 self.connection
931 .execute_command(&commands::zephyr::EraseStorage)
932 .map(Into::into)
933 .map_err(Into::into)
934 }
935
936 pub fn raw_command<T: commands::McuMgrCommand>(
942 &self,
943 command: &T,
944 ) -> Result<T::Response, MCUmgrClientError> {
945 self.connection.execute_command(command).map_err(Into::into)
946 }
947}