Skip to main content

mboot/
mboot.rs

1// Copyright 2025-2026 NXP
2//
3// SPDX-License-Identifier: BSD-3-Clause
4
5use color_print::cstr;
6use indicatif::{ProgressBar, ProgressStyle};
7use log::{info, trace};
8use packets::{
9    Packet, PacketParse,
10    command::{CmdResponse, CommandHeader, CommandPacket},
11    data_phase::DataPhasePacket,
12};
13use protocols::Protocol;
14use tags::{
15    ToAddress,
16    command::{CommandTag, CommandToParams, KeyProvOperation, TrustProvOperation},
17    command_flag::CommandFlag,
18    command_response::CmdResponseTag,
19    property::{PropertyTag, PropertyTagDiscriminants},
20    status::StatusCode,
21};
22
23use crate::CommunicationError;
24
25mod formatters;
26pub mod memory;
27pub mod packets;
28pub mod protocols;
29pub mod tags;
30
31/// Response structure for [`CommandTag::GetProperty`] command
32///
33/// Contains the status code, raw response words, and parsed property value.
34#[derive(Clone, Debug)]
35pub struct GetPropertyResponse {
36    /// Status code of the operation
37    pub status: StatusCode,
38    /// Raw response words from the device
39    pub response_words: Box<[u32]>,
40    /// Parsed property value
41    pub property: PropertyTag,
42}
43
44/// Response structure for [`CommandTag::ReadMemory`] command
45///
46/// Contains the status code, response metadata, and actual data bytes read.
47#[derive(Clone, Debug)]
48pub struct ReadMemoryResponse {
49    /// Status code of the operation
50    pub status: StatusCode,
51    /// Response metadata (typically contains the byte count)
52    pub response_words: Box<[u32]>,
53    /// Actual data bytes read from memory
54    pub bytes: Box<[u8]>,
55}
56
57/// Response types for [`CommandTag::KeyProvisioning`] operations
58#[derive(Clone, Debug)]
59pub enum KeyProvisioningResponse {
60    /// Simple status response for most key provisioning operations
61    Status(StatusCode),
62    /// Extended response for [`KeyProvOperation::ReadKeyStore`] operation containing key data
63    KeyStore {
64        /// Status code of the operation
65        status: StatusCode,
66        /// Response metadata
67        response_words: Box<[u32]>,
68        /// Key store data bytes
69        bytes: Box<[u8]>,
70    },
71}
72
73trait InvalidData<T> {
74    /// Convert a type to [`Result`] of [`CommunicationError`].
75    fn or_invalid(self) -> Result<T, CommunicationError>;
76}
77
78impl<T, E> InvalidData<T> for Result<T, E> {
79    /// Transforms `E` into [`CommunicationError::InvalidData`]
80    fn or_invalid(self) -> Result<T, CommunicationError> {
81        self.or(Err(CommunicationError::InvalidData))
82    }
83}
84
85/// Main MCU Boot communication structure
86///
87/// Provides high-level interface for bootloader communication over various protocols.
88///
89/// # Type Parameters
90///
91/// * `T` - The underlying communication protocol (UART, USB, etc.)
92pub struct McuBoot<T>
93where
94    T: Protocol,
95{
96    device: T,
97    /// Enable/disable progress bar for data transfers
98    pub progress_bar: bool,
99    pub mask_read_data_phase: bool,
100}
101
102/// Result type for communication operations returning a value
103pub type ResultComm<T> = Result<T, CommunicationError>;
104/// Result type for operations returning only a status code
105pub type ResultStatus = ResultComm<StatusCode>;
106
107impl<T> McuBoot<T>
108where
109    T: Protocol,
110{
111    /// Creates a new [`McuBoot`] instance with the specified protocol
112    ///
113    /// # Arguments
114    ///
115    /// * `device` - The communication protocol instance
116    ///
117    /// # Returns
118    ///
119    /// A new [`McuBoot`] instance
120    #[must_use]
121    pub fn new(device: T) -> Self {
122        info!(
123            "Initialized MCU Boot with device identifier: {}",
124            device.get_identifier()
125        );
126        McuBoot {
127            device,
128            progress_bar: false,
129            mask_read_data_phase: false,
130        }
131    }
132
133    /// Get a specific property value from the device
134    ///
135    /// # Arguments
136    ///
137    /// * `tag` - The property tag to query
138    /// * `memory_index` - External memory ID or internal memory region index
139    ///
140    /// # Returns
141    ///
142    /// Property response containing the value and status
143    ///
144    /// # Errors
145    ///
146    /// Returns [`CommunicationError`] if:
147    /// - Communication with device fails
148    /// - Invalid response is received
149    /// - Property is not supported
150    pub fn get_property(
151        &mut self,
152        tag: PropertyTagDiscriminants,
153        memory_index: u32,
154    ) -> ResultComm<GetPropertyResponse> {
155        let command = CommandPacket::new_none_flag(CommandTag::GetProperty { tag, memory_index });
156        self.send_command(&command)?;
157
158        let response = self.read_cmd_response()?;
159
160        if let CmdResponseTag::GetProperty(val) = response.tag {
161            Ok(GetPropertyResponse {
162                status: response.status,
163                property: PropertyTag::from_code(tag, &val),
164                response_words: val,
165            })
166        } else {
167            Err(CommunicationError::InvalidPacketReceived)
168        }
169    }
170
171    /// Set a property value on the device
172    ///
173    /// # Arguments
174    ///
175    /// * `tag` - The property tag to set
176    /// * `value` - The value to set
177    ///
178    /// # Returns
179    ///
180    /// Status code indicating success or failure
181    ///
182    /// # Errors
183    ///
184    /// Returns [`CommunicationError`] if communication fails
185    pub fn set_property(&mut self, tag: PropertyTagDiscriminants, value: u32) -> ResultStatus {
186        let command = CommandPacket::new_none_flag(CommandTag::SetProperty { tag, value });
187        self.send_command(&command)?;
188
189        let response = self.read_cmd_response()?;
190        Ok(response.status)
191    }
192
193    /// Reset the MCU
194    ///
195    /// Sends a reset command to the device. Note that the connection may be lost
196    /// after reset and need to be re-established.
197    ///
198    /// # Returns
199    ///
200    /// Status code (may be [`StatusCode::NoResponse`] if device resets successfully)
201    ///
202    /// # Errors
203    ///
204    /// Any [`CommunicationError`], almost all variants are possible.
205    pub fn reset(&mut self) -> ResultStatus {
206        let command = CommandPacket::new_none_flag(CommandTag::Reset);
207        self.send_command(&command)?;
208        let response = self.read_cmd_response()?;
209        Ok(response.status)
210    }
211
212    /// Call a function at the specified address
213    ///
214    /// # Arguments
215    ///
216    /// * `start_address` - Function address to call (must be word aligned)
217    /// * `argument` - Function argument
218    ///
219    /// # Returns
220    ///
221    /// Status code indicating success or failure
222    ///
223    /// # Errors
224    ///
225    /// Any [`CommunicationError`], almost all variants are possible.
226    pub fn call(&mut self, start_address: u32, argument: u32) -> ResultStatus {
227        let command = CommandPacket::new_none_flag(CommandTag::Call {
228            start_address,
229            argument,
230        });
231        self.send_command(&command)?;
232        let response = self.read_cmd_response()?;
233        Ok(response.status)
234    }
235
236    /// Execute program at the specified address
237    ///
238    /// # Arguments
239    ///
240    /// * `start_address` - Jump address (must be word aligned)
241    /// * `argument` - Function argument address
242    /// * `stackpointer` - Stack pointer address
243    ///
244    /// # Returns
245    ///
246    /// Status code indicating success or failure
247    ///
248    /// # Errors
249    ///
250    /// Any [`CommunicationError`], almost all variants are possible.
251    pub fn execute(&mut self, start_address: u32, argument: u32, stackpointer: u32) -> ResultStatus {
252        let command = CommandPacket::new_none_flag(CommandTag::Execute {
253            start_address,
254            argument,
255            stackpointer,
256        });
257        self.send_command(&command)?;
258        let response = self.read_cmd_response()?;
259        Ok(response.status)
260    }
261
262    /// Fill memory region with a pattern
263    ///
264    /// # Arguments
265    ///
266    /// * `start_address` - Start address (must be word aligned)
267    /// * `byte_count` - Number of bytes to fill (must be word aligned)
268    /// * `pattern` - 32-bit pattern to fill with
269    ///
270    /// # Returns
271    ///
272    /// Status code indicating success or failure
273    ///
274    /// # Errors
275    ///
276    /// Any [`CommunicationError`], almost all variants are possible.
277    pub fn fill_memory(&mut self, start_address: u32, byte_count: u32, pattern: u32) -> ResultStatus {
278        let command = CommandPacket::new_none_flag(CommandTag::FillMemory {
279            start_address,
280            byte_count,
281            pattern,
282        });
283        self.send_command(&command)?;
284        let response = self.read_cmd_response()?;
285        Ok(response.status)
286    }
287
288    /// Write data to MCU memory
289    ///
290    /// # Arguments
291    ///
292    /// * `start_address` - Start address for writing
293    /// * `memory_id` - Memory ID (0 for internal memory, see [`memory::mem_id`] for external)
294    /// * `bytes` - Data to write
295    ///
296    /// # Returns
297    ///
298    /// Status code indicating success or failure
299    ///
300    /// # Note
301    ///
302    /// Data will be automatically split into chunks based on max packet size
303    ///
304    /// # Errors
305    ///
306    /// Any [`CommunicationError`], almost all variants are possible.
307    pub fn write_memory(&mut self, start_address: u32, memory_id: u32, bytes: &[u8]) -> ResultStatus {
308        let command = CommandPacket::new_data_phase(CommandTag::WriteMemory {
309            start_address,
310            memory_id,
311            bytes,
312        });
313        self.send_command(&command)?;
314
315        let response = self.read_cmd_response()?;
316        Ok(response.status)
317    }
318
319    /// Erase all flash memory
320    ///
321    /// # Arguments
322    ///
323    /// * `memory_id` - Memory ID (0 for internal flash)
324    ///
325    /// # Returns
326    ///
327    /// Status code indicating success or failure
328    ///
329    /// # Warning
330    ///
331    /// This operation will erase the entire flash memory without recovering
332    /// the flash security section.
333    ///
334    /// # Errors
335    ///
336    /// Any [`CommunicationError`], almost all variants are possible.
337    pub fn flash_erase_all(&mut self, memory_id: u32) -> ResultStatus {
338        let command = CommandPacket::new_none_flag(CommandTag::FlashEraseAll { memory_id });
339        self.send_command(&command)?;
340        let response = self.read_cmd_response()?;
341        Ok(response.status)
342    }
343
344    /// Erase a specific flash region
345    ///
346    /// # Arguments
347    ///
348    /// * `start_address` - Start address of region to erase
349    /// * `byte_count` - Number of bytes to erase
350    /// * `memory_id` - Memory ID (0 for internal flash)
351    ///
352    /// # Returns
353    ///
354    /// Status code indicating success or failure
355    ///
356    /// # Errors
357    ///
358    /// Any [`CommunicationError`], almost all variants are possible.
359    pub fn flash_erase_region(&mut self, start_address: u32, byte_count: u32, memory_id: u32) -> ResultStatus {
360        let command = CommandPacket::new_none_flag(CommandTag::FlashEraseRegion {
361            start_address,
362            byte_count,
363            memory_id,
364        });
365        self.send_command(&command)?;
366        let response = self.read_cmd_response()?;
367        Ok(response.status)
368    }
369
370    /// Erase all flash and recover security section
371    ///
372    /// This command erases the entire flash memory and recovers the flash security section,
373    /// effectively unsecuring the device.
374    ///
375    /// # Returns
376    ///
377    /// Status code indicating success or failure
378    ///
379    /// # Errors
380    ///
381    /// Any [`CommunicationError`], almost all variants are possible.
382    pub fn flash_erase_all_unsecure(&mut self) -> ResultStatus {
383        let command = CommandPacket::new_none_flag(CommandTag::FlashEraseAllUnsecure);
384        self.send_command(&command)?;
385        let response = self.read_cmd_response()?;
386        Ok(response.status)
387    }
388
389    /// Read data from MCU memory
390    ///
391    /// # Arguments
392    ///
393    /// * `start_address` - Start address to read from
394    /// * `byte_count` - Number of bytes to read
395    /// * `memory_id` - Memory ID (0 for internal memory)
396    ///
397    /// # Returns
398    ///
399    /// Response containing the read data and status
400    ///
401    /// # Errors
402    ///
403    /// Returns [`CommunicationError`] if:
404    /// - Communication fails
405    /// - Invalid response is received
406    /// - Memory is protected or inaccessible
407    pub fn read_memory(
408        &mut self,
409        start_address: u32,
410        byte_count: u32,
411        memory_id: u32,
412    ) -> ResultComm<ReadMemoryResponse> {
413        let command = CommandPacket::new_none_flag(CommandTag::ReadMemory {
414            start_address,
415            byte_count,
416            memory_id,
417        });
418        self.send_command(&command)?;
419
420        // allowing one more status code when reading memory
421        let response = self.read_command()?;
422        let status = &response.status;
423        if !(status.is_success() || status.is_memory_blank_page_read_disallowed()) {
424            return Err((*status).into());
425        }
426
427        if let CmdResponseTag::ReadMemory(bytes) = response.tag {
428            Ok(ReadMemoryResponse {
429                status: response.status,
430                response_words: Box::new([bytes.len() as u32]),
431                bytes,
432            })
433        } else {
434            Err(CommunicationError::InvalidPacketReceived)
435        }
436    }
437
438    /// Generate a key blob by wrapping a DEK with the selected BKEK.
439    ///
440    /// # Arguments
441    ///
442    /// * `dek_data` - Data Encryption Key bytes to wrap
443    /// * `key_sel` - BKEK selector (0-3)
444    ///
445    /// # Returns
446    ///
447    /// The generated key blob.
448    ///
449    /// # Errors
450    ///
451    /// Returns [`CommunicationError`] if communication fails or the device rejects the request.
452    pub fn generate_key_blob(&mut self, dek_data: &[u8], key_sel: u32) -> ResultComm<Box<[u8]>> {
453        let dek_length = u32::try_from(dek_data.len()).or(Err(CommunicationError::InvalidData))?;
454        self.send_command(&CommandPacket::new_data_phase(CommandTag::GenerateKeyBlob {
455            key_sel,
456            length: dek_length,
457            option: 0,
458            data: Some(dek_data),
459        }))?;
460        // final generic response of the DEK data phase
461        self.read_cmd_response()?;
462
463        self.send_command(&CommandPacket::new_none_flag(CommandTag::GenerateKeyBlob {
464            key_sel,
465            length: 72,
466            option: 1,
467            data: None,
468        }))?;
469        let response = self.read_cmd_response()?;
470        match response.tag {
471            CmdResponseTag::KeyBlob(bytes) => Ok(bytes),
472            _ => Err(CommunicationError::InvalidPacketReceived),
473        }
474    }
475
476    /// Configure external memory
477    ///
478    /// # Arguments
479    ///
480    /// * `memory_id` - Memory ID to configure
481    /// * `address` - Address containing configuration data
482    ///
483    /// # Returns
484    ///
485    /// Status code indicating success or failure
486    ///
487    /// # Errors
488    ///
489    /// Any [`CommunicationError`], almost all variants are possible.
490    pub fn configure_memory(&mut self, memory_id: u32, address: u32) -> ResultStatus {
491        let command = CommandPacket::new_none_flag(CommandTag::ConfigureMemory { memory_id, address });
492        self.send_command(&command)?;
493        let response = self.read_cmd_response()?;
494        Ok(response.status)
495    }
496
497    /// Receive and process a Secure Binary (SB) file
498    ///
499    /// # Arguments
500    ///
501    /// * `bytes` - SB file data
502    ///
503    /// # Returns
504    ///
505    /// Status code indicating success or failure
506    ///
507    /// # Note
508    ///
509    /// The SB file will be processed and executed by the bootloader.
510    /// Progress bar will be shown if enabled.
511    ///
512    /// # Errors
513    ///
514    /// Any [`CommunicationError`], almost all variants are possible.
515    pub fn receive_sb_file(&mut self, bytes: &[u8]) -> ResultStatus {
516        let command = CommandPacket::new_data_phase(CommandTag::ReceiveSBFile { bytes });
517        match self.send_command(&command) {
518            Ok(()) | Err(CommunicationError::Aborted) => {
519                let response = self.read_cmd_response()?;
520                Ok(response.status)
521            }
522            Err(err) => Err(err),
523        }
524    }
525
526    /// Execute trust provisioning operation
527    ///
528    /// Performs various trust provisioning operations on the device, such as
529    /// proving genuinity, setting wrapped data, or other security-related operations.
530    ///
531    /// # Arguments
532    ///
533    /// * `operation` - The trust provisioning operation to execute
534    ///
535    /// # Returns
536    ///
537    /// A tuple containing:
538    /// - Status code indicating success or failure
539    /// - Response data specific to the operation
540    ///
541    /// # Errors
542    ///
543    /// Any [`CommunicationError`], almost all variants are possible.
544    pub fn trust_provisioning(&mut self, operation: &TrustProvOperation) -> ResultComm<(StatusCode, Box<[u32]>)> {
545        let command = CommandPacket::new_none_flag(CommandTag::TrustProvisioning(operation));
546        self.send_command(&command)?;
547
548        let response = self.read_cmd_response()?;
549        match response.tag {
550            CmdResponseTag::TrustProvisioning(data) => Ok((response.status, data)),
551            _ => Err(CommunicationError::InvalidPacketReceived),
552        }
553    }
554
555    /// Execute key provisioning operation
556    ///
557    /// Handles various key provisioning operations including enrolling PUF,
558    /// setting intrinsic keys, writing to non-volatile memory, and managing user keys.
559    /// The behavior varies based on the operation type:
560    /// - [`KeyProvOperation::SetUserKey`] operations include a data phase for key transmission
561    /// - [`KeyProvOperation::ReadKeyStore`] operations return the actual key store data
562    /// - Other operations return simple status responses
563    ///
564    /// # Arguments
565    ///
566    /// * `operation` - The key provisioning operation to execute
567    ///
568    /// # Returns
569    ///
570    /// Response type depends on the operation:
571    /// - [`KeyProvisioningResponse::Status`] for most operations
572    /// - [`KeyProvisioningResponse::KeyStore`] for [`KeyProvOperation::ReadKeyStore`] with key data
573    ///
574    /// # Errors
575    ///
576    /// Returns [`CommunicationError`] if:
577    /// - Communication with device fails
578    /// - Invalid response is received
579    /// - Data phase transmission fails for `SetUserKey`
580    pub fn key_provisioning(
581        &mut self,
582        operation: &KeyProvOperation,
583    ) -> Result<KeyProvisioningResponse, CommunicationError> {
584        let command = CommandPacket::new_none_flag(CommandTag::KeyProvisioning(operation));
585        if let KeyProvOperation::ReadKeyStore { .. } = operation {
586            self.send_command(&command)?;
587            let response = self.read_cmd_response()?;
588            // Extract the data based on the response tag
589            match response.tag {
590                CmdResponseTag::KeyProvisioning(data, data_phase) => {
591                    // The data phase should contain the actual key store data
592                    Ok(KeyProvisioningResponse::KeyStore {
593                        status: response.status,
594                        response_words: data,
595                        bytes: data_phase.unwrap_or_default(),
596                    })
597                }
598                _ => Err(CommunicationError::InvalidPacketReceived),
599            }
600        } else {
601            self.mask_read_data_phase = true;
602            self.send_command(&command)?;
603            self.mask_read_data_phase = false;
604            let response = self.read_cmd_response()?;
605            Ok(KeyProvisioningResponse::Status(response.status))
606        }
607    }
608
609    /// Read from MCU flash program once region (eFuse/OTP)
610    ///
611    /// Reads the raw bytes from the one-time programmable (OTP) memory region.
612    /// This memory can only be written once and is typically used for storing
613    /// permanent configuration or security keys.
614    ///
615    /// # Arguments
616    ///
617    /// * `index` - Start index of the eFuse/OTP region
618    /// * `count` - Number of bytes to read (must be 4 or 8)
619    ///
620    /// # Returns
621    ///
622    /// The read value as raw little-endian bytes, `count` bytes long. This matches blhost's
623    /// `FlashReadOnceResponse.data`, which also returns the full requested payload instead of
624    /// truncating 8-byte reads down to a single 32-bit word.
625    ///
626    /// # Errors
627    ///
628    /// Returns [`CommunicationError::ParseError`] if `count` is not 4 or 8. Otherwise, returns
629    /// [`CommunicationError`] if:
630    /// - Communication with device fails
631    /// - Invalid response type is received
632    /// - The specified index is out of range
633    /// - The OTP region is locked or inaccessible
634    pub fn flash_read_once(&mut self, index: u32, count: u32) -> ResultComm<Box<[u8]>> {
635        if count != 4 && count != 8 {
636            return Err(CommunicationError::ParseError(format!(
637                "invalid count {count}: flash-read-once only supports 4 or 8 byte reads, matching blhost"
638            )));
639        }
640
641        let command = CommandPacket::new_none_flag(CommandTag::FlashReadOnce { index, count });
642        self.send_command(&command)?;
643
644        let response = self.read_cmd_response()?;
645        match response.tag {
646            CmdResponseTag::FlashReadOnce(value) => Ok(value),
647            _ => Err(CommunicationError::InvalidPacketReceived),
648        }
649    }
650
651    /// Write into MCU once program region (eFuse/OTP)
652    ///
653    /// Programs a `count`-byte value into the one-time programmable memory region.
654    /// This operation is irreversible - once programmed, the memory cannot be
655    /// erased or reprogrammed. In OTP memory, bits can only change from 0 to 1,
656    /// never from 1 to 0.
657    ///
658    /// # Arguments
659    ///
660    /// * `index` - Start index of the eFuse/OTP region
661    /// * `count` - Number of bytes to write (must be 4 or 8, matching blhost's
662    ///   `byte_count` choice)
663    /// * `data` - Value to write. Only the low `count` bytes are sent on the wire,
664    ///   little-endian, matching blhost's `data.to_bytes(byte_count, "little")`
665    /// * `verify` - If true, reads back and verifies the written value
666    ///
667    /// # Returns
668    ///
669    /// Status code indicating success or failure. If verification is enabled
670    /// and fails, returns [`StatusCode::OtpVerifyFail`].
671    ///
672    /// # Notes
673    ///
674    /// - The verification process checks if all bits that were supposed to be
675    ///   set to 1 are actually set. It uses bitwise AND to accommodate the fact
676    ///   that some bits might have already been programmed.
677    /// - The index is masked to 24 bits during verification read
678    /// - ROM might not report errors when attempting to write to locked OTP,
679    ///   so verification is recommended for critical operations
680    ///
681    /// # Errors
682    ///
683    /// Returns [`CommunicationError::ParseError`] if `count` is not 4 or 8, or if `data` doesn't
684    /// fit into `count` bytes. Otherwise, any
685    /// other [`CommunicationError`] variant may be returned; almost all are possible.
686    /// Verification fail is not an error.
687    pub fn flash_program_once(&mut self, index: u32, count: u32, data: u64, verify: bool) -> ResultStatus {
688        validate_flash_program_once_args(count, data)?;
689
690        let command = CommandPacket::new_none_flag(CommandTag::FlashProgramOnce { index, count, data });
691        self.send_command(&command)?;
692
693        let response = self.read_cmd_response()?;
694
695        if verify && response.status.is_success() {
696            // For verification, we read back the value and check if the bits we set are still set
697            // Note: In OTP, we can only set bits from 0 to 1, not vice versa
698            match self.flash_read_once(index & (EFUSE_LOCK_FLAG - 1), count) {
699                Ok(read_bytes) => {
700                    let mut word = [0u8; 8];
701                    let len = read_bytes.len().min(8);
702                    word[..len].copy_from_slice(&read_bytes[..len]);
703                    let read_value = u64::from_le_bytes(word);
704
705                    if read_value & data == data {
706                        Ok(response.status)
707                    } else {
708                        // Custom status code for verification failure
709                        Ok(StatusCode::OtpVerifyFail)
710                    }
711                }
712                Err(e) => Err(e),
713            }
714        } else {
715            Ok(response.status)
716        }
717    }
718
719    /// Read a 32-bit word from the eFuse/OTP region
720    ///
721    /// Convenience wrapper over [`McuBoot::flash_read_once`] with a fixed 4-byte count, matching
722    /// blhost's `efuse-read-once` command (`McuBoot.efuse_read_once` in SPSDK).
723    ///
724    /// # Arguments
725    ///
726    /// * `index` - Index of the OTP word (not the shadowed memory address)
727    ///
728    /// # Returns
729    ///
730    /// The read 32-bit value, decoded as little-endian.
731    ///
732    /// # Errors
733    ///
734    /// Same as [`McuBoot::flash_read_once`].
735    pub fn efuse_read_once(&mut self, index: u32) -> ResultComm<u32> {
736        let bytes = self.flash_read_once(index, 4)?;
737        let mut word = [0u8; 4];
738        let len = bytes.len().min(word.len());
739        word[..len].copy_from_slice(&bytes[..len]);
740        Ok(u32::from_le_bytes(word))
741    }
742
743    /// Program a 32-bit word into the eFuse/OTP region
744    ///
745    /// Convenience wrapper over [`McuBoot::flash_program_once`] with a fixed 4-byte count, matching
746    /// blhost's `efuse-program-once` command (`McuBoot.efuse_program_once` in SPSDK). This operation
747    /// is irreversible.
748    ///
749    /// # Arguments
750    ///
751    /// * `index` - Index of the OTP word (not the shadowed memory address). Set bit 24
752    ///   ([`EFUSE_LOCK_FLAG`]) to request a permanent lock of the word, as blhost's `lock` option does.
753    /// * `data` - 32-bit value to program
754    /// * `verify` - If true, reads back and verifies the written value
755    ///
756    /// # Returns
757    ///
758    /// Status code indicating success or failure. If verification is enabled
759    /// and fails, returns [`StatusCode::OtpVerifyFail`].
760    ///
761    /// # Errors
762    ///
763    /// Same as [`McuBoot::flash_program_once`].
764    pub fn efuse_program_once(&mut self, index: u32, data: u32, verify: bool) -> ResultStatus {
765        self.flash_program_once(index, 4, u64::from(data), verify)
766    }
767
768    /// Read fuse data
769    ///
770    /// Reads data from the device's fuse memory region. Fuses are one-time
771    /// programmable memory bits used for permanent configuration, security
772    /// settings, and device-specific information.
773    ///
774    /// # Arguments
775    ///
776    /// * `start_address` - Starting address in the fuse memory region
777    /// * `byte_count` - Number of bytes to read
778    /// * `memory_id` - Memory identifier (device-specific)
779    ///
780    /// # Returns
781    ///
782    /// [`ReadMemoryResponse`] containing:
783    /// - Status code of the operation
784    /// - Response metadata (byte count)
785    /// - Actual fuse data bytes
786    ///
787    /// # Errors
788    ///
789    /// Returns [`CommunicationError`] if:
790    /// - The operation fails (converted from status code)
791    /// - Invalid response type is received
792    /// - Fuse region is inaccessible or protected
793    pub fn fuse_read(&mut self, start_address: u32, byte_count: u32, memory_id: u32) -> ResultComm<ReadMemoryResponse> {
794        let command = CommandPacket::new_none_flag(CommandTag::FuseRead {
795            start_address,
796            byte_count,
797            memory_id,
798        });
799        self.send_command(&command)?;
800        let response = self.read_cmd_response()?;
801        let status = &response.status;
802        if !status.is_success() {
803            return Err((*status).into());
804        }
805        match response.tag {
806            CmdResponseTag::ReadMemory(bytes) => Ok(ReadMemoryResponse {
807                status: response.status,
808                response_words: Box::new([bytes.len() as u32]),
809                bytes,
810            }),
811            _ => Err(CommunicationError::InvalidPacketReceived),
812        }
813    }
814
815    /// Program fuse data
816    ///
817    /// Writes data to the device's fuse memory region. This operation is
818    /// permanent and irreversible. Once a fuse is programmed (blown), it
819    /// cannot be restored to its original state.
820    ///
821    /// # Arguments
822    ///
823    /// * `start_address` - Starting address in the fuse memory region
824    /// * `memory_id` - Memory identifier (device-specific)
825    /// * `bytes` - Data to write to the fuses
826    ///
827    /// # Returns
828    ///
829    /// Status code indicating success or failure
830    ///
831    /// # Warning
832    ///
833    /// This operation permanently modifies the device hardware. Incorrect
834    /// fuse programming can render the device unusable. Always verify the
835    /// correct fuse addresses and values before programming.
836    ///
837    /// # Errors
838    ///
839    /// Any [`CommunicationError`], almost all variants are possible.
840    pub fn fuse_program(&mut self, start_address: u32, memory_id: u32, bytes: &[u8]) -> ResultStatus {
841        let command = CommandPacket::new_data_phase(CommandTag::FuseProgram {
842            start_address,
843            memory_id,
844            bytes,
845        });
846        self.send_command(&command)?;
847        let response = self.read_cmd_response()?;
848        Ok(response.status)
849    }
850
851    /// Load image data directly to the device
852    ///
853    /// Sends raw image data to the device without a specific command header.
854    ///
855    /// # Arguments
856    ///
857    /// * `bytes` - Raw image data to be loaded
858    ///
859    /// # Returns
860    ///
861    /// Status code indicating success or failure
862    ///
863    /// # Errors
864    ///
865    /// Any [`CommunicationError`], almost all variants are possible.
866    pub fn load_image(&mut self, bytes: &[u8]) -> ResultStatus {
867        let command = CommandPacket::new_data_phase(CommandTag::NoCommand { bytes });
868        self.send_command(&command)?;
869        Ok(StatusCode::Success)
870    }
871
872    /// Read command response and validate status
873    ///
874    /// Internal helper method that reads a command response from the device
875    /// and validates its status. If the status indicates an error, it converts
876    /// the status code into a [`CommunicationError`].
877    ///
878    /// # Returns
879    ///
880    /// The command response if status indicates success
881    ///
882    /// # Errors
883    ///
884    /// Returns [`CommunicationError`] converted from the status code if the
885    /// operation was not successful
886    fn read_cmd_response(&mut self) -> ResultComm<CmdResponse> {
887        let response = self.read_command()?;
888        info!("{}: {response:02X?}", cstr!("<bold>Received"));
889        if response.status.is_success() {
890            Ok(response)
891        } else {
892            Err(response.status.into())
893        }
894    }
895    /// Send a command packet to the device
896    ///
897    /// Internal helper method that handles the complete command transmission
898    /// process, including data phase handling for commands that require it.
899    /// For commands with data phases, it automatically queries the device's
900    /// maximum packet size and splits the data accordingly.
901    ///
902    /// # Arguments
903    ///
904    /// * `command` - The command packet to send
905    ///
906    /// # Returns
907    ///
908    /// Ok(()) if the command was sent successfully
909    ///
910    /// # Errors
911    ///
912    /// Returns [`CommunicationError`] if:
913    /// - Failed to get max packet size property
914    /// - Device communication fails
915    /// - Data phase transmission fails
916    ///
917    /// # Workflow
918    ///
919    /// 1. Extracts parameters and data phase from the command
920    /// 2. Constructs and sends the initial command packet
921    /// 3. If data phase exists:
922    ///    - Queries max packet size from device
923    ///    - Reads intermediate response
924    ///    - Splits data into chunks
925    ///    - Sends each chunk with optional progress tracking
926    fn send_command(&mut self, command: &CommandPacket) -> ResultComm<()> {
927        let tag = &command.tag;
928        let (params, data_phase) = tag.to_params();
929        let packet = command.header.construct_frame(&params, tag.code());
930        info!("{}: {command:02X?}", cstr!("<bold>Sending"));
931
932        if let Some(data) = data_phase {
933            info!("Sending data phase: {data:02X?}");
934            let max_packet_size: u32 = {
935                let response = self.get_property(PropertyTagDiscriminants::MaxPacketSize, 0)?;
936                match response.property {
937                    PropertyTag::MaxPacketSize(size) => size,
938                    _ => return Err(CommunicationError::InvalidData),
939                }
940            };
941            if !matches!(tag, CommandTag::NoCommand { .. }) {
942                self.device.write_packet_raw(&packet)?;
943                // this is the intermediate generic response
944                self.read_cmd_response()?;
945            }
946            // Block for progress bar
947            {
948                let progress_bar = self.create_progress_bar(data.len() as u64, "Sending data");
949                for bytes in data.chunks(
950                    max_packet_size
951                        .try_into()
952                        .expect("pointer size of this platform is too small"),
953                ) {
954                    self.device.write_packet_concrete(DataPhasePacket::parse(bytes)?)?;
955                    if let Some(bar) = progress_bar.as_ref() {
956                        bar.inc(max_packet_size.into());
957                    }
958                }
959            }
960        } else {
961            self.device.write_packet_raw(&packet)?;
962        }
963        Ok(())
964    }
965
966    /// Read a command response from the device
967    ///
968    /// Internal helper method that reads and parses command responses,
969    /// handling both simple responses and those with data phases.
970    ///
971    /// # Returns
972    ///
973    /// Parsed command response
974    ///
975    /// # Errors
976    ///
977    /// Returns [`CommunicationError`] if:
978    /// - Communication timeout occurs
979    /// - Invalid data format is received
980    /// - Command flag is unrecognized
981    /// - Data phase read fails
982    ///
983    /// # Data Phase Handling
984    ///
985    /// When a response includes a data phase:
986    /// 1. Reads the initial response header
987    /// 2. Extracts the data phase length
988    /// 3. Reads data packets until complete
989    /// 4. Shows progress bar if enabled
990    /// 5. Reads final status response
991    fn read_command(&mut self) -> ResultComm<CmdResponse> {
992        trace!("Starting to read command");
993        let data = self.device.read_packet_raw(CmdResponse::get_code())?;
994        let params_slice = &data[8..];
995
996        // data[3] = param count
997        if params_slice.len() % 4 != 0 && params_slice.len() != 4 * data[3] as usize {
998            return Err(CommunicationError::InvalidData);
999        }
1000
1001        let header = CommandHeader {
1002            flag: CommandFlag::try_from(data[1]).or(Err(CommunicationError::InvalidData))?,
1003            reserved: data[2],
1004        };
1005        let status = parse_status(data[4..8].try_into().or_invalid()?)?;
1006
1007        // If we dont expect data phase, we can force to return response without data
1008        // This is necessary for key-provisionning commands since their intermediate
1009        // generic response commands have data phase flag set, which is incorrect
1010        if self.mask_read_data_phase {
1011            return Ok(CmdResponse {
1012                header,
1013                status,
1014                tag: CmdResponseTag::from_code(data[0], params_slice, None).ok_or(CommunicationError::InvalidData)?,
1015            });
1016        }
1017
1018        match header.flag {
1019            CommandFlag::NoData => Ok(CmdResponse {
1020                header,
1021                status,
1022                tag: CmdResponseTag::from_code(data[0], params_slice, None).ok_or(CommunicationError::InvalidData)?,
1023            }),
1024            CommandFlag::HasDataPhase => {
1025                let length = u32::from_le_bytes(params_slice[0..4].try_into().or_invalid()?);
1026                trace!("Data phase length: {length}");
1027
1028                let mut data_phase = Vec::new();
1029                // Block for progress bar
1030                {
1031                    let progress_bar = self.create_progress_bar(length.into(), "Receiving data");
1032                    while data_phase.len() != length as usize {
1033                        trace!("Reading data phase packet");
1034                        data_phase.extend(match self.device.read_packet_concrete::<DataPhasePacket>() {
1035                            Ok(data) => {
1036                                if let Some(bar) = progress_bar.as_ref() {
1037                                    bar.inc(data.data.len() as u64);
1038                                }
1039                                data.data
1040                            }
1041                            Err(CommunicationError::Aborted) => break,
1042                            Err(err) => return Err(err),
1043                        });
1044                    }
1045                }
1046
1047                trace!("Reading final response");
1048                let final_response = self.device.read_packet_raw(CmdResponse::get_code())?;
1049                let status = parse_status(final_response[4..8].try_into().or_invalid()?)?;
1050
1051                Ok(CmdResponse {
1052                    header: CommandHeader {
1053                        flag: CommandFlag::NoData,
1054                        reserved: data[2],
1055                    },
1056                    status,
1057                    tag: CmdResponseTag::from_code(data[0], params_slice, Some(&data_phase))
1058                        .ok_or(CommunicationError::InvalidData)?,
1059                })
1060            }
1061        }
1062    }
1063
1064    /// Create a progress bar for data transfers
1065    ///
1066    /// Internal helper method that creates a progress bar if progress tracking is enabled.
1067    /// The progress bar displays the transfer status with binary size formatting.
1068    ///
1069    /// # Arguments
1070    ///
1071    /// * `len` - Total length of data to transfer in bytes
1072    /// * `prefix` - Descriptive prefix for the progress bar
1073    ///
1074    /// # Returns
1075    ///
1076    /// Optional progress bar instance:
1077    /// - Some(ProgressBar) if progress tracking is enabled
1078    /// - None if progress tracking is disabled
1079    ///
1080    /// # Progress Bar Format
1081    ///
1082    /// The progress bar displays:
1083    /// - Custom prefix text
1084    /// - Visual progress indicator (40 characters wide)
1085    /// - Current bytes transferred / total bytes
1086    fn create_progress_bar(&self, len: u64, prefix: &'static str) -> Option<ProgressBar> {
1087        if self.progress_bar {
1088            let bar = ProgressBar::new(len);
1089            bar.set_style(
1090                ProgressStyle::with_template("{prefix} [{bar:40}] {binary_bytes:>}/{binary_total_bytes}")
1091                    .unwrap()
1092                    .progress_chars("##-"),
1093            );
1094            bar.set_prefix(prefix);
1095            Some(bar)
1096        } else {
1097            None
1098        }
1099    }
1100}
1101
1102/// Bit of the eFuse/OTP index requesting a permanent lock of the word, set by blhost's
1103/// `efuse-program-once ... lock` option.
1104pub const EFUSE_LOCK_FLAG: u32 = 1 << 24;
1105
1106/// Checks `flash-program-once` arguments before anything is sent to the device: `count` must be
1107/// 4 or 8, and `data` must fit into `count` bytes. OTP writes are irreversible, so upper bits that
1108/// don't fit into a 4-byte write are rejected instead of silently dropped (blhost raises an error
1109/// there too).
1110fn validate_flash_program_once_args(count: u32, data: u64) -> ResultComm<()> {
1111    if count != 4 && count != 8 {
1112        return Err(CommunicationError::ParseError(format!(
1113            "invalid count {count}: flash-program-once only supports 4 or 8 byte writes, matching blhost"
1114        )));
1115    }
1116    if count == 4 && data > u64::from(u32::MAX) {
1117        return Err(CommunicationError::ParseError(format!(
1118            "data 0x{data:X} does not fit into {count} bytes; use count 8 or a 32-bit value"
1119        )));
1120    }
1121    Ok(())
1122}
1123
1124/// Parse status code from raw bytes
1125///
1126/// Converts a 4-byte little-endian value into a [`StatusCode`] enum.
1127/// This function is used throughout the module to interpret device responses.
1128///
1129/// # Arguments
1130///
1131/// * `data` - 4 bytes containing the status code in little-endian format
1132///
1133/// # Returns
1134///
1135/// Parsed status code enum variant
1136///
1137/// # Errors
1138///
1139/// Returns [`CommunicationError::UnexpectedStatus`] if:
1140/// - The status code value is not recognized
1141/// - The discriminant doesn't match any known [`StatusCode`] variant
1142///
1143/// The error includes both the unknown status placeholder and the actual
1144/// numeric value for debugging purposes.
1145fn parse_status(data: [u8; 4]) -> ResultComm<StatusCode> {
1146    let discriminant = u32::from_le_bytes(data);
1147    StatusCode::try_from(discriminant).or(Err(CommunicationError::UnexpectedStatus(
1148        StatusCode::UnknownStatusCode,
1149        discriminant,
1150    )))
1151}
1152
1153#[cfg(test)]
1154mod tests {
1155    use crate::mboot::{
1156        McuBoot,
1157        protocols::{ProtocolOpen, uart::UARTProtocol},
1158        tags::property::{PropertyTag, PropertyTagDiscriminants},
1159    };
1160
1161    #[test]
1162    fn test_validate_flash_program_once_args() {
1163        use super::validate_flash_program_once_args as validate;
1164        assert!(validate(4, 0xFFFF_FFFF).is_ok());
1165        assert!(validate(8, 0x1122_3344_5566_7788).is_ok());
1166        assert!(validate(4, 0x1_0000_0000).is_err());
1167        assert!(validate(0x12, 4).is_err());
1168        assert!(validate(5, 0).is_err());
1169    }
1170
1171    /// Device replaying scripted responses and recording written packets
1172    struct ScriptedDevice {
1173        responses: std::collections::VecDeque<Vec<u8>>,
1174        written: Vec<Vec<u8>>,
1175    }
1176
1177    impl crate::mboot::protocols::Protocol for ScriptedDevice {
1178        fn get_timeout(&self) -> std::time::Duration {
1179            std::time::Duration::from_secs(1)
1180        }
1181
1182        fn get_polling_interval(&self) -> std::time::Duration {
1183            std::time::Duration::from_millis(1)
1184        }
1185
1186        fn get_identifier(&self) -> &'static str {
1187            "scripted"
1188        }
1189
1190        fn read(&mut self, _bytes: usize) -> crate::mboot::ResultComm<Vec<u8>> {
1191            unimplemented!()
1192        }
1193
1194        fn write_packet_raw(&mut self, data: &[u8]) -> crate::mboot::ResultComm<()> {
1195            self.written.push(data.to_vec());
1196            Ok(())
1197        }
1198
1199        fn read_packet_raw(&mut self, _packet_code: u8) -> crate::mboot::ResultComm<Vec<u8>> {
1200            self.responses.pop_front().ok_or(crate::CommunicationError::Timeout)
1201        }
1202    }
1203
1204    /// Command response packet: tag, flag, reserved, parameter count, status and parameters
1205    fn response(tag: u8, flag: u8, params: &[u32]) -> Vec<u8> {
1206        let mut packet = vec![tag, flag, 0, u8::try_from(params.len() + 1).unwrap(), 0, 0, 0, 0];
1207        for param in params {
1208            packet.extend_from_slice(&param.to_le_bytes());
1209        }
1210        packet
1211    }
1212
1213    #[test]
1214    fn test_generate_key_blob_reads_data_phase_response() {
1215        const GENERATE_KEY_BLOB: u32 = 0x13;
1216        let blob: Vec<u8> = (0..72).collect();
1217        let responses = [
1218            // max packet size
1219            response(0xA7, 0, &[32]),
1220            // DEK command accepted, then DEK data phase done
1221            response(0xA0, 0, &[GENERATE_KEY_BLOB]),
1222            response(0xA0, 0, &[GENERATE_KEY_BLOB]),
1223            // key blob with data phase, then final response
1224            response(0xB3, 1, &[72]),
1225            blob.clone(),
1226            response(0xA0, 0, &[GENERATE_KEY_BLOB]),
1227        ];
1228        let mut boot = McuBoot::new(ScriptedDevice {
1229            responses: responses.into(),
1230            written: Vec::new(),
1231        });
1232
1233        let key_blob = boot.generate_key_blob(&[0xAB; 16], 0).unwrap();
1234
1235        assert_eq!(&*key_blob, &blob[..]);
1236        assert!(boot.device.responses.is_empty());
1237    }
1238
1239    const DEVICE: &str = "COM3";
1240    fn get_boot() -> McuBoot<UARTProtocol> {
1241        McuBoot::new(UARTProtocol::open(DEVICE).unwrap())
1242    }
1243
1244    #[test]
1245    #[ignore = "Requires hardware connection to board"]
1246    fn test_board_get_version() {
1247        let mut boot = get_boot();
1248        let version = boot.get_property(PropertyTagDiscriminants::CurrentVersion, 0).unwrap();
1249        if let PropertyTag::CurrentVersion(ver) = version.property {
1250            assert_eq!(ver.mark, 'K');
1251            assert_eq!(ver.major, 3);
1252            assert_eq!(ver.minor, 1);
1253            assert_eq!(ver.fixation, 1);
1254        } else {
1255            panic!()
1256        }
1257    }
1258}