tele0592 1.2.0

Control an alternate firmware for the DFR0592 DC motor driver hat
Documentation
//! High-level interface with the embedded I²C bootloader

use crate::device::{Device, Result};
use core::time::Duration;
use low_level::{
    CMD_CHANGE_PROGRAMMING_MODE, CMD_ERASE_PAGES, CMD_PROGRAMMING_STATUS, CMD_PROGRAM_DATA,
    CMD_READ_MEMORY, CMD_SET_CHECKSUM, CMD_SET_PROGRAMMING_ADDRESS,
};

/// Commands available when the board is in bootloader mode.
pub trait Bootloader: Device {
    /// Write command, data, and a xor of all data bytes. The xor
    /// computation is returned.
    ///
    /// # Errors
    /// This method may fail if the communication with the board fails.
    fn write_xored(&mut self, command: u8, data: impl AsRef<[u8]>) -> Result<u8, Self::I2cError> {
        let data = data.as_ref();
        let data_len = data.len();
        let mut buf = [0; 10];
        buf[0] = command;
        buf[1..=data_len].copy_from_slice(data);
        let xor = data.iter().fold(0, |a, &b| a ^ b);
        buf[data_len + 1] = xor;
        self.write(&buf[..data_len + 2])?;
        Ok(xor)
    }

    /// Get the programming status of the bootloader.
    ///
    /// # Errors
    /// This method may fail if the communication with the board fails.
    fn get_programming_status(&mut self) -> Result<ProgrammingStatus, Self::I2cError> {
        let mut buf = [0; 2];
        self.write_read([CMD_PROGRAMMING_STATUS], &mut buf)?;
        Ok(ProgrammingStatus {
            status: buf[0],
            checksum: buf[1],
        })
    }

    /// Enter programming mode by sending the required sequence.
    ///
    /// # Errors
    /// This method may fail if the communication with the board fails.
    fn enter_programming_mode(&mut self) -> Result<(), Self::I2cError> {
        self.write([CMD_CHANGE_PROGRAMMING_MODE, 0x17, 0x27, 0x65, 0x40])
    }

    /// Leave the programming mode, and mark or not the application has having been
    /// successfully written.
    ///
    /// # Errors
    /// This method may fail if the communication with the board fails.
    fn leave_programming_mode(&mut self, commit: bool) -> Result<(), Self::I2cError> {
        self.write([CMD_CHANGE_PROGRAMMING_MODE, u8::from(commit)])
    }

    /// Erase `count` application pages starting at `index`.
    ///
    /// Return a duration for which is it advised to wait before sending other
    /// operations.
    ///
    /// # Errors
    /// This method may fail if the communication with the board fails.
    fn erase_pages(&mut self, index: u8, count: u8) -> Result<Duration, Self::I2cError> {
        self.write_xored(CMD_ERASE_PAGES, [index, count])?;
        Ok(Duration::from_millis(u64::from(count + 1) * 30))
    }

    /// Set the current programming address in the application space.
    ///
    /// # Errors
    /// This method may fail if the communication with the board fails.
    fn set_programming_address(&mut self, address: u32) -> Result<(), Self::I2cError> {
        self.write_xored(CMD_SET_PROGRAMMING_ADDRESS, address.to_le_bytes())?;
        Ok(())
    }

    /// Program data starting at the current programming address.
    /// The address will advance automatically.x
    /// The checksum will be updated with a xor of the data.
    ///
    /// # Errors
    /// This method may fail if the communication with the board fails.
    fn program_data(&mut self, data: &[u8], checksum: &mut u8) -> Result<(), Self::I2cError> {
        let xor = self.write_xored(CMD_PROGRAM_DATA, data)?;
        *checksum ^= xor;
        Ok(())
    }

    /// Read memory starting from the current programming address.
    /// The address will advance automatically.
    /// Zeroes will be returned for data outside the application memory.
    ///
    /// # Errors
    /// This method may fail if the communication with the board fails.
    fn read_memory(&mut self) -> Result<[u8; 8], Self::I2cError> {
        let mut result = [0; 8];
        self.write_read([CMD_READ_MEMORY], &mut result)?;
        Ok(result)
    }

    /// Set the current value of the running checksum.
    ///
    /// # Errors
    /// This method may fail if the communication with the board fails.
    fn set_checksum(&mut self, checksum: u8) -> Result<(), Self::I2cError> {
        self.write([CMD_SET_CHECKSUM, checksum])
    }
}

impl<D: Device> Bootloader for D {}

/// Bootloader programming status returned by the
/// [`Bootloader::get_programming_status()`] method.
#[derive(Clone, Copy, Debug)]
pub struct ProgrammingStatus {
    status: u8,
    checksum: u8,
}

impl ProgrammingStatus {
    /// Check if the bootloader is in programming mode.
    #[must_use]
    pub fn programming_mode(self) -> bool {
        self.status & 1 != 0
    }

    /// Check if an error has been detected since the last time the
    /// programming mode has been entered.
    #[must_use]
    pub fn error_detected(self) -> bool {
        self.status & 2 != 0
    }

    /// Check if a valid application is marked present.
    #[must_use]
    pub fn application_present(self) -> bool {
        self.status & 4 != 0
    }

    /// Current running checksum value.
    #[must_use]
    pub fn checksum(self) -> u8 {
        self.checksum
    }
}

// Constants used in the low-level protocol between the host and the
// controller or bootloader firmware.
//
// *Note: this module requires that the `low-level` feature is
// selected.*
pub(crate) mod low_level {
    pub const CMD_PROGRAMMING_STATUS: u8 = 0x10;
    pub const CMD_CHANGE_PROGRAMMING_MODE: u8 = 0x20;
    pub const CMD_ERASE_PAGES: u8 = 0x24;
    pub const CMD_SET_PROGRAMMING_ADDRESS: u8 = 0x28;
    pub const CMD_PROGRAM_DATA: u8 = 0x2c;
    pub const CMD_READ_MEMORY: u8 = 0x30;
    pub const CMD_SET_CHECKSUM: u8 = 0x34;
    #[cfg(feature = "low-level")]
    pub const CMD_REBOOT_TO_APPLICATION: u8 = 0xe2;
}