1mod firmware_update;
3
4pub use firmware_update::{
5 FirmwareUpdateError, FirmwareUpdateParams, FirmwareUpdateProgressCallback,
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 FileDownloadError {
46 #[error("Command execution failed")]
48 #[diagnostic(code(mcumgr_toolkit::client::file_download::execute))]
49 ExecuteError(#[from] ExecuteError),
50 #[error("Received offset does not match requested offset")]
52 #[diagnostic(code(mcumgr_toolkit::client::file_download::offset_mismatch))]
53 UnexpectedOffset,
54 #[error("Writer returned an error")]
56 #[diagnostic(code(mcumgr_toolkit::client::file_download::writer))]
57 WriterError(#[from] io::Error),
58 #[error("Received data does not match reported size")]
60 #[diagnostic(code(mcumgr_toolkit::client::file_download::size_mismatch))]
61 SizeMismatch,
62 #[error("Received data is missing file size information")]
64 #[diagnostic(code(mcumgr_toolkit::client::file_download::missing_size))]
65 MissingSize,
66 #[error("Progress callback returned an error")]
68 #[diagnostic(code(mcumgr_toolkit::client::file_download::progress_cb_error))]
69 ProgressCallbackError,
70}
71
72#[derive(Error, Debug, Diagnostic)]
74pub enum FileUploadError {
75 #[error("Command execution failed")]
77 #[diagnostic(code(mcumgr_toolkit::client::file_upload::execute))]
78 ExecuteError(#[from] ExecuteError),
79 #[error("Reader returned an error")]
81 #[diagnostic(code(mcumgr_toolkit::client::file_upload::reader))]
82 ReaderError(#[from] io::Error),
83 #[error("Progress callback returned an error")]
85 #[diagnostic(code(mcumgr_toolkit::client::file_upload::progress_cb_error))]
86 ProgressCallbackError,
87 #[error("SMP frame size too small for this command")]
89 #[diagnostic(code(mcumgr_toolkit::client::file_upload::framesize_too_small))]
90 FrameSizeTooSmall(#[source] io::Error),
91}
92
93#[derive(Error, Debug, Diagnostic)]
95pub enum ImageUploadError {
96 #[error("Command execution failed")]
98 #[diagnostic(code(mcumgr_toolkit::client::image_upload::execute))]
99 ExecuteError(#[from] ExecuteError),
100 #[error("Progress callback returned an error")]
102 #[diagnostic(code(mcumgr_toolkit::client::image_upload::progress_cb_error))]
103 ProgressCallbackError,
104 #[error("SMP frame size too small for this command")]
106 #[diagnostic(code(mcumgr_toolkit::client::image_upload::framesize_too_small))]
107 FrameSizeTooSmall(#[source] io::Error),
108 #[error("Received offset out of expected range")]
110 #[diagnostic(code(mcumgr_toolkit::client::image_upload::invalid_offset))]
111 UnexpectedOffset,
112 #[error("Device reported checksum mismatch")]
114 #[diagnostic(code(mcumgr_toolkit::client::image_upload::checksum_mismatch_on_device))]
115 ChecksumMismatchOnDevice,
116 #[error("Firmware image does not match given checksum")]
118 #[diagnostic(code(mcumgr_toolkit::client::image_upload::checksum_mismatch))]
119 ChecksumMismatch,
120}
121
122#[derive(Debug, Serialize, Clone, Eq, PartialEq)]
124pub struct UsbSerialPortInfo {
125 pub identifier: String,
127 pub port_name: String,
129 pub port_info: serialport::UsbPortInfo,
131}
132
133#[derive(Serialize, Clone, Eq, PartialEq)]
137#[serde(transparent)]
138pub struct UsbSerialPorts(pub Vec<UsbSerialPortInfo>);
139impl std::fmt::Display for UsbSerialPorts {
140 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141 if self.0.is_empty() {
142 writeln!(f)?;
143 write!(f, " - None -")?;
144 return Ok(());
145 }
146
147 for UsbSerialPortInfo {
148 identifier,
149 port_name,
150 port_info,
151 } in &self.0
152 {
153 writeln!(f)?;
154 write!(f, " - {identifier}")?;
155
156 let mut print_port_string = true;
157 let port_string = format!("({port_name})");
158
159 if port_info.manufacturer.is_some() || port_info.product.is_some() {
160 write!(f, " -")?;
161 if let Some(manufacturer) = &port_info.manufacturer {
162 let mut print_manufacturer = true;
163
164 if let Some(product) = &port_info.product {
165 if product.starts_with(manufacturer) {
166 print_manufacturer = false;
167 }
168 }
169
170 if print_manufacturer {
171 write!(f, " {manufacturer}")?;
172 }
173 }
174 if let Some(product) = &port_info.product {
175 write!(f, " {product}")?;
176
177 if product.ends_with(&port_string) {
178 print_port_string = false;
179 }
180 }
181 }
182
183 if print_port_string {
184 write!(f, " {port_string}")?;
185 }
186 }
187 Ok(())
188 }
189}
190impl std::fmt::Debug for UsbSerialPorts {
191 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
192 std::fmt::Debug::fmt(&self.0, f)
193 }
194}
195
196#[derive(Error, Debug, Diagnostic)]
198pub enum UsbSerialError {
199 #[error("Serialport returned an error")]
201 #[diagnostic(code(mcumgr_toolkit::usb_serial::serialport_error))]
202 SerialPortError(#[from] serialport::Error),
203 #[error("No serial port matched the identifier '{identifier}'\nAvailable ports:\n{available}")]
205 #[diagnostic(code(mcumgr_toolkit::usb_serial::no_matches))]
206 NoMatchingPort {
207 identifier: String,
209 available: UsbSerialPorts,
211 },
212 #[error("Multiple serial ports matched the identifier '{identifier}'\n{ports}")]
214 #[diagnostic(code(mcumgr_toolkit::usb_serial::multiple_matches))]
215 MultipleMatchingPorts {
216 identifier: String,
218 ports: UsbSerialPorts,
220 },
221 #[error("An empty identifier was provided")]
224 #[diagnostic(code(mcumgr_toolkit::usb_serial::empty_identifier))]
225 IdentifierEmpty {
226 ports: UsbSerialPorts,
228 },
229 #[error("The given identifier was not a valid RegEx")]
231 #[diagnostic(code(mcumgr_toolkit::usb_serial::regex_error))]
232 RegexError(#[from] regex::Error),
233}
234
235impl MCUmgrClient {
236 pub fn new_from_serial<T: Send + Read + Write + ConfigurableTimeout + 'static>(
250 serial: T,
251 ) -> Self {
252 Self {
253 connection: Connection::new(SerialTransport::new(serial)),
254 smp_frame_size: ZEPHYR_DEFAULT_SMP_FRAME_SIZE.into(),
255 }
256 }
257
258 pub fn new_from_usb_serial(
276 identifier: impl AsRef<str>,
277 baud_rate: u32,
278 timeout: Duration,
279 ) -> Result<Self, UsbSerialError> {
280 let identifier = identifier.as_ref();
281
282 let ports = serialport::available_ports()?
283 .into_iter()
284 .filter_map(|port| {
285 if let serialport::SerialPortType::UsbPort(port_info) = port.port_type {
286 if let Some(interface) = port_info.interface {
287 Some(UsbSerialPortInfo {
288 identifier: format!(
289 "{:04x}:{:04x}:{}",
290 port_info.vid, port_info.pid, interface
291 ),
292 port_name: port.port_name,
293 port_info,
294 })
295 } else {
296 Some(UsbSerialPortInfo {
297 identifier: format!("{:04x}:{:04x}", port_info.vid, port_info.pid),
298 port_name: port.port_name,
299 port_info,
300 })
301 }
302 } else {
303 None
304 }
305 })
306 .collect::<Vec<_>>();
307
308 if identifier.is_empty() {
309 return Err(UsbSerialError::IdentifierEmpty {
310 ports: UsbSerialPorts(ports),
311 });
312 }
313
314 let port_regex = regex::RegexBuilder::new(identifier)
315 .case_insensitive(true)
316 .unicode(true)
317 .build()?;
318
319 let matches = ports
320 .iter()
321 .filter(|port| {
322 if let Some(m) = port_regex.find(&port.identifier) {
323 m.start() == 0
325 } else {
326 false
327 }
328 })
329 .cloned()
330 .collect::<Vec<_>>();
331
332 if matches.len() > 1 {
333 return Err(UsbSerialError::MultipleMatchingPorts {
334 identifier: identifier.to_string(),
335 ports: UsbSerialPorts(matches),
336 });
337 }
338
339 let port_name = match matches.into_iter().next() {
340 Some(port) => port.port_name,
341 None => {
342 return Err(UsbSerialError::NoMatchingPort {
343 identifier: identifier.to_string(),
344 available: UsbSerialPorts(ports),
345 });
346 }
347 };
348
349 let serial = serialport::new(port_name, baud_rate)
350 .timeout(timeout)
351 .open()?;
352
353 Ok(Self::new_from_serial(serial))
354 }
355
356 pub fn set_frame_size(&self, smp_frame_size: usize) {
361 self.smp_frame_size
362 .store(smp_frame_size, std::sync::atomic::Ordering::SeqCst);
363 }
364
365 pub fn use_auto_frame_size(&self) -> Result<(), ExecuteError> {
369 let mcumgr_params = self
370 .connection
371 .execute_command(&commands::os::MCUmgrParameters)?;
372
373 log::debug!("Using frame size {}.", mcumgr_params.buf_size);
374
375 self.smp_frame_size.store(
376 mcumgr_params.buf_size as usize,
377 std::sync::atomic::Ordering::SeqCst,
378 );
379
380 Ok(())
381 }
382
383 pub fn set_timeout(&self, timeout: Duration) -> Result<(), miette::Report> {
388 self.connection.set_timeout(timeout)
389 }
390
391 pub fn check_connection(&self) -> Result<(), ExecuteError> {
399 let random_message = rand::distr::Alphanumeric.sample_string(&mut rand::rng(), 16);
400 let response = self.os_echo(&random_message)?;
401 if random_message == response {
402 Ok(())
403 } else {
404 Err(ExecuteError::ReceiveFailed(
405 crate::transport::ReceiveError::UnexpectedResponse,
406 ))
407 }
408 }
409
410 pub fn firmware_update(
420 &self,
421 firmware: impl AsRef<[u8]>,
422 checksum: Option<[u8; 32]>,
423 params: FirmwareUpdateParams,
424 progress: Option<&mut FirmwareUpdateProgressCallback>,
425 ) -> Result<(), FirmwareUpdateError> {
426 firmware_update::firmware_update(self, firmware, checksum, params, progress)
427 }
428
429 pub fn os_echo(&self, msg: impl AsRef<str>) -> Result<String, ExecuteError> {
433 self.connection
434 .execute_command(&commands::os::Echo { d: msg.as_ref() })
435 .map(|resp| resp.r)
436 }
437
438 pub fn os_task_statistics(
449 &self,
450 ) -> Result<HashMap<String, commands::os::TaskStatisticsEntry>, ExecuteError> {
451 self.connection
452 .execute_command(&commands::os::TaskStatistics)
453 .map(|resp| {
454 let mut tasks = resp.tasks;
455 for (_, stats) in tasks.iter_mut() {
456 stats.stkuse = stats.stkuse.map(|val| val * 4);
457 stats.stksiz = stats.stksiz.map(|val| val * 4);
458 }
459 tasks
460 })
461 }
462
463 pub fn os_set_datetime(&self, datetime: chrono::NaiveDateTime) -> Result<(), ExecuteError> {
465 self.connection
466 .execute_command(&commands::os::DateTimeSet { datetime })
467 .map(Into::into)
468 }
469
470 pub fn os_get_datetime(&self) -> Result<chrono::NaiveDateTime, ExecuteError> {
472 self.connection
473 .execute_command(&commands::os::DateTimeGet)
474 .map(|val| val.datetime)
475 }
476
477 pub fn os_system_reset(&self, force: bool, boot_mode: Option<u8>) -> Result<(), ExecuteError> {
491 self.connection
492 .execute_command(&commands::os::SystemReset { force, boot_mode })
493 .map(Into::into)
494 }
495
496 pub fn os_mcumgr_parameters(
498 &self,
499 ) -> Result<commands::os::MCUmgrParametersResponse, ExecuteError> {
500 self.connection
501 .execute_command(&commands::os::MCUmgrParameters)
502 }
503
504 pub fn os_application_info(&self, format: Option<&str>) -> Result<String, ExecuteError> {
516 self.connection
517 .execute_command(&commands::os::ApplicationInfo { format })
518 .map(|resp| resp.output)
519 }
520
521 pub fn os_bootloader_info(&self) -> Result<BootloaderInfo, ExecuteError> {
523 Ok(
524 match self
525 .connection
526 .execute_command(&commands::os::BootloaderInfo)?
527 .bootloader
528 .as_str()
529 {
530 "MCUboot" => {
531 let mode_data = self
532 .connection
533 .execute_command(&commands::os::BootloaderInfoMcubootMode {})?;
534 BootloaderInfo::MCUboot {
535 mode: mode_data.mode,
536 no_downgrade: mode_data.no_downgrade,
537 }
538 }
539 name => BootloaderInfo::Unknown {
540 name: name.to_string(),
541 },
542 },
543 )
544 }
545
546 pub fn image_get_state(&self) -> Result<Vec<commands::image::ImageState>, ExecuteError> {
548 self.connection
549 .execute_command(&commands::image::GetImageState)
550 .map(|val| val.images)
551 }
552
553 pub fn image_set_state(
569 &self,
570 hash: Option<[u8; 32]>,
571 confirm: bool,
572 ) -> Result<Vec<commands::image::ImageState>, ExecuteError> {
573 self.connection
574 .execute_command(&commands::image::SetImageState {
575 hash: hash.as_ref(),
576 confirm,
577 })
578 .map(|val| val.images)
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<(), ImageUploadError> {
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(ImageUploadError::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(ImageUploadError::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(|_| ImageUploadError::UnexpectedOffset)?;
649
650 if offset > size {
651 return Err(ImageUploadError::UnexpectedOffset);
652 }
653
654 if let Some(progress) = &mut progress {
655 if !progress(offset as u64, size as u64) {
656 return Err(ImageUploadError::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(ImageUploadError::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<(), ExecuteError> {
683 self.connection
684 .execute_command(&commands::image::ImageErase { slot })
685 .map(Into::into)
686 }
687
688 pub fn image_slot_info(&self) -> Result<Vec<commands::image::SlotInfoImage>, ExecuteError> {
690 self.connection
691 .execute_command(&commands::image::SlotInfo)
692 .map(|val| val.images)
693 }
694
695 pub fn fs_file_download<T: Write>(
709 &self,
710 name: impl AsRef<str>,
711 mut writer: T,
712 mut progress: Option<&mut dyn FnMut(u64, u64) -> bool>,
713 ) -> Result<(), FileDownloadError> {
714 let name = name.as_ref();
715 let response = self
716 .connection
717 .execute_command(&commands::fs::FileDownload { name, off: 0 })?;
718
719 let file_len = response.len.ok_or(FileDownloadError::MissingSize)?;
720 if response.off != 0 {
721 return Err(FileDownloadError::UnexpectedOffset);
722 }
723
724 let mut offset = 0;
725
726 if let Some(progress) = &mut progress {
727 if !progress(offset, file_len) {
728 return Err(FileDownloadError::ProgressCallbackError);
729 };
730 }
731
732 writer.write_all(&response.data)?;
733 offset += response.data.len() as u64;
734
735 if let Some(progress) = &mut progress {
736 if !progress(offset, file_len) {
737 return Err(FileDownloadError::ProgressCallbackError);
738 };
739 }
740
741 while offset < file_len {
742 let response = self
743 .connection
744 .execute_command(&commands::fs::FileDownload { name, off: offset })?;
745
746 if response.off != offset {
747 return Err(FileDownloadError::UnexpectedOffset);
748 }
749
750 writer.write_all(&response.data)?;
751 offset += response.data.len() as u64;
752
753 if let Some(progress) = &mut progress {
754 if !progress(offset, file_len) {
755 return Err(FileDownloadError::ProgressCallbackError);
756 };
757 }
758 }
759
760 if offset != file_len {
761 return Err(FileDownloadError::SizeMismatch);
762 }
763
764 Ok(())
765 }
766
767 pub fn fs_file_upload<T: Read>(
783 &self,
784 name: impl AsRef<str>,
785 mut reader: T,
786 size: u64,
787 mut progress: Option<&mut dyn FnMut(u64, u64) -> bool>,
788 ) -> Result<(), FileUploadError> {
789 let name = name.as_ref();
790
791 let chunk_size_max = file_upload_max_data_chunk_size(
792 self.smp_frame_size
793 .load(std::sync::atomic::Ordering::SeqCst),
794 name,
795 )
796 .map_err(FileUploadError::FrameSizeTooSmall)?;
797 let mut data_buffer = vec![0u8; chunk_size_max].into_boxed_slice();
798
799 let mut offset = 0;
800
801 while offset < size {
802 let current_chunk_size = (size - offset).min(data_buffer.len() as u64) as usize;
803
804 let chunk_buffer = &mut data_buffer[..current_chunk_size];
805 reader.read_exact(chunk_buffer)?;
806
807 self.connection.execute_command(&commands::fs::FileUpload {
808 off: offset,
809 data: chunk_buffer,
810 name,
811 len: if offset == 0 { Some(size) } else { None },
812 })?;
813
814 offset += chunk_buffer.len() as u64;
815
816 if let Some(progress) = &mut progress {
817 if !progress(offset, size) {
818 return Err(FileUploadError::ProgressCallbackError);
819 };
820 }
821 }
822
823 Ok(())
824 }
825
826 pub fn fs_file_status(
828 &self,
829 name: impl AsRef<str>,
830 ) -> Result<commands::fs::FileStatusResponse, ExecuteError> {
831 self.connection.execute_command(&commands::fs::FileStatus {
832 name: name.as_ref(),
833 })
834 }
835
836 pub fn fs_file_checksum(
848 &self,
849 name: impl AsRef<str>,
850 algorithm: Option<impl AsRef<str>>,
851 offset: u64,
852 length: Option<u64>,
853 ) -> Result<commands::fs::FileChecksumResponse, ExecuteError> {
854 self.connection
855 .execute_command(&commands::fs::FileChecksum {
856 name: name.as_ref(),
857 r#type: algorithm.as_ref().map(AsRef::as_ref),
858 off: offset,
859 len: length,
860 })
861 }
862
863 pub fn fs_supported_checksum_types(
865 &self,
866 ) -> Result<HashMap<String, commands::fs::FileChecksumProperties>, ExecuteError> {
867 self.connection
868 .execute_command(&commands::fs::SupportedFileChecksumTypes)
869 .map(|val| val.types)
870 }
871
872 pub fn fs_file_close(&self) -> Result<(), ExecuteError> {
874 self.connection
875 .execute_command(&commands::fs::FileClose)
876 .map(Into::into)
877 }
878
879 pub fn shell_execute(&self, argv: &[String]) -> Result<(i32, String), ExecuteError> {
889 self.connection
890 .execute_command(&commands::shell::ShellCommandLineExecute { argv })
891 .map(|ret| (ret.ret, ret.o))
892 }
893
894 pub fn zephyr_erase_storage(&self) -> Result<(), ExecuteError> {
896 self.connection
897 .execute_command(&commands::zephyr::EraseStorage)
898 .map(Into::into)
899 }
900
901 pub fn raw_command<T: commands::McuMgrCommand>(
907 &self,
908 command: &T,
909 ) -> Result<T::Response, ExecuteError> {
910 self.connection.execute_command(command)
911 }
912}