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>(
231 serial: T,
232 ) -> Self {
233 Self {
234 connection: Connection::new(SerialTransport::new(serial)),
235 smp_frame_size: ZEPHYR_DEFAULT_SMP_FRAME_SIZE.into(),
236 }
237 }
238
239 pub fn new_from_usb_serial(
257 identifier: impl AsRef<str>,
258 baud_rate: u32,
259 timeout: Duration,
260 ) -> Result<Self, UsbSerialError> {
261 let identifier = identifier.as_ref();
262
263 let ports = serialport::available_ports()?
264 .into_iter()
265 .filter_map(|port| {
266 if let serialport::SerialPortType::UsbPort(port_info) = port.port_type {
267 if let Some(interface) = port_info.interface {
268 Some(UsbSerialPortInfo {
269 identifier: format!(
270 "{:04x}:{:04x}:{}",
271 port_info.vid, port_info.pid, interface
272 ),
273 port_name: port.port_name,
274 port_info,
275 })
276 } else {
277 Some(UsbSerialPortInfo {
278 identifier: format!("{:04x}:{:04x}", port_info.vid, port_info.pid),
279 port_name: port.port_name,
280 port_info,
281 })
282 }
283 } else {
284 None
285 }
286 })
287 .collect::<Vec<_>>();
288
289 if identifier.is_empty() {
290 return Err(UsbSerialError::IdentifierEmpty {
291 ports: UsbSerialPorts(ports),
292 });
293 }
294
295 let port_regex = regex::RegexBuilder::new(identifier)
296 .case_insensitive(true)
297 .unicode(true)
298 .build()?;
299
300 let matches = ports
301 .iter()
302 .filter(|port| {
303 if let Some(m) = port_regex.find(&port.identifier) {
304 m.start() == 0
306 } else {
307 false
308 }
309 })
310 .cloned()
311 .collect::<Vec<_>>();
312
313 if matches.len() > 1 {
314 return Err(UsbSerialError::MultipleMatchingPorts {
315 identifier: identifier.to_string(),
316 ports: UsbSerialPorts(matches),
317 });
318 }
319
320 let port_name = match matches.into_iter().next() {
321 Some(port) => port.port_name,
322 None => {
323 return Err(UsbSerialError::NoMatchingPort {
324 identifier: identifier.to_string(),
325 available: UsbSerialPorts(ports),
326 });
327 }
328 };
329
330 let serial = serialport::new(port_name, baud_rate)
331 .timeout(timeout)
332 .open()?;
333
334 Ok(Self::new_from_serial(serial))
335 }
336
337 pub fn set_frame_size(&self, smp_frame_size: usize) {
342 self.smp_frame_size
343 .store(smp_frame_size, std::sync::atomic::Ordering::SeqCst);
344 }
345
346 pub fn use_auto_frame_size(&self) -> Result<(), MCUmgrClientError> {
350 let mcumgr_params = self
351 .connection
352 .execute_command(&commands::os::MCUmgrParameters)?;
353
354 log::debug!("Using frame size {}.", mcumgr_params.buf_size);
355
356 self.smp_frame_size.store(
357 mcumgr_params.buf_size as usize,
358 std::sync::atomic::Ordering::SeqCst,
359 );
360
361 Ok(())
362 }
363
364 pub fn set_timeout(&self, timeout: Duration) -> Result<(), MCUmgrClientError> {
369 self.connection
370 .set_timeout(timeout)
371 .map_err(MCUmgrClientError::SetTimeoutFailed)
372 }
373
374 pub fn check_connection(&self) -> Result<(), MCUmgrClientError> {
382 let random_message = rand::distr::Alphanumeric.sample_string(&mut rand::rng(), 16);
383 let response = self.os_echo(&random_message)?;
384 if random_message == response {
385 Ok(())
386 } else {
387 Err(
388 ExecuteError::ReceiveFailed(crate::transport::ReceiveError::UnexpectedResponse)
389 .into(),
390 )
391 }
392 }
393
394 pub fn firmware_update(
404 &self,
405 firmware: impl AsRef<[u8]>,
406 checksum: Option<[u8; 32]>,
407 params: FirmwareUpdateParams,
408 progress: Option<&mut FirmwareUpdateProgressCallback>,
409 ) -> Result<(), FirmwareUpdateError> {
410 firmware_update::firmware_update(self, firmware, checksum, params, progress)
411 }
412
413 pub fn os_echo(&self, msg: impl AsRef<str>) -> Result<String, MCUmgrClientError> {
417 self.connection
418 .execute_command(&commands::os::Echo { d: msg.as_ref() })
419 .map(|resp| resp.r)
420 .map_err(Into::into)
421 }
422
423 pub fn os_task_statistics(
434 &self,
435 ) -> Result<HashMap<String, commands::os::TaskStatisticsEntry>, MCUmgrClientError> {
436 self.connection
437 .execute_command(&commands::os::TaskStatistics)
438 .map(|resp| {
439 let mut tasks = resp.tasks;
440 for (_, stats) in tasks.iter_mut() {
441 stats.stkuse = stats.stkuse.map(|val| val * 4);
442 stats.stksiz = stats.stksiz.map(|val| val * 4);
443 }
444 tasks
445 })
446 .map_err(Into::into)
447 }
448
449 pub fn os_set_datetime(
451 &self,
452 datetime: chrono::NaiveDateTime,
453 ) -> Result<(), MCUmgrClientError> {
454 self.connection
455 .execute_command(&commands::os::DateTimeSet { datetime })
456 .map(Into::into)
457 .map_err(Into::into)
458 }
459
460 pub fn os_get_datetime(&self) -> Result<chrono::NaiveDateTime, MCUmgrClientError> {
462 self.connection
463 .execute_command(&commands::os::DateTimeGet)
464 .map(|val| val.datetime)
465 .map_err(Into::into)
466 }
467
468 pub fn os_system_reset(
482 &self,
483 force: bool,
484 boot_mode: Option<u8>,
485 ) -> Result<(), MCUmgrClientError> {
486 self.connection
487 .execute_command(&commands::os::SystemReset { force, boot_mode })
488 .map(Into::into)
489 .map_err(Into::into)
490 }
491
492 pub fn os_mcumgr_parameters(
494 &self,
495 ) -> Result<commands::os::MCUmgrParametersResponse, MCUmgrClientError> {
496 self.connection
497 .execute_command(&commands::os::MCUmgrParameters)
498 .map_err(Into::into)
499 }
500
501 pub fn os_application_info(&self, format: Option<&str>) -> Result<String, MCUmgrClientError> {
513 self.connection
514 .execute_command(&commands::os::ApplicationInfo { format })
515 .map(|resp| resp.output)
516 .map_err(Into::into)
517 }
518
519 pub fn os_bootloader_info(&self) -> Result<BootloaderInfo, MCUmgrClientError> {
521 Ok(
522 match self
523 .connection
524 .execute_command(&commands::os::BootloaderInfo)?
525 .bootloader
526 .as_str()
527 {
528 "MCUboot" => {
529 let mode_data = self
530 .connection
531 .execute_command(&commands::os::BootloaderInfoMcubootMode {})?;
532 BootloaderInfo::MCUboot {
533 mode: mode_data.mode,
534 no_downgrade: mode_data.no_downgrade,
535 }
536 }
537 name => BootloaderInfo::Unknown {
538 name: name.to_string(),
539 },
540 },
541 )
542 }
543
544 pub fn image_get_state(&self) -> Result<Vec<commands::image::ImageState>, MCUmgrClientError> {
546 self.connection
547 .execute_command(&commands::image::GetImageState)
548 .map(|val| val.images)
549 .map_err(Into::into)
550 }
551
552 pub fn image_set_state(
568 &self,
569 hash: Option<[u8; 32]>,
570 confirm: bool,
571 ) -> Result<Vec<commands::image::ImageState>, MCUmgrClientError> {
572 self.connection
573 .execute_command(&commands::image::SetImageState {
574 hash: hash.as_ref(),
575 confirm,
576 })
577 .map(|val| val.images)
578 .map_err(Into::into)
579 }
580
581 pub fn image_upload(
592 &self,
593 data: impl AsRef<[u8]>,
594 image: Option<u32>,
595 checksum: Option<[u8; 32]>,
596 upgrade_only: bool,
597 mut progress: Option<&mut dyn FnMut(u64, u64) -> bool>,
598 ) -> Result<(), MCUmgrClientError> {
599 let chunk_size_max = image_upload_max_data_chunk_size(
600 self.smp_frame_size
601 .load(std::sync::atomic::Ordering::SeqCst),
602 )
603 .map_err(MCUmgrClientError::FrameSizeTooSmall)?;
604
605 let data = data.as_ref();
606
607 let actual_checksum: [u8; 32] = Sha256::digest(data).into();
608 if let Some(checksum) = checksum {
609 if actual_checksum != checksum {
610 return Err(MCUmgrClientError::ChecksumMismatch);
611 }
612 }
613
614 let mut offset = 0;
615 let size = data.len();
616
617 let mut checksum_matched = None;
618
619 while offset < size {
620 let current_chunk_size = (size - offset).min(chunk_size_max);
621 let chunk_data = &data[offset..offset + current_chunk_size];
622
623 let upload_response = if offset == 0 {
624 self.connection
625 .execute_command(&commands::image::ImageUpload {
626 image,
627 len: Some(size as u64),
628 off: offset as u64,
629 sha: Some(&actual_checksum),
630 data: chunk_data,
631 upgrade: Some(upgrade_only),
632 })?
633 } else {
634 self.connection
635 .execute_command(&commands::image::ImageUpload {
636 image: None,
637 len: None,
638 off: offset as u64,
639 sha: None,
640 data: chunk_data,
641 upgrade: None,
642 })?
643 };
644
645 offset = upload_response
646 .off
647 .try_into()
648 .map_err(|_| MCUmgrClientError::UnexpectedOffset)?;
649
650 if offset > size {
651 return Err(MCUmgrClientError::UnexpectedOffset);
652 }
653
654 if let Some(progress) = &mut progress {
655 if !progress(offset as u64, size as u64) {
656 return Err(MCUmgrClientError::ProgressCallbackError);
657 };
658 }
659
660 if let Some(is_match) = upload_response.r#match {
661 checksum_matched = Some(is_match);
662 }
663 }
664
665 if let Some(checksum_matched) = checksum_matched {
666 if !checksum_matched {
667 return Err(MCUmgrClientError::ChecksumMismatchOnDevice);
668 }
669 } else {
670 log::warn!("Device did not perform image checksum verification");
671 }
672
673 Ok(())
674 }
675
676 pub fn image_erase(&self, slot: Option<u32>) -> Result<(), MCUmgrClientError> {
683 self.connection
684 .execute_command(&commands::image::ImageErase { slot })
685 .map(Into::into)
686 .map_err(Into::into)
687 }
688
689 pub fn image_slot_info(
691 &self,
692 ) -> Result<Vec<commands::image::SlotInfoImage>, MCUmgrClientError> {
693 self.connection
694 .execute_command(&commands::image::SlotInfo)
695 .map(|val| val.images)
696 .map_err(Into::into)
697 }
698
699 pub fn fs_file_download<T: Write>(
713 &self,
714 name: impl AsRef<str>,
715 mut writer: T,
716 mut progress: Option<&mut dyn FnMut(u64, u64) -> bool>,
717 ) -> Result<(), MCUmgrClientError> {
718 let name = name.as_ref();
719 let response = self
720 .connection
721 .execute_command(&commands::fs::FileDownload { name, off: 0 })?;
722
723 let file_len = response.len.ok_or(MCUmgrClientError::MissingSize)?;
724 if response.off != 0 {
725 return Err(MCUmgrClientError::UnexpectedOffset);
726 }
727
728 let mut offset = 0;
729
730 if let Some(progress) = &mut progress {
731 if !progress(offset, file_len) {
732 return Err(MCUmgrClientError::ProgressCallbackError);
733 };
734 }
735
736 writer
737 .write_all(&response.data)
738 .map_err(MCUmgrClientError::WriterError)?;
739 offset += response.data.len() as u64;
740
741 if let Some(progress) = &mut progress {
742 if !progress(offset, file_len) {
743 return Err(MCUmgrClientError::ProgressCallbackError);
744 };
745 }
746
747 while offset < file_len {
748 let response = self
749 .connection
750 .execute_command(&commands::fs::FileDownload { name, off: offset })?;
751
752 if response.off != offset {
753 return Err(MCUmgrClientError::UnexpectedOffset);
754 }
755
756 writer
757 .write_all(&response.data)
758 .map_err(MCUmgrClientError::WriterError)?;
759 offset += response.data.len() as u64;
760
761 if let Some(progress) = &mut progress {
762 if !progress(offset, file_len) {
763 return Err(MCUmgrClientError::ProgressCallbackError);
764 };
765 }
766 }
767
768 if offset != file_len {
769 return Err(MCUmgrClientError::SizeMismatch);
770 }
771
772 Ok(())
773 }
774
775 pub fn fs_file_upload<T: Read>(
791 &self,
792 name: impl AsRef<str>,
793 mut reader: T,
794 size: u64,
795 mut progress: Option<&mut dyn FnMut(u64, u64) -> bool>,
796 ) -> Result<(), MCUmgrClientError> {
797 let name = name.as_ref();
798
799 let chunk_size_max = file_upload_max_data_chunk_size(
800 self.smp_frame_size
801 .load(std::sync::atomic::Ordering::SeqCst),
802 name,
803 )
804 .map_err(MCUmgrClientError::FrameSizeTooSmall)?;
805 let mut data_buffer = vec![0u8; chunk_size_max].into_boxed_slice();
806
807 let mut offset = 0;
808
809 while offset < size {
810 let current_chunk_size = (size - offset).min(data_buffer.len() as u64) as usize;
811
812 let chunk_buffer = &mut data_buffer[..current_chunk_size];
813 reader
814 .read_exact(chunk_buffer)
815 .map_err(MCUmgrClientError::ReaderError)?;
816
817 self.connection.execute_command(&commands::fs::FileUpload {
818 off: offset,
819 data: chunk_buffer,
820 name,
821 len: if offset == 0 { Some(size) } else { None },
822 })?;
823
824 offset += chunk_buffer.len() as u64;
825
826 if let Some(progress) = &mut progress {
827 if !progress(offset, size) {
828 return Err(MCUmgrClientError::ProgressCallbackError);
829 };
830 }
831 }
832
833 Ok(())
834 }
835
836 pub fn fs_file_status(
838 &self,
839 name: impl AsRef<str>,
840 ) -> Result<commands::fs::FileStatusResponse, MCUmgrClientError> {
841 self.connection
842 .execute_command(&commands::fs::FileStatus {
843 name: name.as_ref(),
844 })
845 .map_err(Into::into)
846 }
847
848 pub fn fs_file_checksum(
860 &self,
861 name: impl AsRef<str>,
862 algorithm: Option<impl AsRef<str>>,
863 offset: u64,
864 length: Option<u64>,
865 ) -> Result<commands::fs::FileChecksumResponse, MCUmgrClientError> {
866 self.connection
867 .execute_command(&commands::fs::FileChecksum {
868 name: name.as_ref(),
869 r#type: algorithm.as_ref().map(AsRef::as_ref),
870 off: offset,
871 len: length,
872 })
873 .map_err(Into::into)
874 }
875
876 pub fn fs_supported_checksum_types(
878 &self,
879 ) -> Result<HashMap<String, commands::fs::FileChecksumProperties>, MCUmgrClientError> {
880 self.connection
881 .execute_command(&commands::fs::SupportedFileChecksumTypes)
882 .map(|val| val.types)
883 .map_err(Into::into)
884 }
885
886 pub fn fs_file_close(&self) -> Result<(), MCUmgrClientError> {
888 self.connection
889 .execute_command(&commands::fs::FileClose)
890 .map(Into::into)
891 .map_err(Into::into)
892 }
893
894 pub fn shell_execute(&self, argv: &[String]) -> Result<(i32, String), MCUmgrClientError> {
904 self.connection
905 .execute_command(&commands::shell::ShellCommandLineExecute { argv })
906 .map(|ret| (ret.ret, ret.o))
907 .map_err(Into::into)
908 }
909
910 pub fn zephyr_erase_storage(&self) -> Result<(), MCUmgrClientError> {
912 self.connection
913 .execute_command(&commands::zephyr::EraseStorage)
914 .map(Into::into)
915 .map_err(Into::into)
916 }
917
918 pub fn raw_command<T: commands::McuMgrCommand>(
924 &self,
925 command: &T,
926 ) -> Result<T::Response, MCUmgrClientError> {
927 self.connection.execute_command(command).map_err(Into::into)
928 }
929}