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(
602 &self,
603 data: impl AsRef<[u8]>,
604 image: Option<u32>,
605 checksum: Option<[u8; 32]>,
606 upgrade_only: bool,
607 mut progress: Option<&mut dyn FnMut(u64, u64) -> bool>,
608 ) -> Result<(), MCUmgrClientError> {
609 let chunk_size_max = image_upload_max_data_chunk_size(
610 self.smp_frame_size
611 .load(std::sync::atomic::Ordering::SeqCst),
612 )
613 .map_err(MCUmgrClientError::FrameSizeTooSmall)?;
614
615 let data = data.as_ref();
616
617 let actual_checksum: [u8; 32] = Sha256::digest(data).into();
618 if let Some(checksum) = checksum {
619 if actual_checksum != checksum {
620 return Err(MCUmgrClientError::ChecksumMismatch);
621 }
622 }
623
624 let mut offset = 0;
625 let size = data.len();
626
627 let mut checksum_matched = None;
628
629 while offset < size {
630 let current_chunk_size = (size - offset).min(chunk_size_max);
631 let chunk_data = &data[offset..offset + current_chunk_size];
632
633 let upload_response = if offset == 0 {
634 let result = self
635 .connection
636 .execute_command(&commands::image::ImageUpload {
637 image,
638 len: Some(size as u64),
639 off: offset as u64,
640 sha: Some(&actual_checksum),
641 data: chunk_data,
642 upgrade: Some(upgrade_only),
643 });
644
645 if let Err(ExecuteError::ReceiveFailed(ReceiveError::TransportError(e))) = &result {
646 if let io::ErrorKind::TimedOut = e.kind() {
647 log::warn!(
648 "Timed out during transfer of first chunk. Consider enabling CONFIG_IMG_ERASE_PROGRESSIVELY."
649 )
650 }
651 }
652
653 result?
654 } else {
655 self.connection
656 .execute_command(&commands::image::ImageUpload {
657 image: None,
658 len: None,
659 off: offset as u64,
660 sha: None,
661 data: chunk_data,
662 upgrade: None,
663 })?
664 };
665
666 offset = upload_response
667 .off
668 .try_into()
669 .map_err(|_| MCUmgrClientError::UnexpectedOffset)?;
670
671 if offset > size {
672 return Err(MCUmgrClientError::UnexpectedOffset);
673 }
674
675 if let Some(progress) = &mut progress {
676 if !progress(offset as u64, size as u64) {
677 return Err(MCUmgrClientError::ProgressCallbackError);
678 };
679 }
680
681 if let Some(is_match) = upload_response.r#match {
682 checksum_matched = Some(is_match);
683 }
684 }
685
686 if let Some(checksum_matched) = checksum_matched {
687 if !checksum_matched {
688 return Err(MCUmgrClientError::ChecksumMismatchOnDevice);
689 }
690 } else {
691 log::warn!("Device did not perform image checksum verification");
692 }
693
694 Ok(())
695 }
696
697 pub fn image_erase(&self, slot: Option<u32>) -> Result<(), MCUmgrClientError> {
704 self.connection
705 .execute_command(&commands::image::ImageErase { slot })
706 .map(Into::into)
707 .map_err(Into::into)
708 }
709
710 pub fn image_slot_info(
712 &self,
713 ) -> Result<Vec<commands::image::SlotInfoImage>, MCUmgrClientError> {
714 self.connection
715 .execute_command(&commands::image::SlotInfo)
716 .map(|val| val.images)
717 .map_err(Into::into)
718 }
719
720 pub fn fs_file_download<T: Write>(
734 &self,
735 name: impl AsRef<str>,
736 mut writer: T,
737 mut progress: Option<&mut dyn FnMut(u64, u64) -> bool>,
738 ) -> Result<(), MCUmgrClientError> {
739 let name = name.as_ref();
740 let response = self
741 .connection
742 .execute_command(&commands::fs::FileDownload { name, off: 0 })?;
743
744 let file_len = response.len.ok_or(MCUmgrClientError::MissingSize)?;
745 if response.off != 0 {
746 return Err(MCUmgrClientError::UnexpectedOffset);
747 }
748
749 let mut offset = 0;
750
751 if let Some(progress) = &mut progress {
752 if !progress(offset, file_len) {
753 return Err(MCUmgrClientError::ProgressCallbackError);
754 };
755 }
756
757 writer
758 .write_all(&response.data)
759 .map_err(MCUmgrClientError::WriterError)?;
760 offset += response.data.len() as u64;
761
762 if let Some(progress) = &mut progress {
763 if !progress(offset, file_len) {
764 return Err(MCUmgrClientError::ProgressCallbackError);
765 };
766 }
767
768 while offset < file_len {
769 let response = self
770 .connection
771 .execute_command(&commands::fs::FileDownload { name, off: offset })?;
772
773 if response.off != offset {
774 return Err(MCUmgrClientError::UnexpectedOffset);
775 }
776
777 writer
778 .write_all(&response.data)
779 .map_err(MCUmgrClientError::WriterError)?;
780 offset += response.data.len() as u64;
781
782 if let Some(progress) = &mut progress {
783 if !progress(offset, file_len) {
784 return Err(MCUmgrClientError::ProgressCallbackError);
785 };
786 }
787 }
788
789 if offset != file_len {
790 return Err(MCUmgrClientError::SizeMismatch);
791 }
792
793 Ok(())
794 }
795
796 pub fn fs_file_upload<T: Read>(
812 &self,
813 name: impl AsRef<str>,
814 mut reader: T,
815 size: u64,
816 mut progress: Option<&mut dyn FnMut(u64, u64) -> bool>,
817 ) -> Result<(), MCUmgrClientError> {
818 let name = name.as_ref();
819
820 let chunk_size_max = file_upload_max_data_chunk_size(
821 self.smp_frame_size
822 .load(std::sync::atomic::Ordering::SeqCst),
823 name,
824 )
825 .map_err(MCUmgrClientError::FrameSizeTooSmall)?;
826 let mut data_buffer = vec![0u8; chunk_size_max].into_boxed_slice();
827
828 let mut offset = 0;
829
830 while offset < size {
831 let current_chunk_size = (size - offset).min(data_buffer.len() as u64) as usize;
832
833 let chunk_buffer = &mut data_buffer[..current_chunk_size];
834 reader
835 .read_exact(chunk_buffer)
836 .map_err(MCUmgrClientError::ReaderError)?;
837
838 self.connection.execute_command(&commands::fs::FileUpload {
839 off: offset,
840 data: chunk_buffer,
841 name,
842 len: if offset == 0 { Some(size) } else { None },
843 })?;
844
845 offset += chunk_buffer.len() as u64;
846
847 if let Some(progress) = &mut progress {
848 if !progress(offset, size) {
849 return Err(MCUmgrClientError::ProgressCallbackError);
850 };
851 }
852 }
853
854 Ok(())
855 }
856
857 pub fn fs_file_status(
859 &self,
860 name: impl AsRef<str>,
861 ) -> Result<commands::fs::FileStatusResponse, MCUmgrClientError> {
862 self.connection
863 .execute_command(&commands::fs::FileStatus {
864 name: name.as_ref(),
865 })
866 .map_err(Into::into)
867 }
868
869 pub fn fs_file_checksum(
881 &self,
882 name: impl AsRef<str>,
883 algorithm: Option<impl AsRef<str>>,
884 offset: u64,
885 length: Option<u64>,
886 ) -> Result<commands::fs::FileChecksumResponse, MCUmgrClientError> {
887 self.connection
888 .execute_command(&commands::fs::FileChecksum {
889 name: name.as_ref(),
890 r#type: algorithm.as_ref().map(AsRef::as_ref),
891 off: offset,
892 len: length,
893 })
894 .map_err(Into::into)
895 }
896
897 pub fn fs_supported_checksum_types(
899 &self,
900 ) -> Result<HashMap<String, commands::fs::FileChecksumProperties>, MCUmgrClientError> {
901 self.connection
902 .execute_command(&commands::fs::SupportedFileChecksumTypes)
903 .map(|val| val.types)
904 .map_err(Into::into)
905 }
906
907 pub fn fs_file_close(&self) -> Result<(), MCUmgrClientError> {
909 self.connection
910 .execute_command(&commands::fs::FileClose)
911 .map(Into::into)
912 .map_err(Into::into)
913 }
914
915 pub fn shell_execute(
927 &self,
928 argv: &[String],
929 use_retries: bool,
930 ) -> Result<(i32, String), MCUmgrClientError> {
931 let command = commands::shell::ShellCommandLineExecute { argv };
932
933 if use_retries {
934 self.connection.execute_command(&command)
935 } else {
936 self.connection.execute_command_without_retries(&command)
937 }
938 .map(|ret| (ret.ret, ret.o))
939 .map_err(Into::into)
940 }
941
942 pub fn zephyr_erase_storage(&self) -> Result<(), MCUmgrClientError> {
944 self.connection
945 .execute_command(&commands::zephyr::EraseStorage)
946 .map(Into::into)
947 .map_err(Into::into)
948 }
949
950 pub fn raw_command<T: commands::McuMgrCommand>(
956 &self,
957 command: &T,
958 ) -> Result<T::Response, MCUmgrClientError> {
959 self.connection.execute_command(command).map_err(Into::into)
960 }
961}