From cd8f92dcddb9fde4d2e702cc38b103fe9090ea46 Mon Sep 17 00:00:00 2001 From: Vitalii Bursov Date: Sat, 6 Apr 2024 20:35:43 +0300 Subject: [PATCH] Initial import --- .github/workflows/ci.yml | 80 +++ .github/workflows/publish-release.yml | 27 + .gitignore | 10 + CHANGELOG.md | 14 + Cargo.toml | 17 + README.md | 98 +++ src/bus.rs | 303 +++++++++ src/lib.rs | 898 ++++++++++++++++++++++++++ src/usbdata.rs | 138 ++++ tests/helper_example/mod.rs | 20 + tests/test_device.rs | 283 ++++++++ tests/test_device1/mod.rs | 129 ++++ tests/test_helper_example.rs | 42 ++ 13 files changed, 2059 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/publish-release.yml create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 src/bus.rs create mode 100644 src/lib.rs create mode 100644 src/usbdata.rs create mode 100644 tests/helper_example/mod.rs create mode 100644 tests/test_device.rs create mode 100644 tests/test_device1/mod.rs create mode 100644 tests/test_helper_example.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1ec49a2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,80 @@ +on: + push: + branches: [ main ] + pull_request: + workflow_dispatch: + +name: Continuous integration + +jobs: + + lints: + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - stable + - nightly + + steps: + - uses: actions/checkout@v3 + + - uses: dtolnay/rust-toolchain@master + id: toolchain + with: + toolchain: ${{ matrix.rust }} + components: rustfmt, clippy + + - run: cargo +${{steps.toolchain.outputs.name}} fmt --all -- --check + - run: cargo +${{steps.toolchain.outputs.name}} clippy --all + + build_only: + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - stable + - nightly + + steps: + - uses: actions/checkout@v3 + + - uses: dtolnay/rust-toolchain@master + id: toolchain + with: + toolchain: ${{ matrix.rust }} + targets: x86_64-unknown-linux-gnu + + - run: cargo +${{steps.toolchain.outputs.name}} build --target x86_64-unknown-linux-gnu + + tests: + needs: [build_only] + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - stable + - nightly + + steps: + - uses: actions/checkout@v3 + + - name: Install 32-bit build dependencies + run: | + sudo apt update + sudo apt install -y libc6-dev-i386 + + - uses: dtolnay/rust-toolchain@master + id: toolchain + with: + toolchain: ${{ matrix.rust }} + targets: "x86_64-unknown-linux-gnu,i686-unknown-linux-gnu" + + - run: cargo +${{steps.toolchain.outputs.name}} build --target x86_64-unknown-linux-gnu + - run: cargo +${{steps.toolchain.outputs.name}} test --target x86_64-unknown-linux-gnu + - run: cargo +${{steps.toolchain.outputs.name}} doc --target x86_64-unknown-linux-gnu + + - run: cargo clean + + - run: cargo +${{steps.toolchain.outputs.name}} build --target i686-unknown-linux-gnu + - run: cargo +${{steps.toolchain.outputs.name}} test --target i686-unknown-linux-gnu diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml new file mode 100644 index 0000000..dc76ecc --- /dev/null +++ b/.github/workflows/publish-release.yml @@ -0,0 +1,27 @@ +on: + push: + tags: + - "v0.[0-9]+.[0-9]+" + workflow_dispatch: + +name: Publish release tag to crates.io + +jobs: + + publish: + name: Publish + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Check package version + run: | + cat Cargo.toml | gawk -v ver="$GITHUB_REF_NAME" -F= 'BEGIN {res=1;p=0} /^\[/ {p=0} /^\[package\]/ {p=1} /^version/ {if (p) {gsub(/[" ]/,"", $2); fver="v"$2; if (fver==ver) {res=0}}} END {exit res}' + + - uses: dtolnay/rust-toolchain@stable + id: toolchain + targets: x86_64-unknown-linux-gnu + + - run: cargo +${{steps.toolchain.outputs.name}} publish --target x86_64-unknown-linux-gnu + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..088ba6b --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ee97d11 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,14 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.1.0] - 2024-04-06 + +First version. + +[Unreleased]: https://github.com/vitalyvb/usbd-class-tester/compare/v0.1.0...HEAD +[0.1.0]: https://github.com/vitalyvb/usbd-class-tester/releases/tag/v0.1.0 diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..f6aef0b --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "usbd-class-tester" +version = "0.1.0" +edition = "2021" + +description = "Library for testing usb-device device classes." +authors = ["Vitalii Bursov "] +readme = "README.md" +license = "MIT" +keywords = ["usb-device", "embedded", "testing"] +repository = "https://github.com/vitalyvb/usbd-class-tester" +exclude = [ + ".github", +] + +[dependencies.usb-device] +version = "0.3.2" diff --git a/README.md b/README.md new file mode 100644 index 0000000..78a289e --- /dev/null +++ b/README.md @@ -0,0 +1,98 @@ +# usbd-class-tester + +[![Crates.io](https://img.shields.io/crates/v/usbd-class-tester.svg)](https://crates.io/crates/usbd-class-tester) [![Docs.rs](https://docs.rs/usbd-class-tester/badge.svg)](https://docs.rs/usbd-class-tester) + +A library for running tests of `usb-device` classes on +developer's system natively. + +## About + +Testing is difficult, and if it's even more difficult +when it involves a dedicated hardware and doing +the test manually. Often a lot of stuff needs to be +re-tested even after small code changes. + +This library aims to help testing the implementation of +protocols in USB devices which are based on `usb-device` +crate by providing a means of simulating Host's accesses +to the device. + +Initial implementation was done for tests in `usbd-dfu` +crate. This library is based on that idea, but extends +it a lot. For example it adds a set of convenience +functions for Control transfers, while originally this +was done via plain `u8` arrays only. + +### Supported operations + +* IN and OUT EP0 control transfers + +### Not supported operations + +Almost everything else, including but not limited to: + +* Endpoints other than EP0 in `EmulatedUsbBus::poll()` +* Endpoint allocation in `EmulatedUsbBus::alloc_ep()` +* Reset +* Suspend and Resume +* Interrupt transfers +* Bulk transfers +* Iso transfers +* ... + +## License + +This project is licensed under [MIT License](https://opensource.org/licenses/MIT) +([LICENSE](https://github.com/vitalyvb/usbd-class-tester/blob/main/LICENSE)). + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally +submitted for inclusion in the work by you shall be licensed as above, +without any additional terms or conditions. + +## Example + +The example defines an empty `UsbClass` implementation for `TestUsbClass`. +Normally this would also include things like endpoint allocations, +device-specific descriptor generation, and the code handling everything. +This is not in the scope of this example. + +A minimal `TestCtx` creates `TestUsbClass` that will be passed to +a test case. In general, `TestCtx` allows some degree of environment +customization, like choosing EP0 transfer size, or redefining how +`UsbDevice` is created. + +Check crate tests directory for more examples. + +Also see the documentation for `usb-device`. + +``` +use usb_device::class_prelude::*; +use usbd_class_tester::prelude::*; + +// `UsbClass` under the test. +pub struct TestUsbClass {} +impl UsbClass for TestUsbClass {} + +// Context to create a testable instance of `TestUsbClass` +struct TestCtx {} +impl UsbDeviceCtx for TestCtx { + fn create_class<'a>( + &mut self, + alloc: &'a UsbBusAllocator, + ) -> AnyResult { + Ok(TestUsbClass {}) + } +} + +#[test] +fn test_interface_get_status() { + with_usb(TestCtx {}, |mut cls, mut dev| { + let st = dev.interface_get_status(&mut cls, 0).expect("status"); + assert_eq!(st, 0); + }) + .expect("with_usb"); +} +``` + diff --git a/src/bus.rs b/src/bus.rs new file mode 100644 index 0000000..6368c18 --- /dev/null +++ b/src/bus.rs @@ -0,0 +1,303 @@ +//! An implementation of a `UsbBus` which provides interaction +//! methods for the send and receiving data to/from the device +//! from USB Host perspective. +//! +//! This implementation is not complete and probably buggy. +//! +use std::{cell::RefCell, cmp::min, rc::Rc}; + +use usb_device::bus::PollResult; +use usb_device::endpoint::{EndpointAddress, EndpointType}; +use usb_device::{Result as UsbDeviceResult, UsbDirection, UsbError}; + +/// Holds a simulated Endpoint status which allows bi-directional +/// communication via 1024 byte buffers. +struct EndpointImpl { + alloc: bool, + stall: bool, + read_len: usize, + read: [u8; 1024], + read_ready: bool, + write_len: usize, + write: [u8; 1024], + write_done: bool, + setup: bool, + max_size: usize, +} + +impl EndpointImpl { + fn new() -> Self { + EndpointImpl { + alloc: false, + stall: false, + read_len: 0, + read: [0; 1024], + read_ready: false, + write_len: 0, + write: [0; 1024], + write_done: false, + setup: false, + max_size: 0, + } + } + + /// Sets data that will be read by usb-device from the Endpoint + fn set_read(&mut self, data: &[u8], setup: bool) { + self.read_len = data.len(); + if self.read_len > 0 { + self.read[..self.read_len].clone_from_slice(data); + self.setup = setup; + self.read_ready = true; + } + } + + /// Returns data that was written by usb-device to the Endpoint + fn get_write(&mut self, data: &mut [u8]) -> usize { + let res = self.write_len; + dbg!("g", self.write_len); + self.write_len = 0; + data[..res].clone_from_slice(&self.write[..res]); + self.write_done = true; + res + } +} + +/// Holds internal data like endpoints and provides +/// methods to access endpoint buffers like from +/// the "Host" side. +pub(crate) struct UsbBusImpl { + ep_i: [RefCell; 4], + ep_o: [RefCell; 4], +} + +impl UsbBusImpl { + pub(crate) fn new() -> Self { + Self { + ep_i: [ + RefCell::new(EndpointImpl::new()), + RefCell::new(EndpointImpl::new()), + RefCell::new(EndpointImpl::new()), + RefCell::new(EndpointImpl::new()), + ], + ep_o: [ + RefCell::new(EndpointImpl::new()), + RefCell::new(EndpointImpl::new()), + RefCell::new(EndpointImpl::new()), + RefCell::new(EndpointImpl::new()), + ], + } + } + + fn epidx(&self, ep_addr: EndpointAddress) -> &RefCell { + match ep_addr.direction() { + UsbDirection::In => self.ep_i.get(ep_addr.index()).unwrap(), + UsbDirection::Out => self.ep_o.get(ep_addr.index()).unwrap(), + } + } + + pub(crate) fn get_write(&self, ep_addr: EndpointAddress, data: &mut [u8]) -> usize { + let mut ep = self.epidx(ep_addr).borrow_mut(); + ep.get_write(data) + } + + pub(crate) fn set_read(&self, ep_addr: EndpointAddress, data: &[u8], setup: bool) { + let mut ep = self.epidx(ep_addr).borrow_mut(); + if setup && ep_addr.index() == 0 && ep_addr.direction() == UsbDirection::Out { + // setup packet on EP0OUT removes stall condition + ep.stall = false; + let mut ep0in = self.ep_i.get(0).unwrap().borrow_mut(); + ep0in.stall = false; + } + ep.set_read(data, setup) + } + + pub(crate) fn stalled0(&self) -> bool { + let in0 = EndpointAddress::from_parts(0, UsbDirection::In); + let out0 = EndpointAddress::from_parts(0, UsbDirection::Out); + { + let ep = self.epidx(in0).borrow(); + if ep.stall { + return true; + } + } + { + let ep = self.epidx(out0).borrow(); + if ep.stall { + return true; + } + } + false + } +} + +/// Implements `usb-device` UsbBus on top +/// of `UsbBusImpl`. +/// +/// Not thread-safe. +pub struct EmulatedUsbBus { + usb_address: RefCell, + bus: Rc>, +} + +unsafe impl Sync for EmulatedUsbBus {} + +impl EmulatedUsbBus { + pub(crate) fn new(bus: &Rc>) -> Self { + Self { + usb_address: RefCell::new(0), + bus: bus.clone(), + } + } + + fn bus_ref(&self) -> &RefCell { + self.bus.as_ref() + } + + /// Returns USB Address assigned to Device + /// by the Host. + pub fn get_address(&self) -> u8 { + *self.usb_address.borrow() + } +} + +impl usb_device::bus::UsbBus for EmulatedUsbBus { + fn alloc_ep( + &mut self, + _ep_dir: UsbDirection, + ep_addr: Option, + _ep_type: EndpointType, + max_packet_size: u16, + _interval: u8, + ) -> UsbDeviceResult { + if let Some(ea) = ep_addr { + let io = self.bus_ref().borrow(); + let mut sep = io.epidx(ea).borrow_mut(); + + if sep.alloc { + return Err(UsbError::InvalidEndpoint); + } + + sep.alloc = true; + sep.stall = false; + sep.max_size = max_packet_size as usize; + + Ok(ea) + } else { + // ep_addr is required, endpoint allocation is not implemented + Err(UsbError::EndpointMemoryOverflow) + } + } + + fn enable(&mut self) {} + + fn force_reset(&self) -> UsbDeviceResult<()> { + Err(UsbError::Unsupported) + } + + fn poll(&self) -> PollResult { + let in0 = EndpointAddress::from_parts(0, UsbDirection::In); + let out0 = EndpointAddress::from_parts(0, UsbDirection::Out); + + let io = self.bus_ref().borrow(); + let ep0out = io.epidx(out0).borrow(); + let mut ep0in = io.epidx(in0).borrow_mut(); + + let ep0_write_done = ep0in.write_done; + let ep0_can_read = ep0out.read_ready | ep0in.read_ready; + let ep0_setup = ep0out.setup; + + ep0in.write_done = false; + // dbg!(ep0out.read_ready , ep0in.read_ready); + + dbg!(ep0_write_done, ep0_can_read, ep0_setup); + + if ep0_write_done || ep0_can_read || ep0_setup { + PollResult::Data { + ep_in_complete: if ep0_write_done { 1 } else { 0 }, + ep_out: if ep0_can_read { 1 } else { 0 }, + ep_setup: if ep0_setup { 1 } else { 0 }, + } + } else { + PollResult::None + } + } + + fn read(&self, ep_addr: EndpointAddress, buf: &mut [u8]) -> UsbDeviceResult { + let io = self.bus_ref().borrow(); + let mut ep = io.epidx(ep_addr).borrow_mut(); + let len = min(buf.len(), min(ep.read_len, ep.max_size)); + + dbg!("read len from", buf.len(), len, ep_addr); + + if len == 0 { + return Err(UsbError::WouldBlock); + } + + buf[..len].clone_from_slice(&ep.read[..len]); + + ep.read_len -= len; + ep.read.copy_within(len.., 0); + + if ep.read_len == 0 { + ep.setup = false; + } + + ep.read_ready = ep.read_len > 0; + + Ok(len) + } + + fn reset(&self) { + todo!() + } + + fn resume(&self) { + todo!() + } + + fn suspend(&self) { + todo!() + } + + fn set_device_address(&self, addr: u8) { + self.usb_address.replace(addr); + } + + fn is_stalled(&self, ep_addr: EndpointAddress) -> bool { + let io = self.bus_ref().borrow(); + let ep = io.epidx(ep_addr).borrow(); + ep.stall + } + + fn set_stalled(&self, ep_addr: EndpointAddress, stalled: bool) { + let io = self.bus_ref().borrow(); + let mut ep = io.epidx(ep_addr).borrow_mut(); + ep.stall = stalled; + } + + fn write(&self, ep_addr: EndpointAddress, buf: &[u8]) -> UsbDeviceResult { + let io = self.bus_ref().borrow(); + let mut ep = io.epidx(ep_addr).borrow_mut(); + let offset = ep.write_len; + let mut len = 0; + + dbg!("write", buf.len()); + + if buf.len() > ep.max_size { + return Err(UsbError::BufferOverflow); + } + + for (i, e) in ep.write[offset..].iter_mut().enumerate() { + if i >= buf.len() { + break; + } + *e = buf[i]; + len += 1; + } + + dbg!("wrote", len); + ep.write_len += len; + ep.write_done = false; + Ok(len) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..0c87d30 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,898 @@ +#![allow(clippy::test_attr_in_doctest)] +//! +//! A library for running tests of `usb-device` classes on +//! developer's system natively. +//! +//! ## About +//! +//! Testing is difficult, and if it's even more difficult +//! when it involves a dedicated hardware and doing +//! the test manually. Often a lot of stuff needs to be +//! re-tested even after small code changes. +//! +//! This library aims to help testing the implementation of +//! protocols in USB devices which are based on `usb-device` +//! crate by providing a means of simulating Host's accesses +//! to the device. +//! +//! Initial implementation was done for tests in `usbd-dfu` +//! crate. This library is based on that idea, but extends +//! it a lot. For example it adds a set of convenience +//! functions for Control transfers, while originally this +//! was done via plain `u8` arrays only. +//! +//! ### Supported operations +//! +//! * IN and OUT EP0 control transfers +//! +//! ### Not supported operations +//! +//! Almost everything else, including but not limited to: +//! +//! * Endpoints other than EP0 in `EmulatedUsbBus::poll()` +//! * Endpoint allocation in `EmulatedUsbBus::alloc_ep()` +//! * Reset +//! * Suspend and Resume +//! * Interrupt transfers +//! * Bulk transfers +//! * Iso transfers +//! * ... +//! +//! ## License +//! +//! This project is licensed under [MIT License](https://opensource.org/licenses/MIT) +//! ([LICENSE](https://github.com/vitalyvb/usbd-class-tester/blob/main/LICENSE)). +//! +//! ### Contribution +//! +//! Unless you explicitly state otherwise, any contribution intentionally +//! submitted for inclusion in the work by you shall be licensed as above, +//! without any additional terms or conditions. +//! +//! ## Example +//! +//! The example defines an empty `UsbClass` implementation for `TestUsbClass`. +//! Normally this would also include things like endpoint allocations, +//! device-specific descriptor generation, and the code handling everything. +//! This is not in the scope of this example. +//! +//! A minimal `TestCtx` creates `TestUsbClass` that will be passed to +//! a test case. In general, `TestCtx` allows some degree of environment +//! customization, like choosing EP0 transfer size, or redefining how +//! `UsbDevice` is created. +//! +//! Check crate tests directory for more examples. +//! +//! Also see the documentation for `usb-device`. +//! +//! ``` +//! use usb_device::class_prelude::*; +//! use usbd_class_tester::prelude::*; +//! +//! // `UsbClass` under the test. +//! pub struct TestUsbClass {} +//! impl UsbClass for TestUsbClass {} +//! +//! // Context to create a testable instance of `TestUsbClass` +//! struct TestCtx {} +//! impl UsbDeviceCtx for TestCtx { +//! fn create_class<'a>( +//! &mut self, +//! alloc: &'a UsbBusAllocator, +//! ) -> AnyResult { +//! Ok(TestUsbClass {}) +//! } +//! } +//! +//! #[test] +//! fn test_interface_get_status() { +//! with_usb(TestCtx {}, |mut cls, mut dev| { +//! let st = dev.interface_get_status(&mut cls, 0).expect("status"); +//! assert_eq!(st, 0); +//! }) +//! .expect("with_usb"); +//! } +//! ``` +//! + +use std::fmt::Debug; +use std::marker::PhantomData; +use std::{cell::RefCell, rc::Rc}; + +use usb_device::bus::{UsbBus, UsbBusAllocator}; +use usb_device::class::UsbClass; +use usb_device::device::{StringDescriptors, UsbDevice, UsbDeviceBuilder, UsbVidPid}; +use usb_device::endpoint::EndpointAddress; +use usb_device::prelude::BuilderError; +use usb_device::UsbDirection; + +mod bus; +use bus::*; + +mod usbdata; +use usbdata::*; + +pub mod prelude { + pub use crate::bus::EmulatedUsbBus; + pub use crate::usbdata::{CtrRequestType, SetupPacket}; + pub use crate::{with_usb, AnyResult, AnyUsbError, Device, UsbDeviceCtx}; +} + +const DEFAULT_EP0_SIZE: u8 = 8; +const DEFAULT_ADDRESS: u8 = 5; + +/// Possible errors or other abnormal +/// conditions. +#[derive(Debug, PartialEq)] +pub enum AnyUsbError { + /// EP0 Stalled. Not necessarily an error, + /// the Device rejected EP0 transaction. + /// Next request should clear Stall for EP0. + EP0Stalled, + /// Error while reading from EP0. + /// No data or data limit reached. + /// Usually, this is some internal error. + EP0ReadFailed, + /// Bad reply length for GET_STATUS control request. + /// Length should be 2. + /// Usually, this is some internal error. + EP0BadGetStatusSize, + /// Bad reply length for GET_CONFIGURATION control request. + /// Length should be 1. + /// Usually, this is some internal error. + EP0BadGetConfigSize, + /// Failed to convert one data representation + /// to another, e.g. with TryInto. + /// Usually, this is some internal error. + DataConversion, + /// SET_ADDRESS didn't work during Device setup. + /// Usually, this is some internal error. + SetAddressFailed, + /// + InvalidDescriptorLength, + /// + InvalidDescriptorType, + /// + InvalidStringLength, + /// Wrapper for `BuilderError` of `usb-device` + /// when `UsbDeviceBuilder` fails. + UsbDeviceBuilder(BuilderError), + /// User-defined meaning. + /// Enum value is passed through, not used by the library. + UserDefined1, + /// User-defined meaning + /// Enum value is passed through, not used by the library. + UserDefined2, + /// User-defined meaning + /// Enum value is passed through, not used by the library. + UserDefinedU64(u64), + /// User-defined meaning + /// Enum value is passed through, not used by the library. + UserDefinedString(String), +} + +pub type AnyResult = core::result::Result; + +/// A context for the test, provides some +/// configuration values, initialization, +/// and some customization. +pub trait UsbDeviceCtx> { + /// EP0 size used by `build_usb_device()` when creating + /// `UsbDevice`. + /// + /// Incorrect values should cause `UsbDeviceBuilder` to + /// fail. + const EP0_SIZE: u8 = DEFAULT_EP0_SIZE; + + /// Address the Device gets assigned. + /// + /// A properly configured Device should get + /// a non-zero address. + const ADDRESS: u8 = DEFAULT_ADDRESS; + + /// Create `UsbClass` object. + /// # Example + /// ``` + /// # use usb_device::class_prelude::*; + /// # use usbd_class_tester::prelude::*; + /// # struct TestUsbClass {} + /// # impl TestUsbClass { + /// # pub fn new(_alloc: &UsbBusAllocator) -> Self {Self {}} + /// # } + /// # trait DOC { + /// fn create_class<'a>( + /// &mut self, + /// alloc: &'a UsbBusAllocator, + /// ) -> AnyResult { + /// Ok(TestUsbClass::new(&alloc)) + /// } + /// # } + /// ``` + fn create_class(&mut self, alloc: &UsbBusAllocator) -> AnyResult; + + /// Optional. Called after each `usb-device` `poll()`. + /// + /// Default implementation does nothing. + fn post_poll(&mut self, _cls: &mut C) {} + + /// Optional. If returns `true`, `Device::setup()` is not + /// called to initialize and enumerate device in + /// `with_usb()`. + /// + /// `with_usb()`'s `case` will be called with a + /// non-configured/non-enumerated device. + /// + /// Default implementation always returns `false`. + fn skip_setup(&mut self) -> bool { + false + } + + /// Optional. Implementation overrides the creation of `UsbDevice` + /// if the default implementation needs changing. + /// # Example + /// ``` + /// # use usb_device::prelude::*; + /// # use usb_device::class_prelude::*; + /// # use usbd_class_tester::AnyResult; + /// # trait DOC { + /// fn build_usb_device<'a>(&mut self, alloc: &'a UsbBusAllocator) -> AnyResult> { + /// let usb_dev = UsbDeviceBuilder::new(alloc, UsbVidPid(0, 0)) + /// // .strings() + /// // .max_packet_size_0() + /// // ... + /// .build(); + /// Ok(usb_dev) + /// } + /// # } + /// ```` + fn build_usb_device<'a>( + &mut self, + alloc: &'a UsbBusAllocator, + ) -> AnyResult> { + let usb_dev = UsbDeviceBuilder::new(alloc, UsbVidPid(0x1234, 0x5678)) + .strings(&[StringDescriptors::default() + .manufacturer("TestManufacturer") + .product("TestProduct") + .serial_number("TestSerial")]) + .map_err(AnyUsbError::UsbDeviceBuilder)? + .device_release(0x0200) + .self_powered(true) + .max_power(250) + .map_err(AnyUsbError::UsbDeviceBuilder)? + .max_packet_size_0(Self::EP0_SIZE) + .map_err(AnyUsbError::UsbDeviceBuilder)? + .build(); + + Ok(usb_dev) + } +} + +/// Represents Host's view of the Device via +/// USB bus. +pub struct Device<'a, C, X> +where + C: UsbClass, + X: UsbDeviceCtx, +{ + ctx: X, + usb: &'a RefCell, + dev: UsbDevice<'a, EmulatedUsbBus>, + _cls: PhantomData, +} + +impl<'a, C, X> Device<'a, C, X> +where + C: UsbClass, + X: UsbDeviceCtx, +{ + fn new(usb: &'a RefCell, ctx: X, dev: UsbDevice<'a, EmulatedUsbBus>) -> Self { + Device { + usb, + ctx, + dev, + _cls: PhantomData, + } + } + + /// Provides direct access to `EmulatedUsbBus` + pub fn usb_dev(&mut self) -> &mut UsbDevice<'a, EmulatedUsbBus> { + &mut self.dev + } + + /// Perform EP0 Control transfer. `setup` is `SetupPacket`. + /// If transfer is Host-to-device and + /// `data` is `Some`, then it's sent after the Setup packet + /// and Device can receive it as a payload. For Device-to-host + /// transfers `data` should be `None` and `out` must have + /// enough space to store the response. + pub fn ep0( + &mut self, + d: &mut C, + setup: SetupPacket, + data: Option<&[u8]>, + out: &mut [u8], + ) -> core::result::Result { + let setup_bytes: [u8; 8] = setup.into(); + self.ep0_raw(d, &setup_bytes, data, out) + } + + /// Perform raw EP0 Control transfer. `setup_bytes` is a + /// 8-byte Setup packet. If transfer is Host-to-device and + /// `data` is `Some`, then it's sent after the Setup packet + /// and Device can receive it as a payload. For Device-to-host + /// transfers `data` should be `None` and `out` must have + /// enough space to store the response. + pub fn ep0_raw( + &mut self, + d: &mut C, + setup_bytes: &[u8], + data: Option<&[u8]>, + out: &mut [u8], + ) -> core::result::Result { + let out0 = EndpointAddress::from_parts(0, UsbDirection::Out); + let in0 = EndpointAddress::from_parts(0, UsbDirection::In); + + self.usb.borrow().set_read(out0, setup_bytes, true); + self.dev.poll(&mut [d]); + self.ctx.post_poll(d); + if self.usb.borrow().stalled0() { + return Err(AnyUsbError::EP0Stalled); + } + + if let Some(val) = data { + self.usb.borrow().set_read(out0, val, false); + for i in 1..100 { + let res = self.dev.poll(&mut [d]); + self.ctx.post_poll(d); + if !res { + break; + } + if i >= 99 { + return Err(AnyUsbError::EP0ReadFailed); + } + } + if self.usb.borrow().stalled0() { + return Err(AnyUsbError::EP0Stalled); + } + }; + + let mut len = 0; + + loop { + let one = self.usb.borrow().get_write(in0, &mut out[len..]); + self.dev.poll(&mut [d]); + self.ctx.post_poll(d); + if self.usb.borrow().stalled0() { + return Err(AnyUsbError::EP0Stalled); + } + + len += one; + if one < DEFAULT_EP0_SIZE as usize { + // short read - last block + break; + } + } + + Ok(len) + } + + /// Perform EP0 Control transfer. + /// If transfer is Host-to-device and + /// `data` is `Some`, then it's sent after the Setup packet + /// and Device can receive it as a payload. For Device-to-host + /// transfers `data` should be `None` and the response + /// is returned in a result `Vec`. + #[allow(clippy::too_many_arguments)] + pub fn ep_io_control( + &mut self, + cls: &mut C, + reqt: CtrRequestType, + req: u8, + value: u16, + index: u16, + length: u16, + data: Option<&[u8]>, + ) -> core::result::Result, AnyUsbError> { + let mut buf: Vec = vec![0; length as usize]; + + let setup = SetupPacket::new(reqt, req, value, index, length); + + let len = self.ep0(cls, setup, data, buf.as_mut_slice())?; + buf.truncate(len); + Ok(buf) + } + + /// Perform Device-to-host EP0 Control transfer. + /// The response is returned in a result `Vec`. + /// + /// `reqt` is passed as is. It should be `to_host()`. + pub fn control_read( + &mut self, + cls: &mut C, + reqt: CtrRequestType, + req: u8, + value: u16, + index: u16, + length: u16, + ) -> core::result::Result, AnyUsbError> { + self.ep_io_control(cls, reqt, req, value, index, length, None) + } + + /// Perform Host-to-device EP0 Control transfer. + /// `data` is sent after the Setup packet + /// and Device can receive it as a payload. + /// The response is returned in a result `Vec` + /// and normally it should be empty. + /// + /// `reqt` is passed as is. It should be `to_device()`. + #[allow(clippy::too_many_arguments)] + pub fn control_write( + &mut self, + cls: &mut C, + reqt: CtrRequestType, + req: u8, + value: u16, + index: u16, + length: u16, + data: &[u8], + ) -> core::result::Result, AnyUsbError> { + self.ep_io_control(cls, reqt, req, value, index, length, Some(data)) + } + + /// Standard Device Request: GET_STATUS (0x00) + pub fn device_get_status(&mut self, cls: &mut C) -> core::result::Result { + let data = self.control_read(cls, CtrRequestType::to_host(), 0, 0, 0, 2)?; + if data.len() != 2 { + return Err(AnyUsbError::EP0BadGetStatusSize); + } + + let res = data.try_into().map_err(|_| AnyUsbError::DataConversion)?; + Ok(u16::from_le_bytes(res)) + } + + /// Standard Device Request: CLEAR_FEATURE (0x01) + pub fn device_clear_feature( + &mut self, + cls: &mut C, + feature: u16, + ) -> core::result::Result<(), AnyUsbError> { + self.control_write(cls, CtrRequestType::to_device(), 1, feature, 0, 0, &[]) + .and(Ok(())) + } + + /// Standard Device Request: SET_FEATURE (0x03) + pub fn device_set_feature( + &mut self, + cls: &mut C, + feature: u16, + ) -> core::result::Result<(), AnyUsbError> { + self.control_write(cls, CtrRequestType::to_device(), 3, feature, 0, 0, &[]) + .and(Ok(())) + } + + /// Standard Device Request: SET_ADDRESS (0x05) + pub fn device_set_address( + &mut self, + cls: &mut C, + address: u8, + ) -> core::result::Result<(), AnyUsbError> { + self.control_write( + cls, + CtrRequestType::to_device(), + 5, + address as u16, + 0, + 0, + &[], + ) + .and(Ok(())) + } + + /// Standard Device Request: GET_DESCRIPTOR (0x06) + pub fn device_get_descriptor( + &mut self, + cls: &mut C, + dtype: u8, + dindex: u8, + lang_id: u16, + length: u16, + ) -> core::result::Result, AnyUsbError> { + let typeindex: u16 = ((dtype as u16) << 8) | dindex as u16; + self.control_read( + cls, + CtrRequestType::to_host(), + 6, + typeindex, + lang_id, + length, + ) + } + + /// Get String descriptor from the device and return + /// unicode string. + /// + /// Standard Device Request: GET_DESCRIPTOR (0x06) + pub fn device_get_string( + &mut self, + cls: &mut C, + index: u8, + lang_id: u16, + ) -> core::result::Result { + let typeindex: u16 = (3u16 << 8) | index as u16; + let descr = + self.control_read(cls, CtrRequestType::to_host(), 6, typeindex, lang_id, 255)?; + + if descr.len() < 2 { + return Err(AnyUsbError::InvalidDescriptorLength); + } + + if descr[0] as usize != descr.len() { + return Err(AnyUsbError::InvalidDescriptorLength); + } + + if descr[1] != 3 { + return Err(AnyUsbError::InvalidDescriptorType); + } + + if descr[0] % 2 != 0 { + return Err(AnyUsbError::InvalidStringLength); + } + + let vu16: Vec = descr[2..] + .chunks(2) + .map(|c| u16::from_le_bytes([c[0], c[1]])) + .collect(); + let res = String::from_utf16(&vu16).map_err(|_| AnyUsbError::DataConversion)?; + + Ok(res) + } + + /// Standard Device Request: SET_DESCRIPTOR (0x07) + pub fn device_set_descriptor( + &mut self, + cls: &mut C, + dtype: u8, + dindex: u8, + lang_id: u16, + length: u16, + data: &[u8], + ) -> core::result::Result<(), AnyUsbError> { + let typeindex: u16 = ((dtype as u16) << 8) | dindex as u16; + self.control_write( + cls, + CtrRequestType::to_device(), + 7, + typeindex, + lang_id, + length, + data, + ) + .and(Ok(())) + } + + /// Standard Device Request: GET_CONFIGURATION (0x08) + pub fn device_get_configuration( + &mut self, + cls: &mut C, + ) -> core::result::Result { + let res = self.control_read(cls, CtrRequestType::to_host(), 8, 0, 0, 1)?; + if res.len() != 1 { + return Err(AnyUsbError::EP0BadGetConfigSize); + } + Ok(res[0]) + } + + /// Standard Device Request: SET_CONFIGURATION (0x09) + pub fn device_set_configuration( + &mut self, + cls: &mut C, + configuration: u8, + ) -> core::result::Result<(), AnyUsbError> { + self.control_write( + cls, + CtrRequestType::to_device(), + 9, + configuration as u16, + 0, + 0, + &[], + ) + .and(Ok(())) + } + + /// Standard Interface Request: GET_STATUS (0x00) + pub fn interface_get_status( + &mut self, + cls: &mut C, + interface: u8, + ) -> core::result::Result { + let data = self.control_read( + cls, + CtrRequestType::to_host().interface(), + 0, + 0, + interface as u16, + 2, + )?; + if data.len() != 2 { + return Err(AnyUsbError::EP0BadGetStatusSize); + } + + let res = data.try_into().map_err(|_| AnyUsbError::DataConversion)?; + Ok(u16::from_le_bytes(res)) + } + + /// Standard Interface Request: CLEAR_FEATURE (0x01) + pub fn interface_clear_feature( + &mut self, + cls: &mut C, + interface: u8, + feature: u16, + ) -> core::result::Result<(), AnyUsbError> { + self.control_write( + cls, + CtrRequestType::to_device().interface(), + 1, + feature, + interface as u16, + 0, + &[], + ) + .and(Ok(())) + } + + /// Standard Interface Request: SET_FEATURE (0x03) + pub fn interface_set_feature( + &mut self, + cls: &mut C, + interface: u8, + feature: u16, + ) -> core::result::Result<(), AnyUsbError> { + self.control_write( + cls, + CtrRequestType::to_device().interface(), + 3, + feature, + interface as u16, + 0, + &[], + ) + .and(Ok(())) + } + + /// Standard Interface Request: GET_INTERFACE (0x0a) + pub fn interface_get_interface( + &mut self, + cls: &mut C, + ) -> core::result::Result { + let res = self.control_read(cls, CtrRequestType::to_host().interface(), 10, 0, 0, 1)?; + if res.len() != 1 { + return Err(AnyUsbError::EP0BadGetConfigSize); + } + Ok(res[0]) + } + + /// Standard Interface Request: SET_INTERFACE (0x0b) + pub fn interface_set_interface( + &mut self, + cls: &mut C, + interface: u8, + alt_setting: u8, + ) -> core::result::Result<(), AnyUsbError> { + self.control_write( + cls, + CtrRequestType::to_device().interface(), + 11, + alt_setting as u16, + interface as u16, + 0, + &[], + ) + .and(Ok(())) + } + + /// Standard Endpoint Request: GET_STATUS (0x00) + pub fn endpoint_get_status( + &mut self, + cls: &mut C, + endpoint: u8, + ) -> core::result::Result { + let data = self.control_read( + cls, + CtrRequestType::to_host().endpoint(), + 0, + 0, + endpoint as u16, + 2, + )?; + if data.len() != 2 { + return Err(AnyUsbError::EP0BadGetStatusSize); + } + + let res = data.try_into().map_err(|_| AnyUsbError::DataConversion)?; + Ok(u16::from_le_bytes(res)) + } + + /// Standard Endpoint Request: CLEAR_FEATURE (0x01) + pub fn endpoint_clear_feature( + &mut self, + cls: &mut C, + endpoint: u8, + feature: u16, + ) -> core::result::Result<(), AnyUsbError> { + self.control_write( + cls, + CtrRequestType::to_device().endpoint(), + 1, + feature, + endpoint as u16, + 0, + &[], + ) + .and(Ok(())) + } + + /// Standard Endpoint Request: SET_FEATURE (0x03) + pub fn endpoint_set_feature( + &mut self, + cls: &mut C, + endpoint: u8, + feature: u16, + ) -> core::result::Result<(), AnyUsbError> { + self.control_write( + cls, + CtrRequestType::to_device().endpoint(), + 3, + feature, + endpoint as u16, + 0, + &[], + ) + .and(Ok(())) + } + + /// Standard Endpoint Request: SYNCH_FRAME (0x0c) + pub fn endpoint_synch_frame( + &mut self, + cls: &mut C, + endpoint: u8, + ) -> core::result::Result { + let data = self.control_read( + cls, + CtrRequestType::to_host().endpoint(), + 12, + 0, + endpoint as u16, + 2, + )?; + if data.len() != 2 { + return Err(AnyUsbError::EP0BadGetStatusSize); + } + + let res = data.try_into().map_err(|_| AnyUsbError::DataConversion)?; + Ok(u16::from_le_bytes(res)) + } + + /// Setup device approximately as Host would do. + /// + /// This gets some standard descriptors from the device + /// and performs standard configuration - sets + /// Device address and sets Device configuration + /// to `1`. + /// + /// This is performed automatically unless disabled + /// by `UsbDeviceCtx`. + /// + /// USB reset during enumeration is not performed. + pub fn setup(&mut self, cls: &mut C) -> core::result::Result<(), AnyUsbError> { + let mut vec; + + // get device descriptor for max ep0 size + // we ignore result. + self.device_get_descriptor(cls, 1, 0, 0, 64)?; + + // todo: reset device + + // set address + self.device_set_address(cls, X::ADDRESS)?; + if self.dev.bus().get_address() != X::ADDRESS { + return Err(AnyUsbError::SetAddressFailed); + } + + // get device descriptor again + let devd = self.device_get_descriptor(cls, 1, 0, 0, 18)?; + + // get configuration descriptor for size + vec = self.device_get_descriptor(cls, 2, 0, 0, 9)?; + let conf_desc_len = u16::from_le_bytes([vec[2], vec[3]]); + + // get configuration descriptor + // we ignore result. + self.device_get_descriptor(cls, 2, 0, 0, conf_desc_len)?; + + // get string languages + vec = self.device_get_descriptor(cls, 3, 0, 0, 255)?; + let lang_id = u16::from_le_bytes([vec[2], vec[3]]); + + // get string descriptors from device descriptor + for sid in devd[14..17].iter() { + if *sid != 0 { + self.device_get_descriptor(cls, 3, *sid, lang_id, 255)?; + //println!("========== {:?} {}", vec, sid); + } + } + + // set configuration + self.device_set_configuration(cls, 1)?; + + Ok(()) + } +} + +/// Initialize USB device Class `C` according to the provided +/// context `X` and run `case()` on it. +/// +/// `case` will not be called if `with_usb` encounters a +/// problem during the setup, in this case `with_usb` returns +/// an error. +/// +/// # Example +/// ``` +/// use usb_device::class_prelude::*; +/// use usbd_class_tester::prelude::*; +/// +/// pub struct TestUsbClass {} +/// impl UsbClass for TestUsbClass {} +/// +/// struct TestCtx {} +/// impl TestCtx {} +/// +/// impl UsbDeviceCtx for TestCtx { +/// fn create_class<'a>( +/// &mut self, +/// alloc: &'a UsbBusAllocator, +/// ) -> AnyResult { +/// Ok(TestUsbClass {}) +/// } +/// } +/// +/// #[test] +/// fn test_interface_get_status() { +/// with_usb(TestCtx {}, |mut cls, mut dev| { +/// let st = dev.interface_get_status(&mut cls, 0).expect("status"); +/// assert_eq!(st, 0); +/// }) +/// .expect("with_usb"); +/// } +/// ``` +/// +pub fn with_usb(mut ctx: X, case: for<'a> fn(cls: C, dev: Device<'a, C, X>)) -> AnyResult<()> +where + C: UsbClass, + X: UsbDeviceCtx, +{ + let stio: UsbBusImpl = UsbBusImpl::new(); + let io = Rc::new(RefCell::new(stio)); + let bus = EmulatedUsbBus::new(&io); + + let alloc: usb_device::bus::UsbBusAllocator = UsbBusAllocator::new(bus); + + let mut cls = ctx.create_class(&alloc)?; + + let mut usb_dev = ctx.build_usb_device(&alloc)?; + + let skip_setup = ctx.skip_setup(); + + usb_dev.poll(&mut [&mut cls]); + ctx.post_poll(&mut cls); + + let mut dev = Device::new(io.as_ref(), ctx, usb_dev); + + if !skip_setup { + dev.setup(&mut cls)?; + } + + // run test + case(cls, dev); + Ok(()) +} diff --git a/src/usbdata.rs b/src/usbdata.rs new file mode 100644 index 0000000..be5f18f --- /dev/null +++ b/src/usbdata.rs @@ -0,0 +1,138 @@ +//! A collection of USB-related data-handling stuff. +//! +//! Must not use `usb-device` implementation to +//! be able to test anything. +//! + +/// `CtrRequestType` holds bmRequestType of SETUP +/// packet. +#[must_use] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct CtrRequestType { + direction: u8, + rtype: u8, + recipient: u8, +} + +impl CtrRequestType { + /// Create new `CtrRequestType` with Host-to-device + /// direction (0b00000000). + pub fn to_device() -> Self { + CtrRequestType { + direction: 0, + rtype: 0, + recipient: 0, + } + } + + /// Create new `CtrRequestType` with Device-to-host + /// direction (0b10000000). + pub fn to_host() -> Self { + CtrRequestType { + direction: 1, + rtype: 0, + recipient: 0, + } + } + + /// Copy and set Type to Standard (0bx00xxxxx) + pub fn standard(self) -> Self { + CtrRequestType { rtype: 0, ..self } + } + + /// Copy and set Type to Class (0bx01xxxxx) + pub fn class(self) -> Self { + CtrRequestType { rtype: 1, ..self } + } + + /// Copy and set Type to Vendor (0bx10xxxxx) + pub fn vendor(self) -> Self { + CtrRequestType { rtype: 2, ..self } + } + + /// Copy and set Recipient to Device (0bxxx00000) + pub fn device(self) -> Self { + CtrRequestType { + recipient: 0, + ..self + } + } + + /// Copy and set Recipient to Interface (0bxxx00001) + pub fn interface(self) -> Self { + CtrRequestType { + recipient: 1, + ..self + } + } + + /// Copy and set Recipient to Endpoint (0bxxx00010) + pub fn endpoint(self) -> Self { + CtrRequestType { + recipient: 2, + ..self + } + } + + /// Copy and set Recipient to Other (0bxxx00011) + pub fn other(self) -> Self { + CtrRequestType { + recipient: 3, + ..self + } + } +} + +impl From for u8 { + fn from(val: CtrRequestType) -> Self { + val.direction << 7 | val.rtype << 5 | val.recipient + } +} + +impl From for CtrRequestType { + fn from(value: u8) -> Self { + CtrRequestType { + direction: value >> 7, + rtype: (value >> 5) & 0x3, + recipient: value & 0x1f, + } + } +} + +/// `SetupPacket` structure holds SETUP packet data for +/// all Control transfers. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct SetupPacket { + bm_request_type: CtrRequestType, + b_request: u8, + w_value: u16, + w_index: u16, + w_length: u16, +} + +impl SetupPacket { + pub fn new(reqt: CtrRequestType, req: u8, value: u16, index: u16, length: u16) -> Self { + SetupPacket { + bm_request_type: reqt, + b_request: req, + w_value: value, + w_index: index, + w_length: length, + } + } +} + +impl From for [u8; 8] { + fn from(val: SetupPacket) -> Self { + [ + val.bm_request_type.into(), + val.b_request, + (val.w_value & 0xff) as u8, + (val.w_value >> 8) as u8, + (val.w_index & 0xff) as u8, + (val.w_index >> 8) as u8, + (val.w_length & 0xff) as u8, + (val.w_length >> 8) as u8, + ] + } +} diff --git a/tests/helper_example/mod.rs b/tests/helper_example/mod.rs new file mode 100644 index 0000000..9cfcd74 --- /dev/null +++ b/tests/helper_example/mod.rs @@ -0,0 +1,20 @@ +//! An example how to "extend"/add helper methods to +//! `Device` by implementing a new trait for `Device` +//! in a separate module. +use usbd_class_tester::prelude::*; +use usb_device::class::UsbClass; + +pub trait DeviceExt { + fn custom_get_status(&mut self, cls: &mut T) -> core::result::Result, AnyUsbError>; +} + +impl<'a, T, M> DeviceExt for Device<'a, T, M> +where + T: UsbClass, + M: UsbDeviceCtx, +{ + fn custom_get_status(&mut self, cls: &mut T) -> core::result::Result, AnyUsbError> { + let res = self.device_get_status(cls)?; + Ok(res.to_le_bytes().into()) + } +} diff --git a/tests/test_device.rs b/tests/test_device.rs new file mode 100644 index 0000000..35fc211 --- /dev/null +++ b/tests/test_device.rs @@ -0,0 +1,283 @@ +mod test_device1; +use test_device1::*; + +use usbd_class_tester::prelude::*; + +use usb_device::{ + bus::{UsbBus, UsbBusAllocator}, + class::UsbClass, + device::UsbDeviceState, +}; + +#[derive(Default)] +struct TestCtx { + skip_setup: bool, +} + +impl TestCtx { + fn new() -> Self { + Self::default() + } + fn no_setup() -> Self { + Self { skip_setup: true } + } +} + +impl UsbDeviceCtx for TestCtx { + const ADDRESS: u8 = 55; + + fn create_class<'a>( + &mut self, + alloc: &'a UsbBusAllocator, + ) -> AnyResult { + Ok(TestUsbClass::new(&alloc)) + } + + fn skip_setup(&mut self) -> bool { + self.skip_setup + } +} + +#[test] +fn test_device_get_status_set_self_powered() { + with_usb(TestCtx::new(), |mut cls, mut dev| { + dev.usb_dev().set_self_powered(true); + + let status = dev.device_get_status(&mut cls).expect("vec"); + assert_eq!(status, 1); + + dev.usb_dev().set_self_powered(false); + + let status = dev.device_get_status(&mut cls).expect("vec"); + assert_eq!(status, 0); + }) + .expect("with_usb"); +} + +#[test] +fn test_device_feature_remote_wakeup() { + with_usb(TestCtx::new(), |mut cls, mut dev| { + dev.device_set_feature(&mut cls, 1).expect("failed"); + assert_eq!(dev.usb_dev().remote_wakeup_enabled(), true); + + dev.device_clear_feature(&mut cls, 1).expect("failed"); + assert_eq!(dev.usb_dev().remote_wakeup_enabled(), false); + }) + .expect("with_usb"); +} + +#[test] +fn test_device_address_set() { + with_usb(TestCtx::new(), |mut _cls, mut dev| { + assert_eq!(dev.usb_dev().bus().get_address(), TestCtx::ADDRESS); + }) + .expect("with_usb"); +} + +#[test] +fn test_device_configured() { + with_usb(TestCtx::new(), |mut _cls, mut dev| { + assert_eq!(dev.usb_dev().state(), UsbDeviceState::Configured); + }) + .expect("with_usb"); +} + +#[test] +fn test_device_set_address_and_configuration() { + with_usb(TestCtx::no_setup(), |mut cls, mut dev| { + let mut cnf; + + assert_eq!(dev.usb_dev().state(), UsbDeviceState::Default); + + cnf = dev.device_get_configuration(&mut cls).expect("failed"); + assert_eq!(cnf, 0); + + assert_eq!(dev.usb_dev().state(), UsbDeviceState::Default); + + dev.device_set_address(&mut cls, TestCtx::ADDRESS) + .expect("failed"); + assert_eq!(dev.usb_dev().state(), UsbDeviceState::Addressed); + + dev.device_set_configuration(&mut cls, 1).expect("failed"); + + assert_eq!(dev.usb_dev().state(), UsbDeviceState::Configured); + + cnf = dev.device_get_configuration(&mut cls).expect("failed"); + assert_eq!(cnf, 1); + }) + .expect("with_usb"); +} + +#[test] +fn test_device_get_descriptor_strings() { + with_usb(TestCtx::new(), |mut cls, mut dev| { + let mut vec; + + let desc = |s: &str| { + let unicode_bytes: Vec = s + .encode_utf16() + .map(|x| x.to_le_bytes()) + .flatten() + .collect(); + [&[(unicode_bytes.len() + 2) as u8, 3], &unicode_bytes[..]].concat() + }; + + // get default string descriptors + vec = dev + .device_get_descriptor(&mut cls, 3, 1, 0x409, 255) + .expect("vec"); + assert_eq!(vec, desc("TestManufacturer")); + + vec = dev + .device_get_descriptor(&mut cls, 3, 2, 0x409, 255) + .expect("vec"); + assert_eq!(vec, desc("TestProduct")); + + vec = dev + .device_get_descriptor(&mut cls, 3, 3, 0x409, 255) + .expect("vec"); + assert_eq!(vec, desc("TestSerial")); + }) + .expect("with_usb"); +} + +#[test] +fn test_device_get_strings() { + with_usb(TestCtx::new(), |mut cls, mut dev| { + let mut res; + + // get default string descriptors + res = dev.device_get_string(&mut cls, 1, 0x409).expect("string"); + assert_eq!(res, "TestManufacturer"); + + res = dev.device_get_string(&mut cls, 2, 0x409).expect("string"); + assert_eq!(res, "TestProduct"); + + res = dev.device_get_string(&mut cls, 3, 0x409).expect("string"); + assert_eq!(res, "TestSerial"); + }) + .expect("with_usb"); +} + +#[test] +fn test_interface_get_status() { + with_usb(TestCtx::new(), |mut cls, mut dev| { + let st = dev.interface_get_status(&mut cls, 0).expect("status"); + assert_eq!(st, 0); + }) + .expect("with_usb"); +} + +#[test] +fn test_interface_alt_interface() { + with_usb(TestCtx::new(), |mut cls, mut dev| { + let st = dev + .interface_get_interface(&mut cls) + .expect("get_interface"); + assert_eq!(st, 0); + assert_eq!(cls.alt_setting, 0); + + dev.interface_set_interface(&mut cls, 0, 1) + .expect("set_interface"); + assert_eq!(cls.alt_setting, 1); + + let st = dev + .interface_get_interface(&mut cls) + .expect("get_interface"); + assert_eq!(st, 1); + }) + .expect("with_usb"); +} + +#[test] +fn test_interface_get_set_feature() { + with_usb(TestCtx::new(), |mut cls, mut dev| { + dev.interface_set_feature(&mut cls, 0, 1) + .expect_err("interface feature"); + dev.interface_clear_feature(&mut cls, 0, 1) + .expect_err("interface feature"); + }) + .expect("with_usb"); +} + +#[test] +fn test_device_custom_control_command() { + with_usb(TestCtx::new(), |mut cls, mut dev| { + let mut vec; + + vec = dev + .control_read( + &mut cls, + CtrRequestType::to_host().class().interface(), + 1, + 0, + 0, + 8, + ) + .expect("vec"); + assert_eq!(vec, [1, 2, 0]); + + dev.control_write( + &mut cls, + CtrRequestType::to_device().class().interface(), + 2, + 0, + 0, + 0, + &[], + ) + .expect_err("stall"); + + vec = dev + .control_write( + &mut cls, + CtrRequestType::to_device().class().interface(), + 2, + 0, + 0, + 1, + &[0xaa], + ) + .expect("res"); + assert_eq!(vec, []); + + vec = dev + .control_read( + &mut cls, + CtrRequestType::to_host().class().interface(), + 1, + 0, + 0, + 8, + ) + .expect("vec"); + assert_eq!(vec, [1, 2, 0xaa]); + }) + .expect("with_usb"); +} + +struct FailTestUsbClass {} + +impl UsbClass for FailTestUsbClass {} + +struct FailTestCtx {} + +impl UsbDeviceCtx for FailTestCtx { + const ADDRESS: u8 = 55; + + fn create_class<'a>( + &mut self, + _alloc: &'a UsbBusAllocator, + ) -> AnyResult { + Err(AnyUsbError::UserDefined1) + } +} + +#[test] +#[should_panic(expected = "with_usb: UserDefined1")] +fn test_create_class_fails() { + with_usb(FailTestCtx {}, |mut _cls, mut _dev| { + unreachable!("case should not run"); + }) + .expect("with_usb"); +} diff --git a/tests/test_device1/mod.rs b/tests/test_device1/mod.rs new file mode 100644 index 0000000..af798f5 --- /dev/null +++ b/tests/test_device1/mod.rs @@ -0,0 +1,129 @@ +//! `TestUsbClass` implementation for a test `UsbClass` +use usb_device::{ + bus::{InterfaceNumber, StringIndex, UsbBus, UsbBusAllocator}, + class::UsbClass, + control, LangID, +}; + +pub struct TestUsbClass { + pub iface: InterfaceNumber, + pub interface_string: StringIndex, + pub byte: u8, + pub alt_setting: u8, +} + +impl TestUsbClass { + pub fn new(alloc: &UsbBusAllocator) -> Self { + Self { + iface: alloc.interface(), + interface_string: alloc.string(), + byte: 0, + alt_setting: 0, + } + } +} + +impl UsbClass for TestUsbClass { + fn control_in(&mut self, xfer: usb_device::class::ControlIn) { + let req = xfer.request(); + + if req.request_type != control::RequestType::Class { + return; + } + + if req.recipient != control::Recipient::Interface { + return; + } + + if req.index != u8::from(self.iface) as u16 { + return; + } + + match req.request { + 1 => { + let status: [u8; 3] = [1, 2, self.byte]; + xfer.accept_with(&status).ok(); + } + _ => { + xfer.reject().ok(); + } + } + } + + fn control_out(&mut self, xfer: usb_device::class::ControlOut) { + let req = xfer.request(); + + if req.request_type != control::RequestType::Class { + return; + } + + if req.recipient != control::Recipient::Interface { + return; + } + + if req.index != u8::from(self.iface) as u16 { + return; + } + + let data = xfer.data(); + match req.request { + 2 => { + if data.len() > 0 { + self.byte = data[0]; + xfer.accept().ok(); + } else { + xfer.reject().ok(); + } + } + _ => { + xfer.reject().ok(); + } + } + } + + fn get_alt_setting(&mut self, interface: InterfaceNumber) -> Option { + if interface == self.iface { + Some(self.alt_setting) + } else { + None + } + } + + fn set_alt_setting(&mut self, interface: InterfaceNumber, alternative: u8) -> bool { + if interface == self.iface { + self.alt_setting = alternative; + true + } else { + false + } + } + + fn get_configuration_descriptors( + &self, + writer: &mut usb_device::descriptor::DescriptorWriter, + ) -> usb_device::Result<()> { + writer.interface_alt( + self.iface, + 0, + 0xff, // Vendor Specific + 0x10, + 0x12, + Some(self.interface_string), + )?; + + writer.write(200, &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10])?; + + Ok(()) + } + + fn get_string( + &self, + index: usb_device::bus::StringIndex, + lang_id: usb_device::prelude::LangID, + ) -> Option<&str> { + if index == self.interface_string && (lang_id == LangID::EN_US || u16::from(lang_id) == 0) { + return Some("InterfaceString"); + } + None + } +} diff --git a/tests/test_helper_example.rs b/tests/test_helper_example.rs new file mode 100644 index 0000000..ae8e485 --- /dev/null +++ b/tests/test_helper_example.rs @@ -0,0 +1,42 @@ +//! An example how to "extend"/add helper methods to +//! `Device` by using a module that implements a new +//! trait for `Device`. +mod test_device1; +use test_device1::*; + +mod helper_example; +use helper_example::*; + +use usbd_class_tester::prelude::*; + +use usb_device::bus::UsbBusAllocator; + +struct TestCtx {} + +impl UsbDeviceCtx for TestCtx { + fn create_class<'a>( + &mut self, + alloc: &'a UsbBusAllocator, + ) -> AnyResult { + Ok(TestUsbClass::new(&alloc)) + } +} + +#[test] +fn test_custom_device_get_status_set_self_powered() { + with_usb(TestCtx {}, |mut cls, mut dev| { + dev.usb_dev().set_self_powered(true); + + let status = dev.device_get_status(&mut cls).expect("result"); + assert_eq!(status, 1); + + let vec = dev.custom_get_status(&mut cls).expect("vec"); + assert_eq!(vec, [1, 0]); + + dev.usb_dev().set_self_powered(false); + + let vec = dev.custom_get_status(&mut cls).expect("vec"); + assert_eq!(vec, [0, 0]); + }) + .expect("with_usb"); +}