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::{
28 ReceiveError,
29 serial::{ConfigurableTimeout, SerialTransport},
30 },
31};
32
33const ZEPHYR_DEFAULT_SMP_FRAME_SIZE: usize = 384;
37
38pub struct MCUmgrClient {
42 connection: Connection,
43 smp_frame_size: AtomicUsize,
44}
45
46#[derive(Error, Debug, Diagnostic)]
48pub enum MCUmgrClientError {
49 #[error("Command execution failed")]
51 #[diagnostic(code(mcumgr_toolkit::client::execute))]
52 ExecuteError(#[from] ExecuteError),
53 #[error("Received an unexpected offset value")]
55 #[diagnostic(code(mcumgr_toolkit::client::unexpected_offset))]
56 UnexpectedOffset,
57 #[error("Writer returned an error")]
59 #[diagnostic(code(mcumgr_toolkit::client::writer))]
60 WriterError(#[source] io::Error),
61 #[error("Reader returned an error")]
63 #[diagnostic(code(mcumgr_toolkit::client::reader))]
64 ReaderError(#[source] io::Error),
65 #[error("Received data does not match reported size")]
67 #[diagnostic(code(mcumgr_toolkit::client::size_mismatch))]
68 SizeMismatch,
69 #[error("Received data is missing file size information")]
71 #[diagnostic(code(mcumgr_toolkit::client::missing_size))]
72 MissingSize,
73 #[error("Progress callback returned an error")]
75 #[diagnostic(code(mcumgr_toolkit::client::progress_cb_error))]
76 ProgressCallbackError,
77 #[error("SMP frame size too small for this command")]
79 #[diagnostic(code(mcumgr_toolkit::client::framesize_too_small))]
80 FrameSizeTooSmall(#[source] io::Error),
81 #[error("Device reported checksum mismatch")]
83 #[diagnostic(code(mcumgr_toolkit::client::checksum_mismatch_on_device))]
84 ChecksumMismatchOnDevice,
85 #[error("Firmware image does not match given checksum")]
87 #[diagnostic(code(mcumgr_toolkit::client::checksum_mismatch))]
88 ChecksumMismatch,
89 #[error("Failed to set the device timeout")]
91 #[diagnostic(code(mcumgr_toolkit::client::set_timeout))]
92 SetTimeoutFailed(#[source] Box<dyn std::error::Error + Send + Sync>),
93}
94
95impl MCUmgrClientError {
96 pub fn command_not_supported(&self) -> bool {
98 if let Self::ExecuteError(err) = self {
99 err.command_not_supported()
100 } else {
101 false
102 }
103 }
104}
105
106#[derive(Debug, Serialize, Clone, Eq, PartialEq)]
108pub struct UsbSerialPortInfo {
109 pub identifier: String,
111 pub port_name: String,
113 pub port_info: serialport::UsbPortInfo,
115}
116
117#[derive(Serialize, Clone, Eq, PartialEq)]
121#[serde(transparent)]
122pub struct UsbSerialPorts(pub Vec<UsbSerialPortInfo>);
123impl std::fmt::Display for UsbSerialPorts {
124 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125 if self.0.is_empty() {
126 writeln!(f)?;
127 write!(f, " - None -")?;
128 return Ok(());
129 }
130
131 for UsbSerialPortInfo {
132 identifier,
133 port_name,
134 port_info,
135 } in &self.0
136 {
137 writeln!(f)?;
138 write!(f, " - {identifier}")?;
139
140 let mut print_port_string = true;
141 let port_string = format!("({port_name})");
142
143 if port_info.manufacturer.is_some() || port_info.product.is_some() {
144 write!(f, " -")?;
145 if let Some(manufacturer) = &port_info.manufacturer {
146 let mut print_manufacturer = true;
147
148 if let Some(product) = &port_info.product {
149 if product.starts_with(manufacturer) {
150 print_manufacturer = false;
151 }
152 }
153
154 if print_manufacturer {
155 write!(f, " {manufacturer}")?;
156 }
157 }
158 if let Some(product) = &port_info.product {
159 write!(f, " {product}")?;
160
161 if product.ends_with(&port_string) {
162 print_port_string = false;
163 }
164 }
165 }
166
167 if print_port_string {
168 write!(f, " {port_string}")?;
169 }
170 }
171 Ok(())
172 }
173}
174impl std::fmt::Debug for UsbSerialPorts {
175 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176 std::fmt::Debug::fmt(&self.0, f)
177 }
178}
179
180#[derive(Error, Debug, Diagnostic)]
182pub enum UsbSerialError {
183 #[error("Serialport returned an error")]
185 #[diagnostic(code(mcumgr_toolkit::usb_serial::serialport_error))]
186 SerialPortError(#[from] serialport::Error),
187 #[error("No serial port matched the identifier '{identifier}'\nAvailable ports:\n{available}")]
189 #[diagnostic(code(mcumgr_toolkit::usb_serial::no_matches))]
190 NoMatchingPort {
191 identifier: String,
193 available: UsbSerialPorts,
195 },
196 #[error("Multiple serial ports matched the identifier '{identifier}'\n{ports}")]
198 #[diagnostic(code(mcumgr_toolkit::usb_serial::multiple_matches))]
199 MultipleMatchingPorts {
200 identifier: String,
202 ports: UsbSerialPorts,
204 },
205 #[error("An empty identifier was provided")]
208 #[diagnostic(code(mcumgr_toolkit::usb_serial::empty_identifier))]
209 IdentifierEmpty {
210 ports: UsbSerialPorts,
212 },
213 #[error("The given identifier was not a valid RegEx")]
215 #[diagnostic(code(mcumgr_toolkit::usb_serial::regex_error))]
216 RegexError(#[from] regex::Error),
217}
218
219impl MCUmgrClient {
220 pub fn new_from_serial<T: Send + Read + Write + ConfigurableTimeout + 'static>(
233 serial: T,
234 ) -> Self {
235 Self {
236 connection: Connection::new(SerialTransport::new(serial)),
237 smp_frame_size: ZEPHYR_DEFAULT_SMP_FRAME_SIZE.into(),
238 }
239 }
240
241 pub fn new_from_usb_serial(
259 identifier: impl AsRef<str>,
260 baud_rate: u32,
261 timeout: Duration,
262 ) -> Result<Self, UsbSerialError> {
263 let identifier = identifier.as_ref();
264
265 let ports = serialport::available_ports()?
266 .into_iter()
267 .filter_map(|port| {
268 if let serialport::SerialPortType::UsbPort(port_info) = port.port_type {
269 if let Some(interface) = port_info.interface {
270 Some(UsbSerialPortInfo {
271 identifier: format!(
272 "{:04x}:{:04x}:{}",
273 port_info.vid, port_info.pid, interface
274 ),
275 port_name: port.port_name,
276 port_info,
277 })
278 } else {
279 Some(UsbSerialPortInfo {
280 identifier: format!("{:04x}:{:04x}", port_info.vid, port_info.pid),
281 port_name: port.port_name,
282 port_info,
283 })
284 }
285 } else {
286 None
287 }
288 })
289 .collect::<Vec<_>>();
290
291 if identifier.is_empty() {
292 return Err(UsbSerialError::IdentifierEmpty {
293 ports: UsbSerialPorts(ports),
294 });
295 }
296
297 let port_regex = regex::RegexBuilder::new(identifier)
298 .case_insensitive(true)
299 .unicode(true)
300 .build()?;
301
302 let matches = ports
303 .iter()
304 .filter(|port| {
305 if let Some(m) = port_regex.find(&port.identifier) {
306 m.start() == 0
308 } else {
309 false
310 }
311 })
312 .cloned()
313 .collect::<Vec<_>>();
314
315 if matches.len() > 1 {
316 return Err(UsbSerialError::MultipleMatchingPorts {
317 identifier: identifier.to_string(),
318 ports: UsbSerialPorts(matches),
319 });
320 }
321
322 let port_name = match matches.into_iter().next() {
323 Some(port) => port.port_name,
324 None => {
325 return Err(UsbSerialError::NoMatchingPort {
326 identifier: identifier.to_string(),
327 available: UsbSerialPorts(ports),
328 });
329 }
330 };
331
332 let serial = serialport::new(port_name, baud_rate)
333 .timeout(timeout)
334 .open()?;
335
336 Ok(Self::new_from_serial(serial))
337 }
338
339 pub fn set_frame_size(&self, smp_frame_size: usize) {
344 self.smp_frame_size
345 .store(smp_frame_size, std::sync::atomic::Ordering::SeqCst);
346 }
347
348 pub fn use_auto_frame_size(&self) -> Result<(), MCUmgrClientError> {
352 let mcumgr_params = self
353 .connection
354 .execute_command(&commands::os::MCUmgrParameters)?;
355
356 log::debug!("Using frame size {}.", mcumgr_params.buf_size);
357
358 self.smp_frame_size.store(
359 mcumgr_params.buf_size as usize,
360 std::sync::atomic::Ordering::SeqCst,
361 );
362
363 Ok(())
364 }
365
366 pub fn set_timeout(&self, timeout: Duration) -> Result<(), MCUmgrClientError> {
371 self.connection
372 .set_timeout(timeout)
373 .map_err(MCUmgrClientError::SetTimeoutFailed)
374 }
375
376 pub fn set_retries(&self, retries: u8) {
381 self.connection.set_retries(retries)
382 }
383
384 pub fn check_connection(&self) -> Result<(), MCUmgrClientError> {
392 let random_message = rand::distr::Alphanumeric.sample_string(&mut rand::rng(), 16);
393 let response = self.os_echo(&random_message)?;
394 if random_message == response {
395 Ok(())
396 } else {
397 Err(
398 ExecuteError::ReceiveFailed(crate::transport::ReceiveError::UnexpectedResponse)
399 .into(),
400 )
401 }
402 }
403
404 pub fn firmware_update(
414 &self,
415 firmware: impl AsRef<[u8]>,
416 checksum: Option<[u8; 32]>,
417 params: FirmwareUpdateParams,
418 progress: Option<&mut FirmwareUpdateProgressCallback>,
419 ) -> Result<(), FirmwareUpdateError> {
420 firmware_update::firmware_update(self, firmware, checksum, params, progress)
421 }
422
423 pub fn os_echo(&self, msg: impl AsRef<str>) -> Result<String, MCUmgrClientError> {
427 self.connection
428 .execute_command(&commands::os::Echo { d: msg.as_ref() })
429 .map(|resp| resp.r)
430 .map_err(Into::into)
431 }
432
433 pub fn os_task_statistics(
444 &self,
445 ) -> Result<HashMap<String, commands::os::TaskStatisticsEntry>, MCUmgrClientError> {
446 self.connection
447 .execute_command(&commands::os::TaskStatistics)
448 .map(|resp| {
449 let mut tasks = resp.tasks;
450 for (_, stats) in tasks.iter_mut() {
451 stats.stkuse = stats.stkuse.map(|val| val * 4);
452 stats.stksiz = stats.stksiz.map(|val| val * 4);
453 }
454 tasks
455 })
456 .map_err(Into::into)
457 }
458
459 pub fn os_set_datetime(
461 &self,
462 datetime: chrono::NaiveDateTime,
463 ) -> Result<(), MCUmgrClientError> {
464 self.connection
465 .execute_command(&commands::os::DateTimeSet { datetime })
466 .map(Into::into)
467 .map_err(Into::into)
468 }
469
470 pub fn os_get_datetime(&self) -> Result<chrono::NaiveDateTime, MCUmgrClientError> {
472 self.connection
473 .execute_command(&commands::os::DateTimeGet)
474 .map(|val| val.datetime)
475 .map_err(Into::into)
476 }
477
478 pub fn os_system_reset(
492 &self,
493 force: bool,
494 boot_mode: Option<u8>,
495 ) -> Result<(), MCUmgrClientError> {
496 self.connection
497 .execute_command(&commands::os::SystemReset { force, boot_mode })
498 .map(Into::into)
499 .map_err(Into::into)
500 }
501
502 pub fn os_mcumgr_parameters(
504 &self,
505 ) -> Result<commands::os::MCUmgrParametersResponse, MCUmgrClientError> {
506 self.connection
507 .execute_command(&commands::os::MCUmgrParameters)
508 .map_err(Into::into)
509 }
510
511 pub fn os_application_info(&self, format: Option<&str>) -> Result<String, MCUmgrClientError> {
523 self.connection
524 .execute_command(&commands::os::ApplicationInfo { format })
525 .map(|resp| resp.output)
526 .map_err(Into::into)
527 }
528
529 pub fn os_bootloader_info(&self) -> Result<BootloaderInfo, MCUmgrClientError> {
531 Ok(
532 match self
533 .connection
534 .execute_command(&commands::os::BootloaderInfo)?
535 .bootloader
536 .as_str()
537 {
538 "MCUboot" => {
539 let mode_data = self
540 .connection
541 .execute_command(&commands::os::BootloaderInfoMcubootMode {})?;
542 BootloaderInfo::MCUboot {
543 mode: mode_data.mode,
544 no_downgrade: mode_data.no_downgrade,
545 }
546 }
547 name => BootloaderInfo::Unknown {
548 name: name.to_string(),
549 },
550 },
551 )
552 }
553
554 pub fn image_get_state(&self) -> Result<Vec<commands::image::ImageState>, MCUmgrClientError> {
556 self.connection
557 .execute_command(&commands::image::GetImageState)
558 .map(|val| val.images)
559 .map_err(Into::into)
560 }
561
562 pub fn image_set_state(
578 &self,
579 hash: Option<[u8; 32]>,
580 confirm: bool,
581 ) -> Result<Vec<commands::image::ImageState>, MCUmgrClientError> {
582 self.connection
583 .execute_command(&commands::image::SetImageState {
584 hash: hash.as_ref(),
585 confirm,
586 })
587 .map(|val| val.images)
588 .map_err(Into::into)
589 }
590
591 pub fn image_upload(
609 &self,
610 data: impl AsRef<[u8]>,
611 image: Option<u32>,
612 checksum: Option<[u8; 32]>,
613 upgrade_only: bool,
614 mut progress: Option<&mut dyn FnMut(u64, u64) -> bool>,
615 ) -> Result<(), MCUmgrClientError> {
616 let chunk_size_max = image_upload_max_data_chunk_size(
617 self.smp_frame_size
618 .load(std::sync::atomic::Ordering::SeqCst),
619 )
620 .map_err(MCUmgrClientError::FrameSizeTooSmall)?;
621
622 let data = data.as_ref();
623
624 let actual_checksum: [u8; 32] = Sha256::digest(data).into();
625 if let Some(checksum) = checksum {
626 if actual_checksum != checksum {
627 return Err(MCUmgrClientError::ChecksumMismatch);
628 }
629 }
630
631 let mut offset = 0;
632 let size = data.len();
633
634 let mut checksum_matched = None;
635
636 while offset < size {
637 let current_chunk_size = (size - offset).min(chunk_size_max);
638 let chunk_data = &data[offset..offset + current_chunk_size];
639
640 let upload_response = if offset == 0 {
641 let result = self
642 .connection
643 .execute_command(&commands::image::ImageUpload {
644 image,
645 len: Some(size as u64),
646 off: offset as u64,
647 sha: Some(&actual_checksum),
648 data: chunk_data,
649 upgrade: Some(upgrade_only),
650 });
651
652 if let Err(ExecuteError::ReceiveFailed(ReceiveError::TransportError(e))) = &result {
653 if let io::ErrorKind::TimedOut = e.kind() {
654 log::warn!(
655 "Timed out during transfer of first chunk. Consider enabling CONFIG_IMG_ERASE_PROGRESSIVELY."
656 )
657 }
658 }
659
660 result?
661 } else {
662 self.connection
663 .execute_command(&commands::image::ImageUpload {
664 image: None,
665 len: None,
666 off: offset as u64,
667 sha: None,
668 data: chunk_data,
669 upgrade: None,
670 })?
671 };
672
673 offset = upload_response
674 .off
675 .try_into()
676 .map_err(|_| MCUmgrClientError::UnexpectedOffset)?;
677
678 if offset > size {
679 return Err(MCUmgrClientError::UnexpectedOffset);
680 }
681
682 if let Some(progress) = &mut progress {
683 if !progress(offset as u64, size as u64) {
684 return Err(MCUmgrClientError::ProgressCallbackError);
685 };
686 }
687
688 if let Some(is_match) = upload_response.r#match {
689 checksum_matched = Some(is_match);
690 }
691 }
692
693 if let Some(checksum_matched) = checksum_matched {
694 if !checksum_matched {
695 return Err(MCUmgrClientError::ChecksumMismatchOnDevice);
696 }
697 } else {
698 log::warn!("Device did not perform image checksum verification");
699 }
700
701 Ok(())
702 }
703
704 pub fn image_erase(&self, slot: Option<u32>) -> Result<(), MCUmgrClientError> {
711 self.connection
712 .execute_command(&commands::image::ImageErase { slot })
713 .map(Into::into)
714 .map_err(Into::into)
715 }
716
717 pub fn image_slot_info(
719 &self,
720 ) -> Result<Vec<commands::image::SlotInfoImage>, MCUmgrClientError> {
721 self.connection
722 .execute_command(&commands::image::SlotInfo)
723 .map(|val| val.images)
724 .map_err(Into::into)
725 }
726
727 pub fn stats_get_group_data(
734 &self,
735 name: impl AsRef<str>,
736 ) -> Result<HashMap<String, u64>, MCUmgrClientError> {
737 self.connection
738 .execute_command(&commands::stats::GroupData {
739 name: name.as_ref(),
740 })
741 .map(|val| val.fields)
742 .map_err(Into::into)
743 }
744
745 pub fn stats_list_groups(&self) -> Result<Vec<String>, MCUmgrClientError> {
747 self.connection
748 .execute_command(&commands::stats::ListGroups)
749 .map(|val| val.stat_list)
750 .map_err(Into::into)
751 }
752
753 pub fn fs_file_download<T: Write>(
767 &self,
768 name: impl AsRef<str>,
769 mut writer: T,
770 mut progress: Option<&mut dyn FnMut(u64, u64) -> bool>,
771 ) -> Result<(), MCUmgrClientError> {
772 let name = name.as_ref();
773 let response = self
774 .connection
775 .execute_command(&commands::fs::FileDownload { name, off: 0 })?;
776
777 let file_len = response.len.ok_or(MCUmgrClientError::MissingSize)?;
778 if response.off != 0 {
779 return Err(MCUmgrClientError::UnexpectedOffset);
780 }
781
782 let mut offset = 0;
783
784 if let Some(progress) = &mut progress {
785 if !progress(offset, file_len) {
786 return Err(MCUmgrClientError::ProgressCallbackError);
787 };
788 }
789
790 writer
791 .write_all(&response.data)
792 .map_err(MCUmgrClientError::WriterError)?;
793 offset += response.data.len() as u64;
794
795 if let Some(progress) = &mut progress {
796 if !progress(offset, file_len) {
797 return Err(MCUmgrClientError::ProgressCallbackError);
798 };
799 }
800
801 while offset < file_len {
802 let response = self
803 .connection
804 .execute_command(&commands::fs::FileDownload { name, off: offset })?;
805
806 if response.off != offset {
807 return Err(MCUmgrClientError::UnexpectedOffset);
808 }
809
810 writer
811 .write_all(&response.data)
812 .map_err(MCUmgrClientError::WriterError)?;
813 offset += response.data.len() as u64;
814
815 if let Some(progress) = &mut progress {
816 if !progress(offset, file_len) {
817 return Err(MCUmgrClientError::ProgressCallbackError);
818 };
819 }
820 }
821
822 if offset != file_len {
823 return Err(MCUmgrClientError::SizeMismatch);
824 }
825
826 Ok(())
827 }
828
829 pub fn fs_file_upload<T: Read>(
845 &self,
846 name: impl AsRef<str>,
847 mut reader: T,
848 size: u64,
849 mut progress: Option<&mut dyn FnMut(u64, u64) -> bool>,
850 ) -> Result<(), MCUmgrClientError> {
851 let name = name.as_ref();
852
853 let chunk_size_max = file_upload_max_data_chunk_size(
854 self.smp_frame_size
855 .load(std::sync::atomic::Ordering::SeqCst),
856 name,
857 )
858 .map_err(MCUmgrClientError::FrameSizeTooSmall)?;
859 let mut data_buffer = vec![0u8; chunk_size_max].into_boxed_slice();
860
861 let mut offset = 0;
862
863 while offset < size {
864 let current_chunk_size = (size - offset).min(data_buffer.len() as u64) as usize;
865
866 let chunk_buffer = &mut data_buffer[..current_chunk_size];
867 reader
868 .read_exact(chunk_buffer)
869 .map_err(MCUmgrClientError::ReaderError)?;
870
871 self.connection.execute_command(&commands::fs::FileUpload {
872 off: offset,
873 data: chunk_buffer,
874 name,
875 len: if offset == 0 { Some(size) } else { None },
876 })?;
877
878 offset += chunk_buffer.len() as u64;
879
880 if let Some(progress) = &mut progress {
881 if !progress(offset, size) {
882 return Err(MCUmgrClientError::ProgressCallbackError);
883 };
884 }
885 }
886
887 Ok(())
888 }
889
890 pub fn fs_file_status(
892 &self,
893 name: impl AsRef<str>,
894 ) -> Result<commands::fs::FileStatusResponse, MCUmgrClientError> {
895 self.connection
896 .execute_command(&commands::fs::FileStatus {
897 name: name.as_ref(),
898 })
899 .map_err(Into::into)
900 }
901
902 pub fn fs_file_checksum(
914 &self,
915 name: impl AsRef<str>,
916 algorithm: Option<impl AsRef<str>>,
917 offset: u64,
918 length: Option<u64>,
919 ) -> Result<commands::fs::FileChecksumResponse, MCUmgrClientError> {
920 self.connection
921 .execute_command(&commands::fs::FileChecksum {
922 name: name.as_ref(),
923 r#type: algorithm.as_ref().map(AsRef::as_ref),
924 off: offset,
925 len: length,
926 })
927 .map_err(Into::into)
928 }
929
930 pub fn fs_supported_checksum_types(
932 &self,
933 ) -> Result<HashMap<String, commands::fs::FileChecksumProperties>, MCUmgrClientError> {
934 self.connection
935 .execute_command(&commands::fs::SupportedFileChecksumTypes)
936 .map(|val| val.types)
937 .map_err(Into::into)
938 }
939
940 pub fn fs_file_close(&self) -> Result<(), MCUmgrClientError> {
942 self.connection
943 .execute_command(&commands::fs::FileClose)
944 .map(Into::into)
945 .map_err(Into::into)
946 }
947
948 pub fn shell_execute(
960 &self,
961 argv: &[String],
962 use_retries: bool,
963 ) -> Result<(i32, String), MCUmgrClientError> {
964 let command = commands::shell::ShellCommandLineExecute { argv };
965
966 if use_retries {
967 self.connection.execute_command(&command)
968 } else {
969 self.connection.execute_command_without_retries(&command)
970 }
971 .map(|ret| (ret.ret, ret.o))
972 .map_err(Into::into)
973 }
974
975 pub fn zephyr_erase_storage(&self) -> Result<(), MCUmgrClientError> {
977 self.connection
978 .execute_command(&commands::zephyr::EraseStorage)
979 .map(Into::into)
980 .map_err(Into::into)
981 }
982
983 pub fn raw_command<T: commands::McuMgrCommand>(
989 &self,
990 command: &T,
991 ) -> Result<T::Response, MCUmgrClientError> {
992 self.connection.execute_command(command).map_err(Into::into)
993 }
994}