dfu-core 0.5.0

Sans IO core library (traits and tools) for DFU
Documentation
//! Sans IO core library (traits and tools) for DFU.
#![no_std]
#![warn(missing_docs)]
#![allow(clippy::type_complexity)]
#![cfg_attr(docsrs, feature(doc_cfg))]

#[cfg(any(feature = "std", test))]
#[macro_use]
extern crate std;

/// Commands to detach the device.
pub mod detach;
/// Commands to download a firmware into the device.
pub mod download;
/// Functional descriptor.
pub mod functional_descriptor;
/// Commands to get the status of the device.
pub mod get_status;
/// Memory layout.
pub mod memory_layout;
/// Generic synchronous implementation.
#[cfg(any(feature = "std", test))]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
pub mod sync;

use core::convert::TryFrom;

use displaydoc::Display;
#[cfg(any(feature = "std", test))]
use thiserror::Error;

#[derive(Debug, Display)]
#[cfg_attr(any(feature = "std", test), derive(Error))]
#[allow(missing_docs)]
pub enum Error {
    /// The size of the data being transferred exceeds the DFU capabilities.
    OutOfCapabilities,
    /// The device is in an invalid state (got: {got:?}, expected: {expected:?}).
    InvalidState { got: State, expected: State },
    /// Buffer size exceeds the maximum allowed.
    BufferTooBig { got: usize, expected: usize },
    /// Maximum transfer size exceeded.
    MaximumTransferSizeExceeded,
    /// Erasing limit reached.
    EraseLimitReached,
    /// Maximum number of chunks exceeded.
    MaximumChunksExceeded,
    /// Not enough space on device.
    NoSpaceLeft,
    /// Unrecognized status code: {0}
    UnrecognizedStatusCode(u8),
    /// Unrecognized state code: {0}
    UnrecognizedStateCode(u8),
    /// Device response is too short (got: {got:?}, expected: {expected:?}).
    ResponseTooShort { got: usize, expected: usize },
    /// Device status is in error: {0}
    StatusError(Status),
    /// Device state is in error: {0}
    StateError(State),
    /// Unknown DFU protocol
    UnknownProtocol,
    /// Failed to parse dfuse interface string
    InvalidInterfaceString,
    /// Failed to parse dfuse address from interface string
    #[cfg(any(feature = "std", test))]
    MemoryLayout(memory_layout::Error),
    /// Failed to parse dfuse address from interface string
    InvalidAddress,
}

/// Trait to implement lower level communication with a USB device.
pub trait DfuIo {
    /// Return type after calling [`Self::read_control`].
    type Read;
    /// Return type after calling [`Self::write_control`].
    type Write;
    /// Return type after calling [`Self::usb_reset`].
    type Reset;
    /// Error type.
    type Error: From<Error>;
    /// Dfuse Memory layout type
    type MemoryLayout: AsRef<memory_layout::mem>;

    /// Read data using control transfer.
    fn read_control(
        &self,
        request_type: u8,
        request: u8,
        value: u16,
        buffer: &mut [u8],
    ) -> Result<Self::Read, Self::Error>;

    /// Write data using control transfer.
    fn write_control(
        &self,
        request_type: u8,
        request: u8,
        value: u16,
        buffer: &[u8],
    ) -> Result<Self::Write, Self::Error>;

    /// Triggers a USB reset.
    fn usb_reset(&self) -> Result<Self::Reset, Self::Error>;

    /// Returns the protocol of the device
    fn protocol(&self) -> &DfuProtocol<Self::MemoryLayout>;

    /// Returns the functional descriptor of the device.
    fn functional_descriptor(&self) -> &functional_descriptor::FunctionalDescriptor;
}

/// The DFU protocol variant in use
pub enum DfuProtocol<M> {
    /// DFU 1.1
    Dfu,
    /// STM DFU extensions aka DfuSe
    Dfuse {
        /// Start memory address
        address: u32,
        /// Memory layout for flash
        memory_layout: M,
    },
}

#[cfg(any(feature = "std", test))]
impl DfuProtocol<memory_layout::MemoryLayout> {
    /// Create a DFU Protocol object from the interface string and DFU version
    pub fn new(interface_string: &str, version: (u8, u8)) -> Result<Self, Error> {
        match version {
            (0x1, 0x10) => Ok(DfuProtocol::Dfu),
            (0x1, 0x1a) => {
                let (rest, memory_layout) = interface_string
                    .rsplit_once('/')
                    .ok_or(Error::InvalidInterfaceString)?;
                let memory_layout = memory_layout::MemoryLayout::try_from(memory_layout)
                    .map_err(Error::MemoryLayout)?;
                let (_rest, address) =
                    rest.rsplit_once('/').ok_or(Error::InvalidInterfaceString)?;
                let address = address
                    .strip_prefix("0x")
                    .and_then(|s| u32::from_str_radix(s, 16).ok())
                    .ok_or(Error::InvalidAddress)?;
                Ok(DfuProtocol::Dfuse {
                    address,
                    memory_layout,
                })
            }
            _ => Err(Error::UnknownProtocol),
        }
    }
}

