diff --git a/embassy-usb/src/class/mod.rs b/embassy-usb/src/class/mod.rs index af27577a..464c3e85 100644 --- a/embassy-usb/src/class/mod.rs +++ b/embassy-usb/src/class/mod.rs @@ -1,3 +1,4 @@ pub mod cdc_acm; pub mod cdc_ncm; pub mod hid; +pub mod msc; diff --git a/embassy-usb/src/class/msc/mod.rs b/embassy-usb/src/class/msc/mod.rs new file mode 100644 index 00000000..129ebbd2 --- /dev/null +++ b/embassy-usb/src/class/msc/mod.rs @@ -0,0 +1,57 @@ +pub mod subclass; +pub mod transport; + +use core::marker::PhantomData; + +use crate::driver::Driver; +use crate::types::InterfaceNumber; +use crate::Builder; + +/// USB Mass Storage Class ID +/// +/// Section 4.3 [USB Bulk Only Transport Spec](https://www.usb.org/document-library/mass-storage-bulk-only-10) +pub const USB_CLASS_MSC: u8 = 0x08; + +/// Command set used by the MSC interface. +/// +/// Reported in `bInterfaceSubclass` field. +/// +/// Section 2 [USB Mass Storage Class Overview](https://www.usb.org/document-library/mass-storage-class-specification-overview-14) +#[derive(Clone, Copy, Eq, PartialEq, Debug)] +pub enum MscSubclass { + /// SCSI command set not reported. De facto use + ScsiCommandSetNotReported = 0x00, + /// Allocated by USB-IF for RBC. RBC is defined outside of USB + Rbc = 0x01, + /// Allocated by USB-IF for MMC-5. MMC-5 is defined outside of USB + Mmc5Atapi = 0x02, + /// Specifies how to interface Floppy Disk Drives to USB + Ufi = 0x04, + /// Allocated by USB-IF for SCSI. SCSI standards are defined outside of USB + ScsiTransparentCommandSet = 0x06, + /// LSDFS specifies how host has to negotiate access before trying SCSI + LsdFs = 0x07, + /// Allocated by USB-IF for IEEE 1667. IEEE 1667 is defined outside of USB + Ieee1667 = 0x08, + /// Specific to device vendor. De facto use + VendorSpecific = 0xFF, +} + +/// Transport protocol of the MSC interface. +/// +/// Reported in `bInterfaceProtocol` field. +/// +/// Section 3 [USB Mass Storage Class Overview](https://www.usb.org/document-library/mass-storage-class-specification-overview-14) +#[derive(Clone, Copy, Eq, PartialEq, Debug)] +pub enum MscProtocol { + /// USB Mass Storage Class Control/Bulk/Interrupt (CBI) Transport (with command completion interrupt) + CbiWithCCInterrupt = 0x00, + /// USB Mass Storage Class Control/Bulk/Interrupt (CBI) Transport (with no command completion interrupt) + CbiNoCCInterrupt = 0x01, + /// USB Mass Storage Class Bulk-Only (BBB) Transport + BulkOnlyTransport = 0x50, + /// Allocated by USB-IF for UAS. UAS is defined outside of USB + Uas = 0x62, + /// Specific to device vendor. De facto use + VendorSpecific = 0xFF, +} diff --git a/embassy-usb/src/class/msc/subclass/mod.rs b/embassy-usb/src/class/msc/subclass/mod.rs new file mode 100644 index 00000000..b967efab --- /dev/null +++ b/embassy-usb/src/class/msc/subclass/mod.rs @@ -0,0 +1 @@ +pub mod scsi; diff --git a/embassy-usb/src/class/msc/subclass/scsi/mod.rs b/embassy-usb/src/class/msc/subclass/scsi/mod.rs new file mode 100644 index 00000000..e69de29b diff --git a/embassy-usb/src/class/msc/transport/bulk_only/command_block_wrapper.rs b/embassy-usb/src/class/msc/transport/bulk_only/command_block_wrapper.rs new file mode 100644 index 00000000..f4cfb3bc --- /dev/null +++ b/embassy-usb/src/class/msc/transport/bulk_only/command_block_wrapper.rs @@ -0,0 +1,88 @@ +use core::mem::{size_of, size_of_val}; + +use embassy_usb_driver::Direction; + +/// Signature that identifies this packet as CBW +pub const CBW_SIGNATURE: u32 = 0x43425355; + +/// A wrapper that identifies a command sent from the host to the +/// device on the OUT endpoint. Describes the data transfer IN or OUT +/// that should happen immediatly after this wrapper is received. +/// Little Endian +#[repr(packed)] +#[derive(Clone, Copy, Eq, PartialEq, Debug)] +pub struct CommandBlockWrapper { + /// Signature that identifies this packet as CBW + /// Must contain 0x43425355 + pub signature: u32, + /// Tag sent by the host. Must be echoed back to host in tag + /// field of the command status wrapper sent after the command + /// has been executed/rejected. Host uses it to positively + /// associate a CSW with the corresponding CBW + pub tag: u32, + /// Number of bytes of data that the host expects to receive on + /// the IN or OUT endpoint (as indicated by the direction field) + /// during the execution of this command. If this field is zero, + /// must respond directly with CSW + pub data_transfer_length: u32, + /// Direction of transfer initiated by this command. + /// 0b0XXXXXXX = OUT from host to device + /// 0b1XXXXXXX = IN from device to host + /// X bits are obsolete or reserved + pub direction: u8, + /// The device Logical Unit Number (LUN) to which the command is + /// for. For devices that don't support multiple LUNs the host will + /// set this field to zero. + /// Devices that don't support multiple LUNS must not ignore this + /// field and apply all commands to LUN 0, [see General Problems with Commands](http://janaxelson.com/device_errors.htm) + pub lun: u8, + /// The number of valid bytes in data field + pub data_length: u8, + /// The command set specific data for this command + pub data: [u8; 16], +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum CommandBlockWrapperDeserializeError { + BufferTooShort, + InvalidSignature, + InvalidDirection, + InvalidDataLength, +} + +impl CommandBlockWrapper { + pub fn from_bytes(buf: &[u8]) -> Result { + if buf.len() < size_of::() { + return Err(CommandBlockWrapperDeserializeError::BufferTooShort); + } + + let cbw = unsafe { core::ptr::read(buf.as_ptr() as *const Self) }; + + if cbw.signature != CBW_SIGNATURE { + return Err(CommandBlockWrapperDeserializeError::InvalidSignature); + } + + if cbw.direction & 0b01111111 != 0 { + return Err(CommandBlockWrapperDeserializeError::InvalidDirection); + } + + if cbw.data_length as usize > size_of_val(&cbw.data) { + return Err(CommandBlockWrapperDeserializeError::InvalidDataLength); + } + + Ok(cbw) + } + + pub fn dir(&self) -> Direction { + if self.direction == 0x80 { + Direction::In + } else { + Direction::Out + } + } + + pub fn data(&self) -> &[u8] { + &self.data[..self.data_length as usize] + } +} diff --git a/embassy-usb/src/class/msc/transport/bulk_only/command_status_wrapper.rs b/embassy-usb/src/class/msc/transport/bulk_only/command_status_wrapper.rs new file mode 100644 index 00000000..57080b57 --- /dev/null +++ b/embassy-usb/src/class/msc/transport/bulk_only/command_status_wrapper.rs @@ -0,0 +1,66 @@ +use core::mem::size_of; +use core::ptr::copy_nonoverlapping; + +/// Signature that identifies this packet as CSW +pub const CSW_SIGNATURE: u32 = 0x53425355; + +/// The status of a command +#[derive(Clone, Copy, Eq, PartialEq, Debug)] +pub enum CommandStatus { + /// Ok, command completed successfully + CommandOk = 0x00, + /// Error, command failed + CommandError = 0x01, + /// Fatal device error, reset required + PhaseError = 0x02, +} + +/// A wrapper that identifies a command sent from the host to the +/// device on the OUT endpoint. Describes the data transfer IN or OUT +/// that should happen immediatly after this wrapper is received. +/// Little Endian +#[repr(packed)] +#[derive(Clone, Copy, Eq, PartialEq, Debug)] +pub struct CommandStatusWrapper { + /// Signature that identifies this packet as CSW + /// Must contain 0x53425355 + pub signature: u32, + /// Tag that matches this CSW back to the CBW that initiated it. + /// Must be copied from CBW tag field. Host uses it to positively + /// associate a CSW with the corresponding CBW + pub tag: u32, + /// Difference between the expected data length from CSW.data_transfer_length + /// and the the actual amount of data sent or received. Cannot be greater + /// than data_transfer_length. Non-zero for an OUT (host to device) transfer + /// likely means there was an error whereas non-zero on IN (device to host) may + /// mean the host allocated enough space for an extended/complete result but + /// a shorter result was sent. + pub data_residue: u32, + /// The status of the command + /// 0x00 = Command succeeded + /// 0x01 = Command failed + /// 0x02 = Phase error. Causes the host to perform a reset recovery on the + /// device. This indicates the device state machine has got messed up + /// or similar unrecoverable condition. Processing further CBWs without + /// a reset gives indeterminate results. + pub status: u8, +} + +impl CommandStatusWrapper { + pub fn new(tag: u32, data_residue: u32, status: CommandStatus) -> Self { + Self { + signature: CSW_SIGNATURE, + tag, + data_residue, + status: status as _, + } + } + + pub fn to_bytes<'d>(&self, buf: &'d mut [u8]) -> &'d [u8] { + let len = size_of::(); + + assert!(buf.len() >= len); + unsafe { copy_nonoverlapping(self as *const _ as *const u8, buf.as_mut_ptr(), len) } + &buf[..len] + } +} diff --git a/embassy-usb/src/class/msc/transport/bulk_only/mod.rs b/embassy-usb/src/class/msc/transport/bulk_only/mod.rs new file mode 100644 index 00000000..845688dd --- /dev/null +++ b/embassy-usb/src/class/msc/transport/bulk_only/mod.rs @@ -0,0 +1,268 @@ +pub mod command_block_wrapper; +pub mod command_status_wrapper; + +use core::mem::{size_of, MaybeUninit}; + +use embassy_usb_driver::{Direction, Endpoint, EndpointError, EndpointIn, EndpointOut}; + +use self::command_block_wrapper::CommandBlockWrapper; +use self::command_status_wrapper::{CommandStatus, CommandStatusWrapper}; +use super::{CommandError, CommandSetHandler, DataPipeError, DataPipeIn, DataPipeOut}; +use crate::class::msc::{MscProtocol, MscSubclass, USB_CLASS_MSC}; +use crate::control::{ControlHandler, InResponse, OutResponse, Request, RequestType}; +use crate::driver::Driver; +use crate::types::InterfaceNumber; +use crate::Builder; + +const REQ_GET_MAX_LUN: u8 = 0xFE; +const REQ_BULK_ONLY_RESET: u8 = 0xFF; + +pub struct State { + control: MaybeUninit, +} + +impl Default for State { + fn default() -> Self { + Self { + control: MaybeUninit::uninit(), + } + } +} + +pub struct Control { + max_lun: u8, +} + +impl ControlHandler for Control { + fn control_in<'a>(&'a mut self, req: Request, buf: &'a mut [u8]) -> InResponse<'a> { + match (req.request_type, req.request) { + (RequestType::Class, REQ_GET_MAX_LUN) => { + debug!("REQ_GET_MAX_LUN"); + buf[0] = self.max_lun; + InResponse::Accepted(&buf[..1]) + } + (RequestType::Class, REQ_BULK_ONLY_RESET) => { + debug!("REQ_BULK_ONLY_RESET"); + InResponse::Accepted(&[]) + } + _ => InResponse::Rejected, + } + } +} + +pub struct BulkOnlyTransport<'d, D: Driver<'d>, C: CommandSetHandler> { + msc_if: InterfaceNumber, + read_ep: D::EndpointOut, + write_ep: D::EndpointIn, + max_packet_size: u16, + handler: C, +} + +impl<'d, D: Driver<'d>, C: CommandSetHandler> BulkOnlyTransport<'d, D, C> { + pub fn new( + builder: &mut Builder<'d, D>, + state: &'d mut State, + subclass: MscSubclass, + max_packet_size: u16, + max_lun: u8, + handler: C, + ) -> Self { + assert!(max_lun < 16, "BulkOnlyTransport supports maximum 16 LUNs"); + + let control = state.control.write(Control { max_lun }); + + let mut func = builder.function(USB_CLASS_MSC, subclass as _, MscProtocol::BulkOnlyTransport as _); + + // Control interface + let mut iface = func.interface(); + iface.handler(control); + + let msc_if = iface.interface_number(); + let mut alt = iface.alt_setting(USB_CLASS_MSC, subclass as _, MscProtocol::BulkOnlyTransport as _); + + let read_ep = alt.endpoint_bulk_out(max_packet_size); + let write_ep = alt.endpoint_bulk_in(max_packet_size); + + Self { + msc_if, + read_ep, + write_ep, + max_packet_size, + handler, + } + } + + async fn receive_control_block_wrapper(&mut self) -> CommandBlockWrapper { + let mut cbw_buf = [0u8; size_of::()]; + + loop { + // CBW is always sent at a packet boundary and is a short packet of 31 bytes + match self.read_ep.read(&mut cbw_buf).await { + Ok(len) => { + if len != cbw_buf.len() { + error!("Invalid CBW length"); + } + + match CommandBlockWrapper::from_bytes(&cbw_buf) { + Ok(cbw) => return cbw, + Err(e) => { + error!("Invalid CBW: {:?}", e); + } + } + } + Err(e) => match e { + EndpointError::BufferOverflow => { + error!("Host sent too long CBW"); + } + EndpointError::Disabled => self.read_ep.wait_enabled().await, + }, + }; + } + } + + async fn send_csw(&mut self, csw: CommandStatusWrapper) { + let mut csw_buf = [0u8; size_of::()]; + match self.write_ep.write(csw.to_bytes(&mut csw_buf)).await { + Ok(_) => {} + Err(e) => error!("error sending CSW: {:?}", e), + } + } + + async fn handle_command_out(&mut self, cbw: CommandBlockWrapper) -> CommandStatusWrapper { + let mut pipe_out = BulkOnlyTransportDataPipeOut { + ep: &mut self.read_ep, + data_residue: cbw.data_transfer_length as _, + max_packet_size: self.max_packet_size, + last_packet_full: true, + }; + + let status = match self.handler.command_out(cbw.lun, cbw.data(), &mut pipe_out).await { + Ok(_) => CommandStatus::CommandOk, + Err(e) => match e { + CommandError::PipeError(e) => { + error!("data pipe error: {:?}", e); + CommandStatus::PhaseError + } + CommandError::CommandError => CommandStatus::CommandError, + }, + }; + + CommandStatusWrapper::new(cbw.tag, pipe_out.data_residue, status) + } + + async fn handle_command_in(&mut self, cbw: CommandBlockWrapper) -> CommandStatusWrapper { + let mut pipe_in = BulkOnlyTransportDataPipeIn { + ep: &mut self.write_ep, + data_residue: cbw.data_transfer_length as _, + max_packet_size: self.max_packet_size, + last_packet_full: true, + }; + + let status = match self.handler.command_in(cbw.lun, cbw.data(), &mut pipe_in).await { + Ok(_) => match pipe_in.finalize().await { + Ok(_) => CommandStatus::CommandOk, + Err(e) => { + error!("Error finalizing data pipe: {:?}", e); + CommandStatus::PhaseError + } + }, + Err(e) => match e { + CommandError::PipeError(e) => { + error!("data pipe error: {:?}", e); + CommandStatus::PhaseError + } + CommandError::CommandError => CommandStatus::CommandError, + }, + }; + + CommandStatusWrapper::new(cbw.tag, pipe_in.data_residue, status) + } + + pub async fn run(&mut self) { + loop { + let cbw = self.receive_control_block_wrapper().await; + trace!("received CBW"); + + let csw = match cbw.dir() { + Direction::Out => { + trace!("handle_command_out"); + self.handle_command_out(cbw).await + } + Direction::In => { + trace!("handle_command_in"); + self.handle_command_in(cbw).await + } + }; + + trace!("sending CSW"); + self.send_csw(csw).await; + } + } +} + +pub struct BulkOnlyTransportDataPipeIn<'d, E: EndpointIn> { + ep: &'d mut E, + // requested transfer size minus already transfered bytes + data_residue: u32, + max_packet_size: u16, + last_packet_full: bool, +} + +impl<'d, E: EndpointIn> DataPipeIn for BulkOnlyTransportDataPipeIn<'d, E> { + async fn write(&mut self, buf: &[u8]) -> Result<(), DataPipeError> { + if !self.last_packet_full { + return Err(DataPipeError::TransferFinalized); + } + + for chunk in buf.chunks(self.max_packet_size.into()) { + if self.data_residue < chunk.len() as _ { + return Err(DataPipeError::TransferSizeExceeded); + } + + self.ep.write(chunk).await?; + self.data_residue -= chunk.len() as u32; + self.last_packet_full = chunk.len() == self.max_packet_size.into(); + } + + Ok(()) + } +} + +impl<'d, E: EndpointIn> BulkOnlyTransportDataPipeIn<'d, E> { + async fn finalize(&mut self) -> Result<(), DataPipeError> { + // Send ZLP only if last packet was full and transfer size was not exhausted + if self.last_packet_full && self.data_residue != 0 { + self.ep.write(&[]).await?; + } + + Ok(()) + } +} + +pub struct BulkOnlyTransportDataPipeOut<'d, E: EndpointOut> { + ep: &'d mut E, + // requested transfer size minus already transfered bytes + data_residue: u32, + max_packet_size: u16, + last_packet_full: bool, +} + +impl<'d, E: EndpointOut> DataPipeOut for BulkOnlyTransportDataPipeOut<'d, E> { + async fn read(&mut self, buf: &mut [u8]) -> Result<(), DataPipeError> { + if !self.last_packet_full { + return Err(DataPipeError::TransferFinalized); + } + + for chunk in buf.chunks_mut(self.max_packet_size.into()) { + if self.data_residue < chunk.len() as _ { + return Err(DataPipeError::TransferSizeExceeded); + } + + self.ep.read(chunk).await?; + self.data_residue -= chunk.len() as u32; + self.last_packet_full = chunk.len() == self.max_packet_size.into(); + } + + Ok(()) + } +} diff --git a/embassy-usb/src/class/msc/transport/mod.rs b/embassy-usb/src/class/msc/transport/mod.rs new file mode 100644 index 00000000..f31085d9 --- /dev/null +++ b/embassy-usb/src/class/msc/transport/mod.rs @@ -0,0 +1,56 @@ +use embassy_usb_driver::EndpointError; + +pub mod bulk_only; + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum DataPipeError { + /// Exceeded the host requested transfer size + TransferSizeExceeded, + /// Transfer was finalized by sending a short (non-full) packet + TransferFinalized, + /// USB driver endpoint error + EndpointError(EndpointError), +} + +impl From for DataPipeError { + fn from(e: EndpointError) -> Self { + Self::EndpointError(e) + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum CommandError { + PipeError(DataPipeError), + CommandError, +} + +/// A pipe that allows [CommandSetHandler] to write command-specific data. +pub trait DataPipeIn { + /// Sends data to host. + /// + /// Must be called only once or in lengths multiple of maximum USB packet size. + /// Otherwise, incomplete USB packet is interpreted as end of transfer. + async fn write(&mut self, buf: &[u8]) -> Result<(), DataPipeError>; +} + +/// A pipe that allows [CommandSetHandler] to read command-specific data. +pub trait DataPipeOut { + /// Receives data to host. + /// + /// Must be called only once or in lengths multiple of maximum USB packet size. + /// Otherwise, incomplete USB packet is interpreted as end of transfer. + async fn read(&mut self, buf: &mut [u8]) -> Result<(), DataPipeError>; +} + +/// Implemented by mass storage subclasses (i.e. SCSI). +/// +/// This trait is tailored to bulk-only transport and may require changes for other transports. +pub trait CommandSetHandler { + /// Handles command where data is sent to device. + async fn command_out(&mut self, lun: u8, cmd: &[u8], pipe: &mut impl DataPipeOut) -> Result<(), CommandError>; + + /// Handles command where data is sent to host. + async fn command_in(&mut self, lun: u8, cmd: &[u8], pipe: &mut impl DataPipeIn) -> Result<(), CommandError>; +} diff --git a/embassy-usb/src/lib.rs b/embassy-usb/src/lib.rs index 096e8b07..18a92f8e 100644 --- a/embassy-usb/src/lib.rs +++ b/embassy-usb/src/lib.rs @@ -1,5 +1,6 @@ #![no_std] #![feature(type_alias_impl_trait)] +#![feature(async_fn_in_trait)] // This mod MUST go first, so that the others see its macros. pub(crate) mod fmt; diff --git a/examples/stm32f4/src/bin/usb_msc.rs b/examples/stm32f4/src/bin/usb_msc.rs new file mode 100644 index 00000000..fad13dd1 --- /dev/null +++ b/examples/stm32f4/src/bin/usb_msc.rs @@ -0,0 +1,137 @@ +#![no_std] +#![no_main] +#![feature(type_alias_impl_trait)] +#![feature(async_fn_in_trait)] + +use defmt::{panic, *}; +use embassy_executor::Spawner; +use embassy_stm32::time::mhz; +use embassy_stm32::usb_otg::{Driver, Instance}; +use embassy_stm32::{interrupt, Config}; +use embassy_usb::class::cdc_acm::{CdcAcmClass, State}; +use embassy_usb::class::msc::transport::bulk_only::BulkOnlyTransport; +use embassy_usb::class::msc::transport::CommandSetHandler; +use embassy_usb::class::msc::MscSubclass; +use embassy_usb::driver::EndpointError; +use embassy_usb::Builder; +use futures::future::join; +use {defmt_rtt as _, panic_probe as _}; + +struct CommandSet {} + +impl CommandSetHandler for CommandSet { + async fn command_out( + &mut self, + lun: u8, + cmd: &[u8], + pipe: &mut impl embassy_usb::class::msc::transport::DataPipeOut, + ) -> Result<(), embassy_usb::class::msc::transport::CommandError> { + info!("CMD_OUT: {:?}", cmd); + Ok(()) + } + + async fn command_in( + &mut self, + lun: u8, + cmd: &[u8], + pipe: &mut impl embassy_usb::class::msc::transport::DataPipeIn, + ) -> Result<(), embassy_usb::class::msc::transport::CommandError> { + info!("CMD_IN: {:?}", cmd); + Ok(()) + } +} + +#[embassy_executor::main] +async fn main(_spawner: Spawner) { + info!("Hello World!"); + + let mut config = Config::default(); + config.rcc.pll48 = true; + config.rcc.sys_ck = Some(mhz(48)); + + let p = embassy_stm32::init(config); + + // Create the driver, from the HAL. + let irq = interrupt::take!(OTG_FS); + let mut ep_out_buffer = [0u8; 256]; + let driver = Driver::new_fs(p.USB_OTG_FS, irq, p.PA12, p.PA11, &mut ep_out_buffer); + + // Create embassy-usb Config + let mut config = embassy_usb::Config::new(0xc0de, 0xcafe); + config.manufacturer = Some("Embassy"); + config.product = Some("USB-serial example"); + config.serial_number = Some("12345678"); + + // Required for windows compatiblity. + // https://developer.nordicsemi.com/nRF_Connect_SDK/doc/1.9.1/kconfig/CONFIG_CDC_ACM_IAD.html#help + config.device_class = 0xEF; + config.device_sub_class = 0x02; + config.device_protocol = 0x01; + config.composite_with_iads = true; + + // Create embassy-usb DeviceBuilder using the driver and config. + // It needs some buffers for building the descriptors. + let mut device_descriptor = [0; 256]; + let mut config_descriptor = [0; 256]; + let mut bos_descriptor = [0; 256]; + let mut control_buf = [0; 64]; + + let mut state = Default::default(); + + let mut builder = Builder::new( + driver, + config, + &mut device_descriptor, + &mut config_descriptor, + &mut bos_descriptor, + &mut control_buf, + None, + ); + + let mut msc = BulkOnlyTransport::new( + &mut builder, + &mut state, + MscSubclass::ScsiTransparentCommandSet, + 64, + 0, + CommandSet {}, + ); + + // Build the builder. + let mut usb = builder.build(); + + // Run the USB device. + let usb_fut = usb.run(); + + // Do stuff with the class! + let echo_fut = async { + loop { + msc.run().await; + } + }; + + // Run everything concurrently. + // If we had made everything `'static` above instead, we could do this using separate tasks instead. + join(usb_fut, echo_fut).await; +} + +struct Disconnected {} + +impl From for Disconnected { + fn from(val: EndpointError) -> Self { + match val { + EndpointError::BufferOverflow => panic!("Buffer overflow"), + EndpointError::Disabled => Disconnected {}, + } + } +} + +async fn echo<'d, T: Instance + 'd>(class: &mut CdcAcmClass<'d, Driver<'d, T>>) -> Result<(), Disconnected> { + let mut buf = [0; 64]; + loop { + let n = class.read_packet(&mut buf).await?; + let data = &buf[..n]; + info!("data: {:x}", data); + class.write_packet(data).await?; + } +}