use std::{io, time::Duration};
use bb_helper::cancel::CancellationToken;
use serialport::SerialPort;
use std::sync::mpsc;
use thiserror::Error;
use tracing::{error, info, warn};
use crate::{
Status,
helpers::{chan_send, parse_bin},
};
const CRC_ALGO: crc_fast::CrcAlgorithm = crc_fast::CrcAlgorithm::Crc32IsoHdlc;
const ACK: u8 = 0xcc;
const NACK: u8 = 0x33;
const COMMAND_DOWNLOAD: u8 = 0x21;
const COMMAND_GET_STATUS: u8 = 0x23;
const COMMAND_SEND_DATA: u8 = 0x24;
const COMMAND_RESET: u8 = 0x25;
const COMMAND_CRC32: u8 = 0x27;
const COMMAND_BANK_ERASE: u8 = 0x2c;
const COMMAND_MAX_SIZE: u8 = u8::MAX - 3;
const FIRMWARE_SIZE: u32 = 704 * 1024;
type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum Error {
#[error("Status for failing flash erase or program operation.")]
FlashFail,
#[error("Bootloader sent unexpected response.")]
UnknownResponse,
#[error("Bootloader Responded with Nack.")]
Nack,
#[error("Failed to start Bootloader.")]
FailedToStartBootloader,
#[error("Flashed image is not valid.")]
InvalidImage,
#[error("Failed to open serial port.")]
FailedToOpenPort,
#[error("Aborted before completing.")]
Aborted,
#[error("Unknown Error during IO. Please check logs for more information.")]
IoError {
#[from]
#[source]
source: io::Error,
},
}
struct BeagleConnectFreedom<S: SerialPort> {
port: S,
}
impl<S> BeagleConnectFreedom<S>
where
S: SerialPort,
{
fn new(port: S) -> Result<Self> {
let mut bcf = BeagleConnectFreedom { port };
bcf.invoke_bootloader()?;
bcf.send_sync()?;
Ok(bcf)
}
fn wait_for_ack(&mut self) -> Result<()> {
let mut buf = [0u8; 1];
while buf[0] == 0x00 {
self.port.read_exact(&mut buf)?;
}
match buf[0] {
ACK => Ok(()),
NACK => Err(Error::Nack),
_ => Err(Error::UnknownResponse),
}
}
fn invoke_bootloader(&mut self) -> Result<()> {
info!("Invoke Bootloader");
self.port
.set_break()
.map_err(|_| Error::FailedToStartBootloader)?;
std::thread::sleep(Duration::from_millis(500));
self.port
.clear_break()
.map_err(|_| Error::FailedToStartBootloader)?;
std::thread::sleep(Duration::from_millis(500));
Ok(())
}
fn send_sync(&mut self) -> Result<()> {
info!("Send Sync");
const PKT: &[u8] = &[0x55, 0x55];
self.port.write_all(PKT)?;
self.wait_for_ack()
}
fn crc32(&mut self) -> Result<u32> {
let addr = 0u32.to_be_bytes();
let size = FIRMWARE_SIZE.to_be_bytes();
let read_repeat = 0u32.to_be_bytes();
let mut cmd = [0u8; 2];
let mut cmd_data = [0u8; 4];
let checksum: u8 = size
.iter()
.chain(&[COMMAND_CRC32])
.fold(0u8, |acc, t| acc.wrapping_add(*t));
self.port.write_all(&[15, checksum, COMMAND_CRC32])?;
self.port.write_all(&addr)?;
self.port.write_all(&size)?;
self.port.write_all(&read_repeat)?;
self.wait_for_ack()?;
self.port.read_exact(&mut cmd)?;
assert_eq!(cmd[0], 6);
let checksum = cmd[1];
self.port.read_exact(&mut cmd_data)?;
assert_eq!(
checksum,
cmd_data.iter().fold(0u8, |acc, x| acc.wrapping_add(*x))
);
self.send_ack()?;
Ok(u32::from_be_bytes(cmd_data))
}
fn send_ack(&mut self) -> Result<(), Error> {
const PKT: &[u8] = &[0x00, ACK];
self.port.write_all(PKT).map_err(Into::into)
}
fn send_bank_erase(&mut self) -> Result<(), Error> {
const CMD: &[u8] = &[3, COMMAND_BANK_ERASE, COMMAND_BANK_ERASE];
self.port.write_all(CMD)?;
self.wait_for_ack()?;
self.get_status()
}
fn get_status(&mut self) -> Result<(), Error> {
const CMD: &[u8] = &[3, COMMAND_GET_STATUS, COMMAND_GET_STATUS];
let mut resp = [0u8; 1];
self.port.write_all(CMD)?;
self.wait_for_ack()?;
while resp[0] == 0x00 {
self.port.read_exact(&mut resp)?;
}
self.port.read_exact(&mut resp)?;
self.port.read_exact(&mut resp)?;
self.send_ack()?;
match resp[0] {
0x40 => Ok(()),
0x41 => panic!("Unknown Command"),
0x42 => panic!("Invalid Command"),
0x43 => panic!("Invalid Address"),
0x44 => Err(Error::FlashFail),
_ => Err(Error::UnknownResponse),
}
}
fn send_download(&mut self, addr: u32, size: u32) -> Result<(), Error> {
let addr = addr.to_be_bytes();
let size = size.to_be_bytes();
let checksum: u8 = addr
.into_iter()
.chain(size)
.chain([COMMAND_DOWNLOAD])
.fold(0u8, |acc, t| acc.wrapping_add(t));
self.port.write_all(&[11, checksum, COMMAND_DOWNLOAD])?;
self.port.write_all(&addr)?;
self.port.write_all(&size)?;
self.wait_for_ack()?;
self.get_status()
}
fn send_data(&mut self, data: &[u8]) -> Result<usize> {
let bytes_to_write = std::cmp::min(data.len(), usize::from(COMMAND_MAX_SIZE));
let checksum = data[..bytes_to_write]
.iter()
.chain(&[COMMAND_SEND_DATA])
.fold(0u8, |acc, t| acc.wrapping_add(*t));
self.port
.write_all(&[(bytes_to_write + 3) as u8, checksum, COMMAND_SEND_DATA])?;
self.port.write_all(&data[..bytes_to_write])?;
self.wait_for_ack()?;
self.get_status()?;
Ok(bytes_to_write)
}
fn send_reset(&mut self) -> Result<(), Error> {
const CMD: &[u8] = &[3, COMMAND_RESET, COMMAND_RESET];
self.port.write_all(CMD)?;
self.wait_for_ack()
}
fn verify(&mut self, crc32: u32) -> Result<bool> {
let actual_crc32 = self.crc32()?;
info!(
"CRC32 verification: expected={:08x}, actual={:08x}",
crc32, actual_crc32
);
Ok(actual_crc32 == crc32)
}
}
impl<S> Drop for BeagleConnectFreedom<S>
where
S: SerialPort,
{
fn drop(&mut self) {
let _ = self.send_reset();
}
}
const fn progress(off: usize) -> f32 {
(off as f32) / (FIRMWARE_SIZE as f32)
}
fn check_token(cancel: Option<&CancellationToken>) -> Result<()> {
match cancel {
Some(x) if x.is_cancelled() => Err(Error::Aborted),
_ => Ok(()),
}
}
fn open_bcf(port: &str) -> Result<BeagleConnectFreedom<impl SerialPort>> {
const MAX_RETRIES: usize = 5;
let mut last_err = None;
for _ in 0..MAX_RETRIES {
let port = serialport::new(port, 115200)
.timeout(Duration::from_millis(2000))
.open_native()
.map_err(|_| Error::FailedToOpenPort)?;
match BeagleConnectFreedom::new(port) {
Ok(x) => return Ok(x),
Err(e) => last_err = Some(e),
}
}
Err(last_err.unwrap_or(Error::FailedToOpenPort))
}
pub fn flash(
firmware: &[u8],
port: &str,
verify: bool,
chan: Option<mpsc::SyncSender<Status>>,
cancel: Option<CancellationToken>,
) -> Result<()> {
let firmware_bin = parse_bin(firmware).map_err(|_| Error::InvalidImage)?;
chan_send(chan.as_ref(), Status::Preparing);
let mut bcf = open_bcf(port)?;
info!("BeagleConnectFreedom Connected");
check_token(cancel.as_ref())?;
chan_send(chan.as_ref(), Status::Flashing(0.0));
let img_crc32: u32 = crc_fast::checksum(
CRC_ALGO,
&firmware_bin
.to_bytes(0..(FIRMWARE_SIZE as usize), Some(0xff))
.expect("Unexpected error"),
)
.try_into()
.unwrap();
if bcf.verify(img_crc32)? {
warn!("Skipping flashing same image");
return Ok(());
}
check_token(cancel.as_ref())?;
info!("Erase Flash");
bcf.send_bank_erase()?;
info!("Start Flashing");
check_token(cancel.as_ref())?;
for (start_address, data) in firmware_bin.segments_list() {
let mut offset = 0;
bcf.send_download(
start_address.try_into().expect("Unexpected error"),
data.len().try_into().expect("Unexpected error"),
)?;
while offset < data.len() {
offset += bcf.send_data(&data[offset..])?;
chan_send(
chan.as_ref(),
Status::Flashing(progress(start_address + offset)),
);
check_token(cancel.as_ref())?;
}
}
if verify {
chan_send(chan.as_ref(), Status::Verifying);
std::thread::sleep(Duration::from_millis(100));
if bcf.verify(img_crc32)? {
info!("Flashing Successful");
Ok(())
} else {
error!("Invalid CRC32 in Flash. The flashed image might be corrupted");
Err(Error::InvalidImage)
}
} else {
Ok(())
}
}
pub fn ports(filter: bool) -> impl Iterator<Item = String> {
serialport::available_ports()
.expect("Unsupported OS")
.into_iter()
.filter(move |x| {
if filter && cfg!(target_os = "linux") {
return match &x.port_type {
serialport::SerialPortType::UsbPort(y) => {
y.manufacturer.as_deref() == Some("BeagleBoard.org")
&& y.product.as_deref() == Some("BeagleConnect")
}
_ => false,
};
}
true
})
.map(|x| x.port_name)
}