Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add async support to dfu-core #27

Merged
merged 2 commits into from
Nov 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading