Skip to content

Commit

Permalink
Merge pull request #27 from sjoerdsimons/async
Browse files Browse the repository at this point in the history
Add async support to dfu-core
  • Loading branch information
sjoerdsimons authored Nov 9, 2024
2 parents a5299a6 + 606af1c commit 231655f
Show file tree
Hide file tree
Showing 11 changed files with 737 additions and 204 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: test
args: --workspace
args: --workspace --all-features

test-windows:
runs-on: windows-latest
Expand All @@ -36,7 +36,7 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: test
args: --workspace
args: --workspace --all-features

test-macos:
runs-on: macos-latest
Expand All @@ -50,4 +50,4 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: test
args: --workspace
args: --workspace --all-features
6 changes: 3 additions & 3 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: test
args: --workspace
args: --workspace --all-features

- name: rustfmt
uses: actions-rs/cargo@v1
Expand Down Expand Up @@ -58,7 +58,7 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: test
args: --workspace
args: --workspace --all-features

test-macos:
runs-on: macos-latest
Expand All @@ -78,4 +78,4 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: test
args: --workspace
args: --workspace --all-features
15 changes: 14 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,37 @@ homepage = "https://github.com/dfu-rs/dfu-core"
documentation = "https://docs.rs/dfu-core"
readme = "README.md"
keywords = ["dfu", "sans-io", "nostd"]
autotests = false

[dependencies]
bytes = "1"
displaydoc = "0.2"
futures = {version = "0.3.31", optional = true }
log = "0.4"
pretty-hex = "0.3"
thiserror = { version = "1", optional = true }

[dev-dependencies]
dfu-core = { path = ".", features = [ "std" ] }
env_logger = "0.10.0"
futures-test = "0.3.31"
num-derive = "0.3.3"
num-traits = "0.2.15"
thiserror = "1"

[features]
std = ["thiserror"]
std = ["dep:thiserror"]
async = ["dep:futures", "std"]

[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]

[[test]]
name = "download"
path = "tests/download.rs"

[[test]]
name = "download_async"
path = "tests/download_async.rs"
required-features = [ "async"]
276 changes: 276 additions & 0 deletions src/asynchronous.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
use futures::{io::Cursor, AsyncRead, AsyncReadExt, AsyncSeek, AsyncSeekExt};

use super::*;
use core::future::Future;
use std::convert::TryFrom;
use std::prelude::v1::*;

/// Trait to implement lower level communication with a USB device.
pub trait DfuAsyncIo {
/// 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],
) -> impl Future<Output = Result<Self::Read, Self::Error>> + Send;

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

/// Triggers a USB reset.
fn usb_reset(&self) -> impl Future<Output = Result<Self::Reset, Self::Error>> + Send;

/// 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;
}

impl UsbReadControl<'_> {
/// Execute usb write using io
pub async fn execute_async<IO: DfuAsyncIo>(&mut self, io: &IO) -> Result<IO::Read, IO::Error> {
io.read_control(self.request_type, self.request, self.value, self.buffer)
.await
}
}

impl<D> UsbWriteControl<D>
where
D: AsRef<[u8]>,
{
/// Execute usb write using io
pub async fn execute_async<IO: DfuAsyncIo>(&self, io: &IO) -> Result<IO::Write, IO::Error> {
io.write_control(
self.request_type,
self.request,
self.value,
self.buffer.as_ref(),
)
.await
}
}

struct Buffer<R: AsyncRead + Unpin> {
reader: R,
buf: Box<[u8]>,
level: usize,
}

impl<R: AsyncRead + Unpin> Buffer<R> {
fn new(size: usize, reader: R) -> Self {
Self {
reader,
buf: vec![0; size].into_boxed_slice(),
level: 0,
}
}

async fn fill_buf(&mut self) -> Result<&[u8], std::io::Error> {
while self.level < self.buf.len() {
let dst = &mut self.buf[self.level..];
let r = self.reader.read(dst).await?;
if r == 0 {
break;
} else {
self.level += r;
}
}
Ok(&self.buf[0..self.level])
}

fn consume(&mut self, amt: usize) {
if amt >= self.level {
self.level = 0;
} else {
self.buf.copy_within(amt..self.level, 0);
self.level -= amt;
}
}
}

/// Generic asynchronous implementation of DFU.
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
pub struct DfuASync<IO, E>
where
IO: DfuAsyncIo<Read = usize, Write = usize, Reset = (), Error = E>,
E: From<std::io::Error> + From<Error>,
{
io: IO,
dfu: DfuSansIo,
buffer: Vec<u8>,
}

impl<IO, E> DfuASync<IO, E>
where
IO: DfuAsyncIo<Read = usize, Write = usize, Reset = (), Error = E>,
E: From<std::io::Error> + From<Error>,
{
/// Create a new instance of a generic synchronous implementation of DFU.
pub fn new(io: IO) -> Self {
let transfer_size = io.functional_descriptor().transfer_size as usize;
let descriptor = *io.functional_descriptor();

Self {
io,
dfu: DfuSansIo::new(descriptor),
buffer: vec![0x00; transfer_size],
}
}

/// Override the address onto which the firmware is downloaded.
///
/// This address is only used if the device uses the DfuSe protocol.
pub fn override_address(&mut self, address: u32) -> &mut Self {
self.dfu.set_address(address);
self
}

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

impl<IO, E> DfuASync<IO, E>
where
IO: DfuAsyncIo<Read = usize, Write = usize, Reset = (), Error = E>,
E: From<std::io::Error> + From<Error>,
{
/// Download a firmware into the device from a slice.
pub async fn download_from_slice(&mut self, slice: &[u8]) -> Result<(), IO::Error> {
let length = slice.len();
let cursor = Cursor::new(slice);

self.download(
cursor,
u32::try_from(length).map_err(|_| Error::OutOfCapabilities)?,
)
.await
}

/// Download a firmware into the device from a reader.
pub async fn download<R: AsyncReadExt + Unpin>(
&mut self,
reader: R,
length: u32,
) -> Result<(), IO::Error> {
let transfer_size = self.io.functional_descriptor().transfer_size as usize;
let mut reader = Buffer::new(transfer_size, reader);
let buffer = reader.fill_buf().await?;
if buffer.is_empty() {
return Ok(());
}

macro_rules! wait_status {
($cmd:expr) => {{
let mut cmd = $cmd;
loop {
cmd = match cmd.next() {
get_status::Step::Break(cmd) => break cmd,
get_status::Step::Wait(cmd, poll_timeout) => {
std::thread::sleep(std::time::Duration::from_millis(poll_timeout));
let (cmd, mut control) = cmd.get_status(&mut self.buffer);
let n = control.execute_async(&self.io).await?;
cmd.chain(&self.buffer[..n as usize])??
}
};
}
}};
}

let cmd = self.dfu.download(self.io.protocol(), length)?;
let (cmd, mut control) = cmd.get_status(&mut self.buffer);
let n = control.execute_async(&self.io).await?;
let (cmd, control) = cmd.chain(&self.buffer[..n])?;
if let Some(control) = control {
control.execute_async(&self.io).await?;
}
let (cmd, mut control) = cmd.get_status(&mut self.buffer);
let n = control.execute_async(&self.io).await?;
let mut download_loop = cmd.chain(&self.buffer[..n])??;

loop {
download_loop = match download_loop.next() {
download::Step::Break => break,
download::Step::Erase(cmd) => {
let (cmd, control) = cmd.erase()?;
control.execute_async(&self.io).await?;
wait_status!(cmd)
}
download::Step::SetAddress(cmd) => {
let (cmd, control) = cmd.set_address();
control.execute_async(&self.io).await?;
wait_status!(cmd)
}
download::Step::DownloadChunk(cmd) => {
let chunk = reader.fill_buf().await?;
let (cmd, control) = cmd.download(chunk)?;
let n = control.execute_async(&self.io).await?;
reader.consume(n);
wait_status!(cmd)
}
download::Step::UsbReset => {
log::trace!("Device reset");
self.io.usb_reset().await?;
break;
}
}
}

Ok(())
}

/// Download a firmware into the device.
///
/// The length is guess from the reader.
pub async fn download_all<R: AsyncReadExt + Unpin + AsyncSeek>(
&mut self,
mut reader: R,
) -> Result<(), IO::Error> {
let length = u32::try_from(reader.seek(std::io::SeekFrom::End(0)).await?)
.map_err(|_| Error::MaximumTransferSizeExceeded)?;
reader.seek(std::io::SeekFrom::Start(0)).await?;
self.download(reader, length).await
}

/// Send a Detach request to the device
pub async fn detach(&self) -> Result<(), IO::Error> {
self.dfu.detach().execute_async(&self.io).await?;
Ok(())
}

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

/// 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
}
}
17 changes: 7 additions & 10 deletions src/detach.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,19 @@ const DFU_DETACH: u8 = 0;

/// Command that sends `dfuDETACH` to the device.
#[must_use]
pub struct Detach<'dfu, IO: DfuIo, T> {
pub(crate) dfu: &'dfu DfuSansIo<IO>,
pub struct Detach<T> {
pub(crate) descriptor: FunctionalDescriptor,
pub(crate) chained_command: T,
}

impl<'dfu, IO: DfuIo, T> Detach<'dfu, IO, T> {
impl<T> Detach<T> {
/// Send the command `dfuDETACH` to the device.
pub fn detach(self) -> Result<(T, IO::Write), IO::Error> {
pub fn detach(self) -> (T, UsbWriteControl<[u8; 0]>) {
log::trace!("Detaching device");
let detach_timeout = self.dfu.io.functional_descriptor().detach_timeout;
let detach_timeout = self.descriptor.detach_timeout;
let next = self.chained_command;
let res = self
.dfu
.io
.write_control(REQUEST_TYPE, DFU_DETACH, detach_timeout, &[])?;
let control = UsbWriteControl::new(REQUEST_TYPE, DFU_DETACH, detach_timeout, []);

Ok((next, res))
(next, control)
}
}
Loading

0 comments on commit 231655f

Please sign in to comment.