/// Use this struct to create state machines to make operations on the device.
pub struct DfuSansIo<IO> {
    io: IO,
}

impl<IO: DfuIo> DfuSansIo<IO> {
    /// Create an instance of [`DfuSansIo`].
    pub fn new(io: IO) -> Self {
        Self { io }
    }

    fn protocol(&self) -> &DfuProtocol<IO::MemoryLayout> {
        self.io.protocol()
    }

    /// Create a state machine to download the firmware into the device.
    pub fn download(
        &self,
        length: u32,
    ) -> Result<
        get_status::GetStatus<
            '_,
            IO,
            get_status::ClearStatus<'_, IO, get_status::GetStatus<'_, IO, download::Start<'_, IO>>>,
        >,
        Error,
    > {
        let (protocol, end_pos) = match self.protocol() {
            DfuProtocol::Dfu => (download::ProtocolData::Dfu, length),
            DfuProtocol::Dfuse {
                address,
                memory_layout,
                ..
            } => (
                download::ProtocolData::Dfuse(download::DfuseProtocolData {
                    address: *address,
                    erased_pos: *address,
                    address_set: false,
                    memory_layout: memory_layout.as_ref(),
                }),
                address.checked_add(length).ok_or(Error::NoSpaceLeft)?,
            ),
        };

        Ok(get_status::GetStatus {
            dfu: self,
            chained_command: get_status::ClearStatus {
                dfu: self,
                chained_command: get_status::GetStatus {
                    dfu: self,
                    chained_command: download::Start {
                        dfu: self,
                        protocol,
                        end_pos,
                    },
                },
            },
        })
    }

    /// Send a Detach request to the device
    pub fn detach(&self) -> Result<(), IO::Error> {
        const REQUEST_TYPE: u8 = 0b00100001;
        const DFU_DETACH: u8 = 0;
        self.io.write_control(REQUEST_TYPE, DFU_DETACH, 1000, &[])?;
        Ok(())
    }

    /// Reset the USB device
    pub fn usb_reset(&self) -> Result<IO::Reset, IO::Error> {
        self.io.usb_reset()
    }

    /// Consume the object and return its [`DfuIo`]
    pub fn into_inner(self) -> IO {
        self.io
    }

    /// Returns whether the device is will detach if requested
    pub fn will_detach(&self) -> bool {
        self.io.functional_descriptor().will_detach
    }

    /// Returns whether the device is manifestation tolerant
    pub fn manifestation_tolerant(&self) -> bool {
        self.io.functional_descriptor().manifestation_tolerant
    }
}

/// DFU Status.
///
/// Note: not the same as state!
#[derive(Debug, Clone, Copy, Eq, PartialEq, Display)]
pub enum Status {
    /// No error condition is present.
    Ok,
    /// File is not targeted for use by this device.
    ErrTarget,
    /// File is for this device but fails some vendor-specific verification test.
    ErrFile,
    /// Device is unable to write memory.
    ErrWrite,
    /// Memory erase function failed.
    ErrErase,
    /// Memory erase check failed.
    ErrCheckErased,
    /// Program memory function failed.
    ErrProg,
    /// Programmed memory failed verification.
    ErrVerify,
    /// Cannot program memory due to received address that is out of range.
    ErrAddress,
    /// Received DFU_DNLOAD with wLength = 0, but device does not think it has all of the data yet.
    ErrNotdone,
    /// Device's firmware is corrupt. It cannot return to run-time (non-DFU) operations.
    ErrFirmware,
    /// iString indicates a vendor-specific error.
    ErrVendor,
    /// Device detected unexpected USB reset signaling.
    ErrUsbr,
    /// Device detected unexpected power on reset.
    ErrPor,
    /// Something went wrong, but the device does not know what it was.
    ErrUnknown,
    /// Device stalled an unexpected request.
    ErrStalledpkt,
    /// Other ({0}).
    Other(u8),
}

impl From<u8> for Status {
    fn from(state: u8) -> Self {
        match state {
            0x00 => Status::Ok,
            0x01 => Status::ErrTarget,
            0x02 => Status::ErrFile,
            0x03 => Status::ErrWrite,
            0x04 => Status::ErrErase,
            0x05 => Status::ErrCheckErased,
            0x06 => Status::ErrProg,
            0x07 => Status::ErrVerify,
            0x08 => Status::ErrAddress,
            0x09 => Status::ErrNotdone,
            0x0a => Status::ErrFirmware,
            0x0b => Status::ErrVendor,
            0x0c => Status::ErrUsbr,
            0x0d => Status::ErrPor,
            0x0e => Status::ErrUnknown,
            0x0f => Status::ErrStalledpkt,
            other => Status::Other(other),
        }
    }
}

impl From<Status> for u8 {
    fn from(state: Status) -> Self {
        match state {
            Status::Ok => 0x00,
            Status::ErrTarget => 0x01,
            Status::ErrFile => 0x02,
            Status::ErrWrite => 0x03,
            Status::ErrErase => 0x04,
            Status::ErrCheckErased => 0x05,
            Status::ErrProg => 0x06,
            Status::ErrVerify => 0x07,
            Status::ErrAddress => 0x08,
            Status::ErrNotdone => 0x09,
            Status::ErrFirmware => 0x0a,
            Status::ErrVendor => 0x0b,
            Status::ErrUsbr => 0x0c,
            Status::ErrPor => 0x0d,
            Status::ErrUnknown => 0x0e,
            Status::ErrStalledpkt => 0x0f,
            Status::Other(other) => other,
        }
    }
}

/// DFU State.
///
/// Note: not the same as status!
#[derive(Debug, Clone, Copy, Eq, PartialEq, Display)]
pub enum State {
    /// Device is running its normal application.
    AppIdle,
    /// Device is running its normal application, has received the DFU_DETACH request, and is waiting for a USB reset.
    AppDetach,
    /// Device is operating in the DFU mode and is waiting for requests.
    DfuIdle,
    /// Device has received a block and is waiting for the host to solicit the status via DFU_GETSTATUS.
    DfuDnloadSync,
    /// Device is programming a control-write block into its nonvolatile memories.
    DfuDnbusy,
    /// Device is processing a download operation.  Expecting DFU_DNLOAD requests.
    DfuDnloadIdle,
    /// Device has received the final block of firmware from the host and is waiting for receipt of DFU_GETSTATUS to begin the Manifestation phase; or device has completed the Manifestation phase and is waiting for receipt of DFU_GETSTATUS.  (Devices that can enter this state after the Manifestation phase set bmAttributes bit bitManifestationTolerant to 1.)
    DfuManifestSync,
    /// Device is in the Manifestation phase.  (Not all devices will be able to respond to DFU_GETSTATUS when in this state.)
    DfuManifest,
    /// Device has programmed its memories and is waiting for a USB reset or a power on reset.  (Devices that must enter this state clear bitManifestationTolerant to 0.)
    DfuManifestWaitReset,
    /// The device is processing an upload operation.  Expecting DFU_UPLOAD requests.
    DfuUploadIdle,
    /// An error has occurred. Awaiting the DFU_CLRSTATUS request.
    DfuError,
    /// Other ({0}).
    Other(u8),
}

impl From<u8> for State {
    fn from(state: u8) -> Self {
        match state {
            0 => State::AppIdle,
            1 => State::AppDetach,
            2 => State::DfuIdle,
            3 => State::DfuDnloadSync,
            4 => State::DfuDnbusy,
            5 => State::DfuDnloadIdle,
            6 => State::DfuManifestSync,
            7 => State::DfuManifest,
            8 => State::DfuManifestWaitReset,
            9 => State::DfuUploadIdle,
            10 => State::DfuError,
            other => State::Other(other),
        }
    }
}

impl From<State> for u8 {
    fn from(state: State) -> Self {
        match state {
            State::AppIdle => 0,
            State::AppDetach => 1,
            State::DfuIdle => 2,
            State::DfuDnloadSync => 3,
            State::DfuDnbusy => 4,
            State::DfuDnloadIdle => 5,
            State::DfuManifestSync => 6,
            State::DfuManifest => 7,
            State::DfuManifestWaitReset => 8,
            State::DfuUploadIdle => 9,
            State::DfuError => 10,
            State::Other(other) => other,
        }
    }
}

impl State {
    // Not all possible state are, according to the spec, possible in the GetStatus result.. As
    // that's defined as the state the device will be as a result of the request, which may trigger
    // state transitions. Ofcourse some devices get this wrong... So this does a reasonable
    // converstion to what should have been the result...
    fn for_status(self) -> Self {
        match self {
            State::DfuManifestSync => State::DfuManifest,
            State::DfuDnloadSync => State::DfuDnbusy,
            _ => self,
        }
    }
}

/// A trait for commands that be chained into another.
pub trait ChainedCommand {
    /// Type of the argument to pass with the command for chaining.
    type Arg;
    /// Type of the command after being chained.
    type Into;

    /// Chain this command into another.
    fn chain(self, arg: Self::Arg) -> Self::Into;
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::prelude::v1::*;

    // ensure DfuIo can be made into an object
    const _: [&dyn DfuIo<Read = (), Write = (), Reset = (), MemoryLayout = (), Error = Error>; 0] =
        [];
}