feat: add display UI with splash screen, dashboard, and RSSI sparkline
Implement full SSD1306 OLED display support: - Splash screen on boot (1.5s) with project name, version, board - Dashboard showing radio state, config, counters, RSSI/SNR - RSSI history sparkline (64-sample ring buffer, 2px-wide bars) - DisplayOn/DisplayOff host commands routed via dedicated channel - Pure render functions against generic DrawTarget (no hardware coupling) - Display on/off control bypasses radio task entirely Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
17624 files changed
+4564928
-38
No files matched your search
+547
@@ -0,0 +1,547 @@
|
||||
//! CDC-ACM class implementation, aka Serial over USB.
|
||||
|
||||
use core::cell::{Cell, RefCell};
|
||||
use core::future::poll_fn;
|
||||
use core::mem::{self, MaybeUninit};
|
||||
use core::sync::atomic::{AtomicBool, Ordering};
|
||||
use core::task::Poll;
|
||||
|
||||
use embassy_sync::blocking_mutex::CriticalSectionMutex;
|
||||
use embassy_sync::waitqueue::WakerRegistration;
|
||||
|
||||
use crate::control::{self, InResponse, OutResponse, Recipient, Request, RequestType};
|
||||
use crate::driver::{Driver, Endpoint, EndpointError, EndpointIn, EndpointOut};
|
||||
use crate::types::InterfaceNumber;
|
||||
use crate::{Builder, Handler};
|
||||
|
||||
/// This should be used as `device_class` when building the `UsbDevice`.
|
||||
pub const USB_CLASS_CDC: u8 = 0x02;
|
||||
|
||||
const USB_CLASS_CDC_DATA: u8 = 0x0a;
|
||||
const CDC_SUBCLASS_ACM: u8 = 0x02;
|
||||
const CDC_PROTOCOL_NONE: u8 = 0x00;
|
||||
|
||||
const CS_INTERFACE: u8 = 0x24;
|
||||
const CDC_TYPE_HEADER: u8 = 0x00;
|
||||
const CDC_TYPE_ACM: u8 = 0x02;
|
||||
const CDC_TYPE_UNION: u8 = 0x06;
|
||||
|
||||
const REQ_SEND_ENCAPSULATED_COMMAND: u8 = 0x00;
|
||||
#[allow(unused)]
|
||||
const REQ_GET_ENCAPSULATED_COMMAND: u8 = 0x01;
|
||||
const REQ_SET_LINE_CODING: u8 = 0x20;
|
||||
const REQ_GET_LINE_CODING: u8 = 0x21;
|
||||
const REQ_SET_CONTROL_LINE_STATE: u8 = 0x22;
|
||||
|
||||
/// Internal state for CDC-ACM
|
||||
pub struct State<'a> {
|
||||
control: MaybeUninit<Control<'a>>,
|
||||
shared: ControlShared,
|
||||
}
|
||||
|
||||
impl<'a> Default for State<'a> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> State<'a> {
|
||||
/// Create a new `State`.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
control: MaybeUninit::uninit(),
|
||||
shared: ControlShared::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Packet level implementation of a CDC-ACM serial port.
|
||||
///
|
||||
/// This class can be used directly and it has the least overhead due to directly reading and
|
||||
/// writing USB packets with no intermediate buffers, but it will not act like a stream-like serial
|
||||
/// port. The following constraints must be followed if you use this class directly:
|
||||
///
|
||||
/// - `read_packet` must be called with a buffer large enough to hold `max_packet_size` bytes.
|
||||
/// - `write_packet` must not be called with a buffer larger than `max_packet_size` bytes.
|
||||
/// - If you write a packet that is exactly `max_packet_size` bytes long, it won't be processed by the
|
||||
/// host operating system until a subsequent shorter packet is sent. A zero-length packet (ZLP)
|
||||
/// can be sent if there is no other data to send. This is because USB bulk transactions must be
|
||||
/// terminated with a short packet, even if the bulk endpoint is used for stream-like data.
|
||||
pub struct CdcAcmClass<'d, D: Driver<'d>> {
|
||||
_comm_ep: D::EndpointIn,
|
||||
_data_if: InterfaceNumber,
|
||||
read_ep: D::EndpointOut,
|
||||
write_ep: D::EndpointIn,
|
||||
control: &'d ControlShared,
|
||||
}
|
||||
|
||||
struct Control<'a> {
|
||||
comm_if: InterfaceNumber,
|
||||
shared: &'a ControlShared,
|
||||
}
|
||||
|
||||
/// Shared data between Control and CdcAcmClass
|
||||
struct ControlShared {
|
||||
line_coding: CriticalSectionMutex<Cell<LineCoding>>,
|
||||
dtr: AtomicBool,
|
||||
rts: AtomicBool,
|
||||
|
||||
waker: RefCell<WakerRegistration>,
|
||||
changed: AtomicBool,
|
||||
}
|
||||
|
||||
impl Default for ControlShared {
|
||||
fn default() -> Self {
|
||||
ControlShared {
|
||||
dtr: AtomicBool::new(false),
|
||||
rts: AtomicBool::new(false),
|
||||
line_coding: CriticalSectionMutex::new(Cell::new(LineCoding {
|
||||
stop_bits: StopBits::One,
|
||||
data_bits: 8,
|
||||
parity_type: ParityType::None,
|
||||
data_rate: 8_000,
|
||||
})),
|
||||
waker: RefCell::new(WakerRegistration::new()),
|
||||
changed: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ControlShared {
|
||||
async fn changed(&self) {
|
||||
poll_fn(|cx| {
|
||||
if self.changed.load(Ordering::Relaxed) {
|
||||
self.changed.store(false, Ordering::Relaxed);
|
||||
Poll::Ready(())
|
||||
} else {
|
||||
self.waker.borrow_mut().register(cx.waker());
|
||||
Poll::Pending
|
||||
}
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Control<'a> {
|
||||
fn shared(&mut self) -> &'a ControlShared {
|
||||
self.shared
|
||||
}
|
||||
}
|
||||
|
||||
impl<'d> Handler for Control<'d> {
|
||||
fn reset(&mut self) {
|
||||
let shared = self.shared();
|
||||
shared.line_coding.lock(|x| x.set(LineCoding::default()));
|
||||
shared.dtr.store(false, Ordering::Relaxed);
|
||||
shared.rts.store(false, Ordering::Relaxed);
|
||||
|
||||
shared.changed.store(true, Ordering::Relaxed);
|
||||
shared.waker.borrow_mut().wake();
|
||||
}
|
||||
|
||||
fn control_out(&mut self, req: control::Request, data: &[u8]) -> Option<OutResponse> {
|
||||
if (req.request_type, req.recipient, req.index)
|
||||
!= (RequestType::Class, Recipient::Interface, self.comm_if.0 as u16)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
match req.request {
|
||||
REQ_SEND_ENCAPSULATED_COMMAND => {
|
||||
// We don't actually support encapsulated commands but pretend we do for standards
|
||||
// compatibility.
|
||||
Some(OutResponse::Accepted)
|
||||
}
|
||||
REQ_SET_LINE_CODING if data.len() >= 7 => {
|
||||
let coding = LineCoding {
|
||||
data_rate: u32::from_le_bytes(data[0..4].try_into().unwrap()),
|
||||
stop_bits: data[4].into(),
|
||||
parity_type: data[5].into(),
|
||||
data_bits: data[6],
|
||||
};
|
||||
let shared = self.shared();
|
||||
shared.line_coding.lock(|x| x.set(coding));
|
||||
debug!("Set line coding to: {:?}", coding);
|
||||
|
||||
shared.changed.store(true, Ordering::Relaxed);
|
||||
shared.waker.borrow_mut().wake();
|
||||
|
||||
Some(OutResponse::Accepted)
|
||||
}
|
||||
REQ_SET_CONTROL_LINE_STATE => {
|
||||
let dtr = (req.value & 0x0001) != 0;
|
||||
let rts = (req.value & 0x0002) != 0;
|
||||
|
||||
let shared = self.shared();
|
||||
shared.dtr.store(dtr, Ordering::Relaxed);
|
||||
shared.rts.store(rts, Ordering::Relaxed);
|
||||
debug!("Set dtr {}, rts {}", dtr, rts);
|
||||
|
||||
shared.changed.store(true, Ordering::Relaxed);
|
||||
shared.waker.borrow_mut().wake();
|
||||
|
||||
Some(OutResponse::Accepted)
|
||||
}
|
||||
_ => Some(OutResponse::Rejected),
|
||||
}
|
||||
}
|
||||
|
||||
fn control_in<'a>(&'a mut self, req: Request, buf: &'a mut [u8]) -> Option<InResponse<'a>> {
|
||||
if (req.request_type, req.recipient, req.index)
|
||||
!= (RequestType::Class, Recipient::Interface, self.comm_if.0 as u16)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
match req.request {
|
||||
// REQ_GET_ENCAPSULATED_COMMAND is not really supported - it will be rejected below.
|
||||
REQ_GET_LINE_CODING if req.length == 7 => {
|
||||
debug!("Sending line coding");
|
||||
let coding = self.shared().line_coding.lock(Cell::get);
|
||||
assert!(buf.len() >= 7);
|
||||
buf[0..4].copy_from_slice(&coding.data_rate.to_le_bytes());
|
||||
buf[4] = coding.stop_bits as u8;
|
||||
buf[5] = coding.parity_type as u8;
|
||||
buf[6] = coding.data_bits;
|
||||
Some(InResponse::Accepted(&buf[0..7]))
|
||||
}
|
||||
_ => Some(InResponse::Rejected),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'d, D: Driver<'d>> CdcAcmClass<'d, D> {
|
||||
/// Creates a new CdcAcmClass with the provided UsbBus and `max_packet_size` in bytes. For
|
||||
/// full-speed devices, `max_packet_size` has to be one of 8, 16, 32 or 64.
|
||||
pub fn new(builder: &mut Builder<'d, D>, state: &'d mut State<'d>, max_packet_size: u16) -> Self {
|
||||
assert!(builder.control_buf_len() >= 7);
|
||||
|
||||
let mut func = builder.function(USB_CLASS_CDC, CDC_SUBCLASS_ACM, CDC_PROTOCOL_NONE);
|
||||
|
||||
// Control interface
|
||||
let mut iface = func.interface();
|
||||
let comm_if = iface.interface_number();
|
||||
let data_if = u8::from(comm_if) + 1;
|
||||
let mut alt = iface.alt_setting(USB_CLASS_CDC, CDC_SUBCLASS_ACM, CDC_PROTOCOL_NONE, None);
|
||||
|
||||
alt.descriptor(
|
||||
CS_INTERFACE,
|
||||
&[
|
||||
CDC_TYPE_HEADER, // bDescriptorSubtype
|
||||
0x10,
|
||||
0x01, // bcdCDC (1.10)
|
||||
],
|
||||
);
|
||||
alt.descriptor(
|
||||
CS_INTERFACE,
|
||||
&[
|
||||
CDC_TYPE_ACM, // bDescriptorSubtype
|
||||
0x02, // bmCapabilities:
|
||||
// D1: Device supports the request combination of
|
||||
// Set_Line_Coding, Set_Control_Line_State, Get_Line_Coding,
|
||||
// and the Notification Serial_State.
|
||||
],
|
||||
);
|
||||
alt.descriptor(
|
||||
CS_INTERFACE,
|
||||
&[
|
||||
CDC_TYPE_UNION, // bDescriptorSubtype
|
||||
comm_if.into(), // bControlInterface
|
||||
data_if, // bSubordinateInterface
|
||||
],
|
||||
);
|
||||
|
||||
let comm_ep = alt.endpoint_interrupt_in(8, 255);
|
||||
|
||||
// Data interface
|
||||
let mut iface = func.interface();
|
||||
let data_if = iface.interface_number();
|
||||
let mut alt = iface.alt_setting(USB_CLASS_CDC_DATA, 0x00, CDC_PROTOCOL_NONE, None);
|
||||
let read_ep = alt.endpoint_bulk_out(max_packet_size);
|
||||
let write_ep = alt.endpoint_bulk_in(max_packet_size);
|
||||
|
||||
drop(func);
|
||||
|
||||
let control = state.control.write(Control {
|
||||
shared: &state.shared,
|
||||
comm_if,
|
||||
});
|
||||
builder.handler(control);
|
||||
|
||||
let control_shared = &state.shared;
|
||||
|
||||
CdcAcmClass {
|
||||
_comm_ep: comm_ep,
|
||||
_data_if: data_if,
|
||||
read_ep,
|
||||
write_ep,
|
||||
control: control_shared,
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the maximum packet size in bytes.
|
||||
pub fn max_packet_size(&self) -> u16 {
|
||||
// The size is the same for both endpoints.
|
||||
self.read_ep.info().max_packet_size
|
||||
}
|
||||
|
||||
/// Gets the current line coding. The line coding contains information that's mainly relevant
|
||||
/// for USB to UART serial port emulators, and can be ignored if not relevant.
|
||||
pub fn line_coding(&self) -> LineCoding {
|
||||
self.control.line_coding.lock(Cell::get)
|
||||
}
|
||||
|
||||
/// Gets the DTR (data terminal ready) state
|
||||
pub fn dtr(&self) -> bool {
|
||||
self.control.dtr.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Gets the RTS (request to send) state
|
||||
pub fn rts(&self) -> bool {
|
||||
self.control.rts.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Writes a single packet into the IN endpoint.
|
||||
pub async fn write_packet(&mut self, data: &[u8]) -> Result<(), EndpointError> {
|
||||
self.write_ep.write(data).await
|
||||
}
|
||||
|
||||
/// Reads a single packet from the OUT endpoint.
|
||||
pub async fn read_packet(&mut self, data: &mut [u8]) -> Result<usize, EndpointError> {
|
||||
self.read_ep.read(data).await
|
||||
}
|
||||
|
||||
/// Waits for the USB host to enable this interface
|
||||
pub async fn wait_connection(&mut self) {
|
||||
self.read_ep.wait_enabled().await;
|
||||
}
|
||||
|
||||
/// Split the class into a sender and receiver.
|
||||
///
|
||||
/// This allows concurrently sending and receiving packets from separate tasks.
|
||||
pub fn split(self) -> (Sender<'d, D>, Receiver<'d, D>) {
|
||||
(
|
||||
Sender {
|
||||
write_ep: self.write_ep,
|
||||
control: self.control,
|
||||
},
|
||||
Receiver {
|
||||
read_ep: self.read_ep,
|
||||
control: self.control,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Split the class into sender, receiver and control
|
||||
///
|
||||
/// Allows concurrently sending and receiving packets whilst monitoring for
|
||||
/// control changes (dtr, rts)
|
||||
pub fn split_with_control(self) -> (Sender<'d, D>, Receiver<'d, D>, ControlChanged<'d>) {
|
||||
(
|
||||
Sender {
|
||||
write_ep: self.write_ep,
|
||||
control: self.control,
|
||||
},
|
||||
Receiver {
|
||||
read_ep: self.read_ep,
|
||||
control: self.control,
|
||||
},
|
||||
ControlChanged { control: self.control },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// CDC ACM Control status change monitor
|
||||
///
|
||||
/// You can obtain a `ControlChanged` with [`CdcAcmClass::split_with_control`]
|
||||
pub struct ControlChanged<'d> {
|
||||
control: &'d ControlShared,
|
||||
}
|
||||
|
||||
impl<'d> ControlChanged<'d> {
|
||||
/// Return a future for when the control settings change
|
||||
pub async fn control_changed(&self) {
|
||||
self.control.changed().await;
|
||||
}
|
||||
}
|
||||
|
||||
/// CDC ACM class packet sender.
|
||||
///
|
||||
/// You can obtain a `Sender` with [`CdcAcmClass::split`]
|
||||
pub struct Sender<'d, D: Driver<'d>> {
|
||||
write_ep: D::EndpointIn,
|
||||
control: &'d ControlShared,
|
||||
}
|
||||
|
||||
impl<'d, D: Driver<'d>> Sender<'d, D> {
|
||||
/// Gets the maximum packet size in bytes.
|
||||
pub fn max_packet_size(&self) -> u16 {
|
||||
// The size is the same for both endpoints.
|
||||
self.write_ep.info().max_packet_size
|
||||
}
|
||||
|
||||
/// Gets the current line coding. The line coding contains information that's mainly relevant
|
||||
/// for USB to UART serial port emulators, and can be ignored if not relevant.
|
||||
pub fn line_coding(&self) -> LineCoding {
|
||||
self.control.line_coding.lock(Cell::get)
|
||||
}
|
||||
|
||||
/// Gets the DTR (data terminal ready) state
|
||||
pub fn dtr(&self) -> bool {
|
||||
self.control.dtr.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Gets the RTS (request to send) state
|
||||
pub fn rts(&self) -> bool {
|
||||
self.control.rts.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Writes a single packet into the IN endpoint.
|
||||
pub async fn write_packet(&mut self, data: &[u8]) -> Result<(), EndpointError> {
|
||||
self.write_ep.write(data).await
|
||||
}
|
||||
|
||||
/// Waits for the USB host to enable this interface
|
||||
pub async fn wait_connection(&mut self) {
|
||||
self.write_ep.wait_enabled().await;
|
||||
}
|
||||
}
|
||||
|
||||
/// CDC ACM class packet receiver.
|
||||
///
|
||||
/// You can obtain a `Receiver` with [`CdcAcmClass::split`]
|
||||
pub struct Receiver<'d, D: Driver<'d>> {
|
||||
read_ep: D::EndpointOut,
|
||||
control: &'d ControlShared,
|
||||
}
|
||||
|
||||
impl<'d, D: Driver<'d>> Receiver<'d, D> {
|
||||
/// Gets the maximum packet size in bytes.
|
||||
pub fn max_packet_size(&self) -> u16 {
|
||||
// The size is the same for both endpoints.
|
||||
self.read_ep.info().max_packet_size
|
||||
}
|
||||
|
||||
/// Gets the current line coding. The line coding contains information that's mainly relevant
|
||||
/// for USB to UART serial port emulators, and can be ignored if not relevant.
|
||||
pub fn line_coding(&self) -> LineCoding {
|
||||
self.control.line_coding.lock(Cell::get)
|
||||
}
|
||||
|
||||
/// Gets the DTR (data terminal ready) state
|
||||
pub fn dtr(&self) -> bool {
|
||||
self.control.dtr.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Gets the RTS (request to send) state
|
||||
pub fn rts(&self) -> bool {
|
||||
self.control.rts.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Reads a single packet from the OUT endpoint.
|
||||
/// Must be called with a buffer large enough to hold max_packet_size bytes.
|
||||
pub async fn read_packet(&mut self, data: &mut [u8]) -> Result<usize, EndpointError> {
|
||||
self.read_ep.read(data).await
|
||||
}
|
||||
|
||||
/// Waits for the USB host to enable this interface
|
||||
pub async fn wait_connection(&mut self) {
|
||||
self.read_ep.wait_enabled().await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of stop bits for LineCoding
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
pub enum StopBits {
|
||||
/// 1 stop bit
|
||||
One = 0,
|
||||
|
||||
/// 1.5 stop bits
|
||||
OnePointFive = 1,
|
||||
|
||||
/// 2 stop bits
|
||||
Two = 2,
|
||||
}
|
||||
|
||||
impl From<u8> for StopBits {
|
||||
fn from(value: u8) -> Self {
|
||||
if value <= 2 {
|
||||
unsafe { mem::transmute(value) }
|
||||
} else {
|
||||
StopBits::One
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parity for LineCoding
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
pub enum ParityType {
|
||||
/// No parity bit.
|
||||
None = 0,
|
||||
/// Parity bit is 1 if the amount of `1` bits in the data byte is odd.
|
||||
Odd = 1,
|
||||
/// Parity bit is 1 if the amount of `1` bits in the data byte is even.
|
||||
Even = 2,
|
||||
/// Parity bit is always 1
|
||||
Mark = 3,
|
||||
/// Parity bit is always 0
|
||||
Space = 4,
|
||||
}
|
||||
|
||||
impl From<u8> for ParityType {
|
||||
fn from(value: u8) -> Self {
|
||||
if value <= 4 {
|
||||
unsafe { mem::transmute(value) }
|
||||
} else {
|
||||
ParityType::None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Line coding parameters
|
||||
///
|
||||
/// This is provided by the host for specifying the standard UART parameters such as baud rate. Can
|
||||
/// be ignored if you don't plan to interface with a physical UART.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
pub struct LineCoding {
|
||||
stop_bits: StopBits,
|
||||
data_bits: u8,
|
||||
parity_type: ParityType,
|
||||
data_rate: u32,
|
||||
}
|
||||
|
||||
impl LineCoding {
|
||||
/// Gets the number of stop bits for UART communication.
|
||||
pub fn stop_bits(&self) -> StopBits {
|
||||
self.stop_bits
|
||||
}
|
||||
|
||||
/// Gets the number of data bits for UART communication.
|
||||
pub const fn data_bits(&self) -> u8 {
|
||||
self.data_bits
|
||||
}
|
||||
|
||||
/// Gets the parity type for UART communication.
|
||||
pub const fn parity_type(&self) -> ParityType {
|
||||
self.parity_type
|
||||
}
|
||||
|
||||
/// Gets the data rate in bits per second for UART communication.
|
||||
pub const fn data_rate(&self) -> u32 {
|
||||
self.data_rate
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LineCoding {
|
||||
fn default() -> Self {
|
||||
LineCoding {
|
||||
stop_bits: StopBits::One,
|
||||
data_bits: 8,
|
||||
parity_type: ParityType::None,
|
||||
data_rate: 8_000,
|
||||
}
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
//! [`embassy-net`](https://crates.io/crates/embassy-net) driver for the CDC-NCM class.
|
||||
|
||||
use embassy_futures::select::{select, Either};
|
||||
use embassy_net_driver_channel as ch;
|
||||
use embassy_net_driver_channel::driver::LinkState;
|
||||
use embassy_usb_driver::Driver;
|
||||
|
||||
use super::{CdcNcmClass, Receiver, Sender};
|
||||
|
||||
/// Internal state for the embassy-net integration.
|
||||
pub struct State<const MTU: usize, const N_RX: usize, const N_TX: usize> {
|
||||
ch_state: ch::State<MTU, N_RX, N_TX>,
|
||||
}
|
||||
|
||||
impl<const MTU: usize, const N_RX: usize, const N_TX: usize> State<MTU, N_RX, N_TX> {
|
||||
/// Create a new `State`.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
ch_state: ch::State::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Background runner for the CDC-NCM class.
|
||||
///
|
||||
/// You must call `.run()` in a background task for the class to operate.
|
||||
pub struct Runner<'d, D: Driver<'d>, const MTU: usize> {
|
||||
tx_usb: Sender<'d, D>,
|
||||
rx_usb: Receiver<'d, D>,
|
||||
ch: ch::Runner<'d, MTU>,
|
||||
}
|
||||
|
||||
impl<'d, D: Driver<'d>, const MTU: usize> Runner<'d, D, MTU> {
|
||||
/// Run the CDC-NCM class.
|
||||
///
|
||||
/// You must call this in a background task for the class to operate.
|
||||
pub async fn run(mut self) -> ! {
|
||||
let (state_chan, mut rx_chan, mut tx_chan) = self.ch.split();
|
||||
let rx_fut = async move {
|
||||
loop {
|
||||
trace!("WAITING for connection");
|
||||
state_chan.set_link_state(LinkState::Down);
|
||||
|
||||
self.rx_usb.wait_connection().await.unwrap();
|
||||
|
||||
trace!("Connected");
|
||||
state_chan.set_link_state(LinkState::Up);
|
||||
|
||||
loop {
|
||||
let p = rx_chan.rx_buf().await;
|
||||
match self.rx_usb.read_packet(p).await {
|
||||
Ok(n) => rx_chan.rx_done(n),
|
||||
Err(e) => {
|
||||
warn!("error reading packet: {:?}", e);
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
let tx_fut = async move {
|
||||
loop {
|
||||
let p = tx_chan.tx_buf().await;
|
||||
if let Err(e) = self.tx_usb.write_packet(p).await {
|
||||
warn!("Failed to TX packet: {:?}", e);
|
||||
}
|
||||
tx_chan.tx_done();
|
||||
}
|
||||
};
|
||||
match select(rx_fut, tx_fut).await {
|
||||
Either::First(x) => x,
|
||||
Either::Second(x) => x,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// would be cool to use a TAIT here, but it gives a "may not live long enough". rustc bug?
|
||||
//pub type Device<'d, const MTU: usize> = impl embassy_net_driver_channel::driver::Driver + 'd;
|
||||
/// Type alias for the embassy-net driver for CDC-NCM.
|
||||
pub type Device<'d, const MTU: usize> = embassy_net_driver_channel::Device<'d, MTU>;
|
||||
|
||||
impl<'d, D: Driver<'d>> CdcNcmClass<'d, D> {
|
||||
/// Obtain a driver for using the CDC-NCM class with [`embassy-net`](https://crates.io/crates/embassy-net).
|
||||
pub fn into_embassy_net_device<const MTU: usize, const N_RX: usize, const N_TX: usize>(
|
||||
self,
|
||||
state: &'d mut State<MTU, N_RX, N_TX>,
|
||||
ethernet_address: [u8; 6],
|
||||
) -> (Runner<'d, D, MTU>, Device<'d, MTU>) {
|
||||
let (tx_usb, rx_usb) = self.split();
|
||||
let (runner, device) = ch::new(
|
||||
&mut state.ch_state,
|
||||
ch::driver::HardwareAddress::Ethernet(ethernet_address),
|
||||
);
|
||||
|
||||
(
|
||||
Runner {
|
||||
tx_usb,
|
||||
rx_usb,
|
||||
ch: runner,
|
||||
},
|
||||
device,
|
||||
)
|
||||
}
|
||||
}
|
||||
+529
@@ -0,0 +1,529 @@
|
||||
//! CDC-NCM class implementation, aka Ethernet over USB.
|
||||
//!
|
||||
//! # Compatibility
|
||||
//!
|
||||
//! Windows: NOT supported in Windows 10 (though there's apparently a driver you can install?). Supported out of the box in Windows 11.
|
||||
//!
|
||||
//! Linux: Well-supported since forever.
|
||||
//!
|
||||
//! Android: Support for CDC-NCM is spotty and varies across manufacturers.
|
||||
//!
|
||||
//! - On Pixel 4a, it refused to work on Android 11, worked on Android 12.
|
||||
//! - if the host's MAC address has the "locally-administered" bit set (bit 1 of first byte),
|
||||
//! it doesn't work! The "Ethernet tethering" option in settings doesn't get enabled.
|
||||
//! This is due to regex spaghetti: <https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-mainline-12.0.0_r84/core/res/res/values/config.xml#417>
|
||||
//! and this nonsense in the linux kernel: <https://github.com/torvalds/linux/blob/c00c5e1d157bec0ef0b0b59aa5482eb8dc7e8e49/drivers/net/usb/usbnet.c#L1751-L1757>
|
||||
|
||||
use core::intrinsics::copy_nonoverlapping;
|
||||
use core::mem::{size_of, MaybeUninit};
|
||||
use core::ptr::addr_of;
|
||||
|
||||
use crate::control::{self, InResponse, OutResponse, Recipient, Request, RequestType};
|
||||
use crate::driver::{Driver, Endpoint, EndpointError, EndpointIn, EndpointOut};
|
||||
use crate::types::{InterfaceNumber, StringIndex};
|
||||
use crate::{Builder, Handler};
|
||||
|
||||
pub mod embassy_net;
|
||||
|
||||
/// This should be used as `device_class` when building the `UsbDevice`.
|
||||
pub const USB_CLASS_CDC: u8 = 0x02;
|
||||
|
||||
const USB_CLASS_CDC_DATA: u8 = 0x0a;
|
||||
const CDC_SUBCLASS_NCM: u8 = 0x0d;
|
||||
|
||||
const CDC_PROTOCOL_NONE: u8 = 0x00;
|
||||
const CDC_PROTOCOL_NTB: u8 = 0x01;
|
||||
|
||||
const CS_INTERFACE: u8 = 0x24;
|
||||
const CDC_TYPE_HEADER: u8 = 0x00;
|
||||
const CDC_TYPE_UNION: u8 = 0x06;
|
||||
const CDC_TYPE_ETHERNET: u8 = 0x0F;
|
||||
const CDC_TYPE_NCM: u8 = 0x1A;
|
||||
|
||||
const REQ_SEND_ENCAPSULATED_COMMAND: u8 = 0x00;
|
||||
//const REQ_GET_ENCAPSULATED_COMMAND: u8 = 0x01;
|
||||
//const REQ_SET_ETHERNET_MULTICAST_FILTERS: u8 = 0x40;
|
||||
//const REQ_SET_ETHERNET_POWER_MANAGEMENT_PATTERN_FILTER: u8 = 0x41;
|
||||
//const REQ_GET_ETHERNET_POWER_MANAGEMENT_PATTERN_FILTER: u8 = 0x42;
|
||||
//const REQ_SET_ETHERNET_PACKET_FILTER: u8 = 0x43;
|
||||
//const REQ_GET_ETHERNET_STATISTIC: u8 = 0x44;
|
||||
const REQ_GET_NTB_PARAMETERS: u8 = 0x80;
|
||||
//const REQ_GET_NET_ADDRESS: u8 = 0x81;
|
||||
//const REQ_SET_NET_ADDRESS: u8 = 0x82;
|
||||
//const REQ_GET_NTB_FORMAT: u8 = 0x83;
|
||||
//const REQ_SET_NTB_FORMAT: u8 = 0x84;
|
||||
//const REQ_GET_NTB_INPUT_SIZE: u8 = 0x85;
|
||||
const REQ_SET_NTB_INPUT_SIZE: u8 = 0x86;
|
||||
//const REQ_GET_MAX_DATAGRAM_SIZE: u8 = 0x87;
|
||||
//const REQ_SET_MAX_DATAGRAM_SIZE: u8 = 0x88;
|
||||
//const REQ_GET_CRC_MODE: u8 = 0x89;
|
||||
//const REQ_SET_CRC_MODE: u8 = 0x8A;
|
||||
|
||||
//const NOTIF_MAX_PACKET_SIZE: u16 = 8;
|
||||
//const NOTIF_POLL_INTERVAL: u8 = 20;
|
||||
|
||||
const NTB_MAX_SIZE: usize = 2048;
|
||||
const SIG_NTH: u32 = 0x484d_434e;
|
||||
const SIG_NDP_NO_FCS: u32 = 0x304d_434e;
|
||||
const SIG_NDP_WITH_FCS: u32 = 0x314d_434e;
|
||||
|
||||
const ALTERNATE_SETTING_DISABLED: u8 = 0x00;
|
||||
const ALTERNATE_SETTING_ENABLED: u8 = 0x01;
|
||||
|
||||
/// Simple NTB header (NTH+NDP all in one) for sending packets
|
||||
#[repr(packed)]
|
||||
#[allow(unused)]
|
||||
struct NtbOutHeader {
|
||||
// NTH
|
||||
nth_sig: u32,
|
||||
nth_len: u16,
|
||||
nth_seq: u16,
|
||||
nth_total_len: u16,
|
||||
nth_first_index: u16,
|
||||
|
||||
// NDP
|
||||
ndp_sig: u32,
|
||||
ndp_len: u16,
|
||||
ndp_next_index: u16,
|
||||
ndp_datagram_index: u16,
|
||||
ndp_datagram_len: u16,
|
||||
ndp_term1: u16,
|
||||
ndp_term2: u16,
|
||||
}
|
||||
|
||||
#[repr(packed)]
|
||||
#[allow(unused)]
|
||||
struct NtbParameters {
|
||||
length: u16,
|
||||
formats_supported: u16,
|
||||
in_params: NtbParametersDir,
|
||||
out_params: NtbParametersDir,
|
||||
}
|
||||
|
||||
#[repr(packed)]
|
||||
#[allow(unused)]
|
||||
struct NtbParametersDir {
|
||||
max_size: u32,
|
||||
divisor: u16,
|
||||
payload_remainder: u16,
|
||||
out_alignment: u16,
|
||||
max_datagram_count: u16,
|
||||
}
|
||||
|
||||
fn byteify<T>(buf: &mut [u8], data: T) -> &[u8] {
|
||||
let len = size_of::<T>();
|
||||
unsafe { copy_nonoverlapping(addr_of!(data).cast(), buf.as_mut_ptr(), len) }
|
||||
&buf[..len]
|
||||
}
|
||||
|
||||
/// Internal state for the CDC-NCM class.
|
||||
pub struct State<'a> {
|
||||
control: MaybeUninit<Control<'a>>,
|
||||
shared: ControlShared,
|
||||
}
|
||||
|
||||
impl<'a> Default for State<'a> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> State<'a> {
|
||||
/// Create a new `State`.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
control: MaybeUninit::uninit(),
|
||||
shared: ControlShared::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared data between Control and `CdcAcmClass`
|
||||
#[derive(Default)]
|
||||
struct ControlShared {
|
||||
mac_addr: [u8; 6],
|
||||
}
|
||||
|
||||
struct Control<'a> {
|
||||
mac_addr_string: StringIndex,
|
||||
shared: &'a ControlShared,
|
||||
mac_addr_str: [u8; 12],
|
||||
comm_if: InterfaceNumber,
|
||||
data_if: InterfaceNumber,
|
||||
}
|
||||
|
||||
impl<'d> Handler for Control<'d> {
|
||||
fn set_alternate_setting(&mut self, iface: InterfaceNumber, alternate_setting: u8) {
|
||||
if iface != self.data_if {
|
||||
return;
|
||||
}
|
||||
|
||||
match alternate_setting {
|
||||
ALTERNATE_SETTING_ENABLED => info!("ncm: interface enabled"),
|
||||
ALTERNATE_SETTING_DISABLED => info!("ncm: interface disabled"),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn control_out(&mut self, req: control::Request, _data: &[u8]) -> Option<OutResponse> {
|
||||
if (req.request_type, req.recipient, req.index)
|
||||
!= (RequestType::Class, Recipient::Interface, self.comm_if.0 as u16)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
match req.request {
|
||||
REQ_SEND_ENCAPSULATED_COMMAND => {
|
||||
// We don't actually support encapsulated commands but pretend we do for standards
|
||||
// compatibility.
|
||||
Some(OutResponse::Accepted)
|
||||
}
|
||||
REQ_SET_NTB_INPUT_SIZE => {
|
||||
// TODO
|
||||
Some(OutResponse::Accepted)
|
||||
}
|
||||
_ => Some(OutResponse::Rejected),
|
||||
}
|
||||
}
|
||||
|
||||
fn control_in<'a>(&'a mut self, req: Request, buf: &'a mut [u8]) -> Option<InResponse<'a>> {
|
||||
if (req.request_type, req.recipient, req.index)
|
||||
!= (RequestType::Class, Recipient::Interface, self.comm_if.0 as u16)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
match req.request {
|
||||
REQ_GET_NTB_PARAMETERS => {
|
||||
let res = NtbParameters {
|
||||
length: size_of::<NtbParameters>() as _,
|
||||
formats_supported: 1, // only 16bit,
|
||||
in_params: NtbParametersDir {
|
||||
max_size: NTB_MAX_SIZE as _,
|
||||
divisor: 4,
|
||||
payload_remainder: 0,
|
||||
out_alignment: 4,
|
||||
max_datagram_count: 0, // not used
|
||||
},
|
||||
out_params: NtbParametersDir {
|
||||
max_size: NTB_MAX_SIZE as _,
|
||||
divisor: 4,
|
||||
payload_remainder: 0,
|
||||
out_alignment: 4,
|
||||
max_datagram_count: 1, // We only decode 1 packet per NTB
|
||||
},
|
||||
};
|
||||
Some(InResponse::Accepted(byteify(buf, res)))
|
||||
}
|
||||
_ => Some(InResponse::Rejected),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_string(&mut self, index: StringIndex, _lang_id: u16) -> Option<&str> {
|
||||
if index == self.mac_addr_string {
|
||||
let mac_addr = self.shared.mac_addr;
|
||||
let s = &mut self.mac_addr_str;
|
||||
for i in 0..12 {
|
||||
let n = (mac_addr[i / 2] >> ((1 - i % 2) * 4)) & 0xF;
|
||||
s[i] = match n {
|
||||
0x0..=0x9 => b'0' + n,
|
||||
0xA..=0xF => b'A' + n - 0xA,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
Some(unsafe { core::str::from_utf8_unchecked(s) })
|
||||
} else {
|
||||
warn!("unknown string index requested");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// CDC-NCM class
|
||||
pub struct CdcNcmClass<'d, D: Driver<'d>> {
|
||||
_comm_if: InterfaceNumber,
|
||||
comm_ep: D::EndpointIn,
|
||||
|
||||
data_if: InterfaceNumber,
|
||||
read_ep: D::EndpointOut,
|
||||
write_ep: D::EndpointIn,
|
||||
|
||||
_control: &'d ControlShared,
|
||||
|
||||
max_packet_size: usize,
|
||||
}
|
||||
|
||||
impl<'d, D: Driver<'d>> CdcNcmClass<'d, D> {
|
||||
/// Create a new CDC NCM class.
|
||||
pub fn new(
|
||||
builder: &mut Builder<'d, D>,
|
||||
state: &'d mut State<'d>,
|
||||
mac_address: [u8; 6],
|
||||
max_packet_size: u16,
|
||||
) -> Self {
|
||||
state.shared.mac_addr = mac_address;
|
||||
|
||||
let mut func = builder.function(USB_CLASS_CDC, CDC_SUBCLASS_NCM, CDC_PROTOCOL_NONE);
|
||||
|
||||
// Control interface
|
||||
let mut iface = func.interface();
|
||||
let mac_addr_string = iface.string();
|
||||
let comm_if = iface.interface_number();
|
||||
let mut alt = iface.alt_setting(USB_CLASS_CDC, CDC_SUBCLASS_NCM, CDC_PROTOCOL_NONE, None);
|
||||
|
||||
alt.descriptor(
|
||||
CS_INTERFACE,
|
||||
&[
|
||||
CDC_TYPE_HEADER, // bDescriptorSubtype
|
||||
0x10,
|
||||
0x01, // bcdCDC (1.10)
|
||||
],
|
||||
);
|
||||
alt.descriptor(
|
||||
CS_INTERFACE,
|
||||
&[
|
||||
CDC_TYPE_UNION, // bDescriptorSubtype
|
||||
comm_if.into(), // bControlInterface
|
||||
u8::from(comm_if) + 1, // bSubordinateInterface
|
||||
],
|
||||
);
|
||||
alt.descriptor(
|
||||
CS_INTERFACE,
|
||||
&[
|
||||
CDC_TYPE_ETHERNET, // bDescriptorSubtype
|
||||
mac_addr_string.into(), // iMACAddress
|
||||
0, // bmEthernetStatistics
|
||||
0, // |
|
||||
0, // |
|
||||
0, // |
|
||||
0xea, // wMaxSegmentSize = 1514
|
||||
0x05, // |
|
||||
0, // wNumberMCFilters
|
||||
0, // |
|
||||
0, // bNumberPowerFilters
|
||||
],
|
||||
);
|
||||
alt.descriptor(
|
||||
CS_INTERFACE,
|
||||
&[
|
||||
CDC_TYPE_NCM, // bDescriptorSubtype
|
||||
0x00, // bcdNCMVersion
|
||||
0x01, // |
|
||||
0, // bmNetworkCapabilities
|
||||
],
|
||||
);
|
||||
|
||||
let comm_ep = alt.endpoint_interrupt_in(8, 255);
|
||||
|
||||
// Data interface
|
||||
let mut iface = func.interface();
|
||||
let data_if = iface.interface_number();
|
||||
let _alt = iface.alt_setting(USB_CLASS_CDC_DATA, 0x00, CDC_PROTOCOL_NTB, None);
|
||||
let mut alt = iface.alt_setting(USB_CLASS_CDC_DATA, 0x00, CDC_PROTOCOL_NTB, None);
|
||||
let read_ep = alt.endpoint_bulk_out(max_packet_size);
|
||||
let write_ep = alt.endpoint_bulk_in(max_packet_size);
|
||||
|
||||
drop(func);
|
||||
|
||||
let control = state.control.write(Control {
|
||||
mac_addr_string,
|
||||
shared: &state.shared,
|
||||
mac_addr_str: [0; 12],
|
||||
comm_if,
|
||||
data_if,
|
||||
});
|
||||
builder.handler(control);
|
||||
|
||||
CdcNcmClass {
|
||||
_comm_if: comm_if,
|
||||
comm_ep,
|
||||
data_if,
|
||||
read_ep,
|
||||
write_ep,
|
||||
_control: &state.shared,
|
||||
max_packet_size: max_packet_size as usize,
|
||||
}
|
||||
}
|
||||
|
||||
/// Split the class into a sender and receiver.
|
||||
///
|
||||
/// This allows concurrently sending and receiving packets from separate tasks.
|
||||
pub fn split(self) -> (Sender<'d, D>, Receiver<'d, D>) {
|
||||
(
|
||||
Sender {
|
||||
write_ep: self.write_ep,
|
||||
seq: 0,
|
||||
max_packet_size: self.max_packet_size,
|
||||
},
|
||||
Receiver {
|
||||
data_if: self.data_if,
|
||||
comm_ep: self.comm_ep,
|
||||
read_ep: self.read_ep,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// CDC NCM class packet sender.
|
||||
///
|
||||
/// You can obtain a `Sender` with [`CdcNcmClass::split`]
|
||||
pub struct Sender<'d, D: Driver<'d>> {
|
||||
write_ep: D::EndpointIn,
|
||||
seq: u16,
|
||||
max_packet_size: usize,
|
||||
}
|
||||
|
||||
impl<'d, D: Driver<'d>> Sender<'d, D> {
|
||||
/// Write a packet.
|
||||
///
|
||||
/// This waits until the packet is successfully stored in the CDC-NCM endpoint buffers.
|
||||
pub async fn write_packet(&mut self, data: &[u8]) -> Result<(), EndpointError> {
|
||||
const OUT_HEADER_LEN: usize = 28;
|
||||
const ABS_MAX_PACKET_SIZE: usize = 512;
|
||||
|
||||
let seq = self.seq;
|
||||
self.seq = self.seq.wrapping_add(1);
|
||||
|
||||
let header = NtbOutHeader {
|
||||
nth_sig: SIG_NTH,
|
||||
nth_len: 0x0c,
|
||||
nth_seq: seq,
|
||||
nth_total_len: (data.len() + OUT_HEADER_LEN) as u16,
|
||||
nth_first_index: 0x0c,
|
||||
|
||||
ndp_sig: SIG_NDP_NO_FCS,
|
||||
ndp_len: 0x10,
|
||||
ndp_next_index: 0x00,
|
||||
ndp_datagram_index: OUT_HEADER_LEN as u16,
|
||||
ndp_datagram_len: data.len() as u16,
|
||||
ndp_term1: 0x00,
|
||||
ndp_term2: 0x00,
|
||||
};
|
||||
|
||||
// Build first packet on a buffer, send next packets straight from `data`.
|
||||
let mut buf = [0; ABS_MAX_PACKET_SIZE];
|
||||
let n = byteify(&mut buf, header);
|
||||
assert_eq!(n.len(), OUT_HEADER_LEN);
|
||||
|
||||
if OUT_HEADER_LEN + data.len() < self.max_packet_size {
|
||||
// First packet is not full, just send it.
|
||||
// No need to send ZLP because it's short for sure.
|
||||
buf[OUT_HEADER_LEN..][..data.len()].copy_from_slice(data);
|
||||
self.write_ep.write(&buf[..OUT_HEADER_LEN + data.len()]).await?;
|
||||
} else {
|
||||
let (d1, d2) = data.split_at(self.max_packet_size - OUT_HEADER_LEN);
|
||||
|
||||
buf[OUT_HEADER_LEN..self.max_packet_size].copy_from_slice(d1);
|
||||
self.write_ep.write(&buf[..self.max_packet_size]).await?;
|
||||
|
||||
for chunk in d2.chunks(self.max_packet_size) {
|
||||
self.write_ep.write(chunk).await?;
|
||||
}
|
||||
|
||||
// Send ZLP if needed.
|
||||
if d2.len() % self.max_packet_size == 0 {
|
||||
self.write_ep.write(&[]).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// CDC NCM class packet receiver.
|
||||
///
|
||||
/// You can obtain a `Receiver` with [`CdcNcmClass::split`]
|
||||
pub struct Receiver<'d, D: Driver<'d>> {
|
||||
data_if: InterfaceNumber,
|
||||
comm_ep: D::EndpointIn,
|
||||
read_ep: D::EndpointOut,
|
||||
}
|
||||
|
||||
impl<'d, D: Driver<'d>> Receiver<'d, D> {
|
||||
/// Write a network packet.
|
||||
///
|
||||
/// This waits until a packet is successfully received from the endpoint buffers.
|
||||
pub async fn read_packet(&mut self, buf: &mut [u8]) -> Result<usize, EndpointError> {
|
||||
// Retry loop
|
||||
loop {
|
||||
// read NTB
|
||||
let mut ntb = [0u8; NTB_MAX_SIZE];
|
||||
let mut pos = 0;
|
||||
loop {
|
||||
let n = self.read_ep.read(&mut ntb[pos..]).await?;
|
||||
pos += n;
|
||||
if n < self.read_ep.info().max_packet_size as usize || pos == NTB_MAX_SIZE {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let ntb = &ntb[..pos];
|
||||
|
||||
// Process NTB header (NTH)
|
||||
let Some(nth) = ntb.get(..12) else {
|
||||
warn!("Received too short NTB");
|
||||
continue;
|
||||
};
|
||||
let sig = u32::from_le_bytes(nth[0..4].try_into().unwrap());
|
||||
if sig != SIG_NTH {
|
||||
warn!("Received bad NTH sig.");
|
||||
continue;
|
||||
}
|
||||
let ndp_idx = u16::from_le_bytes(nth[10..12].try_into().unwrap()) as usize;
|
||||
|
||||
// Process NTB Datagram Pointer (NDP)
|
||||
let Some(ndp) = ntb.get(ndp_idx..ndp_idx + 12) else {
|
||||
warn!("NTH has an NDP pointer out of range.");
|
||||
continue;
|
||||
};
|
||||
let sig = u32::from_le_bytes(ndp[0..4].try_into().unwrap());
|
||||
if sig != SIG_NDP_NO_FCS && sig != SIG_NDP_WITH_FCS {
|
||||
warn!("Received bad NDP sig.");
|
||||
continue;
|
||||
}
|
||||
let datagram_index = u16::from_le_bytes(ndp[8..10].try_into().unwrap()) as usize;
|
||||
let datagram_len = u16::from_le_bytes(ndp[10..12].try_into().unwrap()) as usize;
|
||||
|
||||
if datagram_index == 0 || datagram_len == 0 {
|
||||
// empty, ignore. This is allowed by the spec, so don't warn.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Process actual datagram, finally.
|
||||
let Some(datagram) = ntb.get(datagram_index..datagram_index + datagram_len) else {
|
||||
warn!("NDP has a datagram pointer out of range.");
|
||||
continue;
|
||||
};
|
||||
buf[..datagram_len].copy_from_slice(datagram);
|
||||
|
||||
return Ok(datagram_len);
|
||||
}
|
||||
}
|
||||
|
||||
/// Waits for the USB host to enable this interface
|
||||
pub async fn wait_connection(&mut self) -> Result<(), EndpointError> {
|
||||
loop {
|
||||
self.read_ep.wait_enabled().await;
|
||||
self.comm_ep.wait_enabled().await;
|
||||
|
||||
let buf = [
|
||||
0xA1, //bmRequestType
|
||||
0x00, //bNotificationType = NETWORK_CONNECTION
|
||||
0x01, // wValue = connected
|
||||
0x00,
|
||||
self.data_if.into(), // wIndex = interface
|
||||
0x00,
|
||||
0x00, // wLength
|
||||
0x00,
|
||||
];
|
||||
match self.comm_ep.write(&buf).await {
|
||||
Ok(()) => break, // Done!
|
||||
Err(EndpointError::Disabled) => {} // Got disabled again, wait again.
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Vendored
+552
@@ -0,0 +1,552 @@
|
||||
//! USB HID (Human Interface Device) class implementation.
|
||||
|
||||
use core::mem::MaybeUninit;
|
||||
use core::ops::Range;
|
||||
use core::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
#[cfg(feature = "usbd-hid")]
|
||||
use ssmarshal::serialize;
|
||||
#[cfg(feature = "usbd-hid")]
|
||||
use usbd_hid::descriptor::AsInputReport;
|
||||
|
||||
use crate::control::{InResponse, OutResponse, Recipient, Request, RequestType};
|
||||
use crate::driver::{Driver, Endpoint, EndpointError, EndpointIn, EndpointOut};
|
||||
use crate::types::InterfaceNumber;
|
||||
use crate::{Builder, Handler};
|
||||
|
||||
const USB_CLASS_HID: u8 = 0x03;
|
||||
const USB_SUBCLASS_NONE: u8 = 0x00;
|
||||
const USB_PROTOCOL_NONE: u8 = 0x00;
|
||||
|
||||
// HID
|
||||
const HID_DESC_DESCTYPE_HID: u8 = 0x21;
|
||||
const HID_DESC_DESCTYPE_HID_REPORT: u8 = 0x22;
|
||||
const HID_DESC_SPEC_1_10: [u8; 2] = [0x10, 0x01];
|
||||
const HID_DESC_COUNTRY_UNSPEC: u8 = 0x00;
|
||||
|
||||
const HID_REQ_SET_IDLE: u8 = 0x0a;
|
||||
const HID_REQ_GET_IDLE: u8 = 0x02;
|
||||
const HID_REQ_GET_REPORT: u8 = 0x01;
|
||||
const HID_REQ_SET_REPORT: u8 = 0x09;
|
||||
const HID_REQ_GET_PROTOCOL: u8 = 0x03;
|
||||
const HID_REQ_SET_PROTOCOL: u8 = 0x0b;
|
||||
|
||||
/// Configuration for the HID class.
|
||||
pub struct Config<'d> {
|
||||
/// HID report descriptor.
|
||||
pub report_descriptor: &'d [u8],
|
||||
|
||||
/// Handler for control requests.
|
||||
pub request_handler: Option<&'d mut dyn RequestHandler>,
|
||||
|
||||
/// Configures how frequently the host should poll for reading/writing HID reports.
|
||||
///
|
||||
/// A lower value means better throughput & latency, at the expense
|
||||
/// of CPU on the device & bandwidth on the bus. A value of 10 is reasonable for
|
||||
/// high performance uses, and a value of 255 is good for best-effort usecases.
|
||||
pub poll_ms: u8,
|
||||
|
||||
/// Max packet size for both the IN and OUT endpoints.
|
||||
pub max_packet_size: u16,
|
||||
}
|
||||
|
||||
/// Report ID
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
pub enum ReportId {
|
||||
/// IN report
|
||||
In(u8),
|
||||
/// OUT report
|
||||
Out(u8),
|
||||
/// Feature report
|
||||
Feature(u8),
|
||||
}
|
||||
|
||||
impl ReportId {
|
||||
const fn try_from(value: u16) -> Result<Self, ()> {
|
||||
match value >> 8 {
|
||||
1 => Ok(ReportId::In(value as u8)),
|
||||
2 => Ok(ReportId::Out(value as u8)),
|
||||
3 => Ok(ReportId::Feature(value as u8)),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal state for USB HID.
|
||||
pub struct State<'d> {
|
||||
control: MaybeUninit<Control<'d>>,
|
||||
out_report_offset: AtomicUsize,
|
||||
}
|
||||
|
||||
impl<'d> Default for State<'d> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'d> State<'d> {
|
||||
/// Create a new `State`.
|
||||
pub const fn new() -> Self {
|
||||
State {
|
||||
control: MaybeUninit::uninit(),
|
||||
out_report_offset: AtomicUsize::new(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// USB HID reader/writer.
|
||||
pub struct HidReaderWriter<'d, D: Driver<'d>, const READ_N: usize, const WRITE_N: usize> {
|
||||
reader: HidReader<'d, D, READ_N>,
|
||||
writer: HidWriter<'d, D, WRITE_N>,
|
||||
}
|
||||
|
||||
fn build<'d, D: Driver<'d>>(
|
||||
builder: &mut Builder<'d, D>,
|
||||
state: &'d mut State<'d>,
|
||||
config: Config<'d>,
|
||||
with_out_endpoint: bool,
|
||||
) -> (Option<D::EndpointOut>, D::EndpointIn, &'d AtomicUsize) {
|
||||
let len = config.report_descriptor.len();
|
||||
|
||||
let mut func = builder.function(USB_CLASS_HID, USB_SUBCLASS_NONE, USB_PROTOCOL_NONE);
|
||||
let mut iface = func.interface();
|
||||
let if_num = iface.interface_number();
|
||||
let mut alt = iface.alt_setting(USB_CLASS_HID, USB_SUBCLASS_NONE, USB_PROTOCOL_NONE, None);
|
||||
|
||||
// HID descriptor
|
||||
alt.descriptor(
|
||||
HID_DESC_DESCTYPE_HID,
|
||||
&[
|
||||
// HID Class spec version
|
||||
HID_DESC_SPEC_1_10[0],
|
||||
HID_DESC_SPEC_1_10[1],
|
||||
// Country code not supported
|
||||
HID_DESC_COUNTRY_UNSPEC,
|
||||
// Number of following descriptors
|
||||
1,
|
||||
// We have a HID report descriptor the host should read
|
||||
HID_DESC_DESCTYPE_HID_REPORT,
|
||||
// HID report descriptor size,
|
||||
(len & 0xFF) as u8,
|
||||
(len >> 8 & 0xFF) as u8,
|
||||
],
|
||||
);
|
||||
|
||||
let ep_in = alt.endpoint_interrupt_in(config.max_packet_size, config.poll_ms);
|
||||
let ep_out = if with_out_endpoint {
|
||||
Some(alt.endpoint_interrupt_out(config.max_packet_size, config.poll_ms))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
drop(func);
|
||||
|
||||
let control = state.control.write(Control::new(
|
||||
if_num,
|
||||
config.report_descriptor,
|
||||
config.request_handler,
|
||||
&state.out_report_offset,
|
||||
));
|
||||
builder.handler(control);
|
||||
|
||||
(ep_out, ep_in, &state.out_report_offset)
|
||||
}
|
||||
|
||||
impl<'d, D: Driver<'d>, const READ_N: usize, const WRITE_N: usize> HidReaderWriter<'d, D, READ_N, WRITE_N> {
|
||||
/// Creates a new `HidReaderWriter`.
|
||||
///
|
||||
/// This will allocate one IN and one OUT endpoints. If you only need writing (sending)
|
||||
/// HID reports, consider using [`HidWriter::new`] instead, which allocates an IN endpoint only.
|
||||
///
|
||||
pub fn new(builder: &mut Builder<'d, D>, state: &'d mut State<'d>, config: Config<'d>) -> Self {
|
||||
let (ep_out, ep_in, offset) = build(builder, state, config, true);
|
||||
|
||||
Self {
|
||||
reader: HidReader {
|
||||
ep_out: ep_out.unwrap(),
|
||||
offset,
|
||||
},
|
||||
writer: HidWriter { ep_in },
|
||||
}
|
||||
}
|
||||
|
||||
/// Splits into separate readers/writers for input and output reports.
|
||||
pub fn split(self) -> (HidReader<'d, D, READ_N>, HidWriter<'d, D, WRITE_N>) {
|
||||
(self.reader, self.writer)
|
||||
}
|
||||
|
||||
/// Waits for both IN and OUT endpoints to be enabled.
|
||||
pub async fn ready(&mut self) {
|
||||
self.reader.ready().await;
|
||||
self.writer.ready().await;
|
||||
}
|
||||
|
||||
/// Writes an input report by serializing the given report structure.
|
||||
#[cfg(feature = "usbd-hid")]
|
||||
pub async fn write_serialize<IR: AsInputReport>(&mut self, r: &IR) -> Result<(), EndpointError> {
|
||||
self.writer.write_serialize(r).await
|
||||
}
|
||||
|
||||
/// Writes `report` to its interrupt endpoint.
|
||||
pub async fn write(&mut self, report: &[u8]) -> Result<(), EndpointError> {
|
||||
self.writer.write(report).await
|
||||
}
|
||||
|
||||
/// Reads an output report from the Interrupt Out pipe.
|
||||
///
|
||||
/// See [`HidReader::read`].
|
||||
pub async fn read(&mut self, buf: &mut [u8]) -> Result<usize, ReadError> {
|
||||
self.reader.read(buf).await
|
||||
}
|
||||
}
|
||||
|
||||
/// USB HID writer.
|
||||
///
|
||||
/// You can obtain a `HidWriter` using [`HidReaderWriter::split`].
|
||||
pub struct HidWriter<'d, D: Driver<'d>, const N: usize> {
|
||||
ep_in: D::EndpointIn,
|
||||
}
|
||||
|
||||
/// USB HID reader.
|
||||
///
|
||||
/// You can obtain a `HidReader` using [`HidReaderWriter::split`].
|
||||
pub struct HidReader<'d, D: Driver<'d>, const N: usize> {
|
||||
ep_out: D::EndpointOut,
|
||||
offset: &'d AtomicUsize,
|
||||
}
|
||||
|
||||
/// Error when reading a HID report.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||
pub enum ReadError {
|
||||
/// The given buffer was too small to read the received report.
|
||||
BufferOverflow,
|
||||
/// The endpoint is disabled.
|
||||
Disabled,
|
||||
/// The report was only partially read. See [`HidReader::read`] for details.
|
||||
Sync(Range<usize>),
|
||||
}
|
||||
|
||||
impl From<EndpointError> for ReadError {
|
||||
fn from(val: EndpointError) -> Self {
|
||||
use EndpointError::{BufferOverflow, Disabled};
|
||||
match val {
|
||||
BufferOverflow => ReadError::BufferOverflow,
|
||||
Disabled => ReadError::Disabled,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'d, D: Driver<'d>, const N: usize> HidWriter<'d, D, N> {
|
||||
/// Creates a new HidWriter.
|
||||
///
|
||||
/// This will allocate one IN endpoint only, so the host won't be able to send
|
||||
/// reports to us. If you need that, consider using [`HidReaderWriter::new`] instead.
|
||||
///
|
||||
/// poll_ms configures how frequently the host should poll for reading/writing
|
||||
/// HID reports. A lower value means better throughput & latency, at the expense
|
||||
/// of CPU on the device & bandwidth on the bus. A value of 10 is reasonable for
|
||||
/// high performance uses, and a value of 255 is good for best-effort usecases.
|
||||
pub fn new(builder: &mut Builder<'d, D>, state: &'d mut State<'d>, config: Config<'d>) -> Self {
|
||||
let (ep_out, ep_in, _offset) = build(builder, state, config, false);
|
||||
|
||||
assert!(ep_out.is_none());
|
||||
|
||||
Self { ep_in }
|
||||
}
|
||||
|
||||
/// Waits for the interrupt in endpoint to be enabled.
|
||||
pub async fn ready(&mut self) {
|
||||
self.ep_in.wait_enabled().await;
|
||||
}
|
||||
|
||||
/// Writes an input report by serializing the given report structure.
|
||||
#[cfg(feature = "usbd-hid")]
|
||||
pub async fn write_serialize<IR: AsInputReport>(&mut self, r: &IR) -> Result<(), EndpointError> {
|
||||
let mut buf: [u8; N] = [0; N];
|
||||
let Ok(size) = serialize(&mut buf, r) else {
|
||||
return Err(EndpointError::BufferOverflow);
|
||||
};
|
||||
self.write(&buf[0..size]).await
|
||||
}
|
||||
|
||||
/// Writes `report` to its interrupt endpoint.
|
||||
pub async fn write(&mut self, report: &[u8]) -> Result<(), EndpointError> {
|
||||
assert!(report.len() <= N);
|
||||
|
||||
let max_packet_size = usize::from(self.ep_in.info().max_packet_size);
|
||||
let zlp_needed = report.len() < N && (report.len() % max_packet_size == 0);
|
||||
for chunk in report.chunks(max_packet_size) {
|
||||
self.ep_in.write(chunk).await?;
|
||||
}
|
||||
|
||||
if zlp_needed {
|
||||
self.ep_in.write(&[]).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'d, D: Driver<'d>, const N: usize> HidReader<'d, D, N> {
|
||||
/// Waits for the interrupt out endpoint to be enabled.
|
||||
pub async fn ready(&mut self) {
|
||||
self.ep_out.wait_enabled().await;
|
||||
}
|
||||
|
||||
/// Delivers output reports from the Interrupt Out pipe to `handler`.
|
||||
///
|
||||
/// If `use_report_ids` is true, the first byte of the report will be used as
|
||||
/// the `ReportId` value. Otherwise the `ReportId` value will be 0.
|
||||
pub async fn run<T: RequestHandler>(mut self, use_report_ids: bool, handler: &mut T) -> ! {
|
||||
let offset = self.offset.load(Ordering::Acquire);
|
||||
assert!(offset == 0);
|
||||
let mut buf = [0; N];
|
||||
loop {
|
||||
match self.read(&mut buf).await {
|
||||
Ok(len) => {
|
||||
let id = if use_report_ids { buf[0] } else { 0 };
|
||||
handler.set_report(ReportId::Out(id), &buf[..len]);
|
||||
}
|
||||
Err(ReadError::BufferOverflow) => warn!(
|
||||
"Host sent output report larger than the configured maximum output report length ({})",
|
||||
N
|
||||
),
|
||||
Err(ReadError::Disabled) => self.ep_out.wait_enabled().await,
|
||||
Err(ReadError::Sync(_)) => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads an output report from the Interrupt Out pipe.
|
||||
///
|
||||
/// **Note:** Any reports sent from the host over the control pipe will be
|
||||
/// passed to [`RequestHandler::set_report()`] for handling. The application
|
||||
/// is responsible for ensuring output reports from both pipes are handled
|
||||
/// correctly.
|
||||
///
|
||||
/// **Note:** If `N` > the maximum packet size of the endpoint (i.e. output
|
||||
/// reports may be split across multiple packets) and this method's future
|
||||
/// is dropped after some packets have been read, the next call to `read()`
|
||||
/// will return a [`ReadError::Sync`]. The range in the sync error
|
||||
/// indicates the portion `buf` that was filled by the current call to
|
||||
/// `read()`. If the dropped future used the same `buf`, then `buf` will
|
||||
/// contain the full report.
|
||||
pub async fn read(&mut self, buf: &mut [u8]) -> Result<usize, ReadError> {
|
||||
assert!(N != 0);
|
||||
assert!(buf.len() >= N);
|
||||
|
||||
// Read packets from the endpoint
|
||||
let max_packet_size = usize::from(self.ep_out.info().max_packet_size);
|
||||
let starting_offset = self.offset.load(Ordering::Acquire);
|
||||
let mut total = starting_offset;
|
||||
loop {
|
||||
for chunk in buf[starting_offset..N].chunks_mut(max_packet_size) {
|
||||
match self.ep_out.read(chunk).await {
|
||||
Ok(size) => {
|
||||
total += size;
|
||||
if size < max_packet_size || total == N {
|
||||
self.offset.store(0, Ordering::Release);
|
||||
break;
|
||||
}
|
||||
self.offset.store(total, Ordering::Release);
|
||||
}
|
||||
Err(err) => {
|
||||
self.offset.store(0, Ordering::Release);
|
||||
return Err(err.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Some hosts may send ZLPs even when not required by the HID spec, so we'll loop as long as total == 0.
|
||||
if total > 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if starting_offset > 0 {
|
||||
Err(ReadError::Sync(starting_offset..total))
|
||||
} else {
|
||||
Ok(total)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for HID-related control requests.
|
||||
pub trait RequestHandler {
|
||||
/// Reads the value of report `id` into `buf` returning the size.
|
||||
///
|
||||
/// Returns `None` if `id` is invalid or no data is available.
|
||||
fn get_report(&mut self, id: ReportId, buf: &mut [u8]) -> Option<usize> {
|
||||
let _ = (id, buf);
|
||||
None
|
||||
}
|
||||
|
||||
/// Sets the value of report `id` to `data`.
|
||||
fn set_report(&mut self, id: ReportId, data: &[u8]) -> OutResponse {
|
||||
let _ = (id, data);
|
||||
OutResponse::Rejected
|
||||
}
|
||||
|
||||
/// Get the idle rate for `id`.
|
||||
///
|
||||
/// If `id` is `None`, get the idle rate for all reports. Returning `None`
|
||||
/// will reject the control request. Any duration at or above 1.024 seconds
|
||||
/// or below 4ms will be returned as an indefinite idle rate.
|
||||
fn get_idle_ms(&mut self, id: Option<ReportId>) -> Option<u32> {
|
||||
let _ = id;
|
||||
None
|
||||
}
|
||||
|
||||
/// Set the idle rate for `id` to `dur`.
|
||||
///
|
||||
/// If `id` is `None`, set the idle rate of all input reports to `dur`. If
|
||||
/// an indefinite duration is requested, `dur` will be set to `u32::MAX`.
|
||||
fn set_idle_ms(&mut self, id: Option<ReportId>, duration_ms: u32) {
|
||||
let _ = (id, duration_ms);
|
||||
}
|
||||
}
|
||||
|
||||
struct Control<'d> {
|
||||
if_num: InterfaceNumber,
|
||||
report_descriptor: &'d [u8],
|
||||
request_handler: Option<&'d mut dyn RequestHandler>,
|
||||
out_report_offset: &'d AtomicUsize,
|
||||
hid_descriptor: [u8; 9],
|
||||
}
|
||||
|
||||
impl<'d> Control<'d> {
|
||||
fn new(
|
||||
if_num: InterfaceNumber,
|
||||
report_descriptor: &'d [u8],
|
||||
request_handler: Option<&'d mut dyn RequestHandler>,
|
||||
out_report_offset: &'d AtomicUsize,
|
||||
) -> Self {
|
||||
Control {
|
||||
if_num,
|
||||
report_descriptor,
|
||||
request_handler,
|
||||
out_report_offset,
|
||||
hid_descriptor: [
|
||||
// Length of buf inclusive of size prefix
|
||||
9,
|
||||
// Descriptor type
|
||||
HID_DESC_DESCTYPE_HID,
|
||||
// HID Class spec version
|
||||
HID_DESC_SPEC_1_10[0],
|
||||
HID_DESC_SPEC_1_10[1],
|
||||
// Country code not supported
|
||||
HID_DESC_COUNTRY_UNSPEC,
|
||||
// Number of following descriptors
|
||||
1,
|
||||
// We have a HID report descriptor the host should read
|
||||
HID_DESC_DESCTYPE_HID_REPORT,
|
||||
// HID report descriptor size,
|
||||
(report_descriptor.len() & 0xFF) as u8,
|
||||
(report_descriptor.len() >> 8 & 0xFF) as u8,
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'d> Handler for Control<'d> {
|
||||
fn reset(&mut self) {
|
||||
self.out_report_offset.store(0, Ordering::Release);
|
||||
}
|
||||
|
||||
fn control_out(&mut self, req: Request, data: &[u8]) -> Option<OutResponse> {
|
||||
if (req.request_type, req.recipient, req.index)
|
||||
!= (RequestType::Class, Recipient::Interface, self.if_num.0 as u16)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
// This uses a defmt-specific formatter that causes use of the `log`
|
||||
// feature to fail to build, so leave it defmt-specific for now.
|
||||
#[cfg(feature = "defmt")]
|
||||
trace!("HID control_out {:?} {=[u8]:x}", req, data);
|
||||
match req.request {
|
||||
HID_REQ_SET_IDLE => {
|
||||
if let Some(handler) = self.request_handler.as_mut() {
|
||||
let id = req.value as u8;
|
||||
let id = (id != 0).then_some(ReportId::In(id));
|
||||
let dur = u32::from(req.value >> 8);
|
||||
let dur = if dur == 0 { u32::MAX } else { 4 * dur };
|
||||
handler.set_idle_ms(id, dur);
|
||||
}
|
||||
Some(OutResponse::Accepted)
|
||||
}
|
||||
HID_REQ_SET_REPORT => match (ReportId::try_from(req.value), self.request_handler.as_mut()) {
|
||||
(Ok(id), Some(handler)) => Some(handler.set_report(id, data)),
|
||||
_ => Some(OutResponse::Rejected),
|
||||
},
|
||||
HID_REQ_SET_PROTOCOL => {
|
||||
if req.value == 1 {
|
||||
Some(OutResponse::Accepted)
|
||||
} else {
|
||||
warn!("HID Boot Protocol is unsupported.");
|
||||
Some(OutResponse::Rejected) // UNSUPPORTED: Boot Protocol
|
||||
}
|
||||
}
|
||||
_ => Some(OutResponse::Rejected),
|
||||
}
|
||||
}
|
||||
|
||||
fn control_in<'a>(&'a mut self, req: Request, buf: &'a mut [u8]) -> Option<InResponse<'a>> {
|
||||
if req.index != self.if_num.0 as u16 {
|
||||
return None;
|
||||
}
|
||||
|
||||
match (req.request_type, req.recipient) {
|
||||
(RequestType::Standard, Recipient::Interface) => match req.request {
|
||||
Request::GET_DESCRIPTOR => match (req.value >> 8) as u8 {
|
||||
HID_DESC_DESCTYPE_HID_REPORT => Some(InResponse::Accepted(self.report_descriptor)),
|
||||
HID_DESC_DESCTYPE_HID => Some(InResponse::Accepted(&self.hid_descriptor)),
|
||||
_ => Some(InResponse::Rejected),
|
||||
},
|
||||
|
||||
_ => Some(InResponse::Rejected),
|
||||
},
|
||||
(RequestType::Class, Recipient::Interface) => {
|
||||
trace!("HID control_in {:?}", req);
|
||||
match req.request {
|
||||
HID_REQ_GET_REPORT => {
|
||||
let size = match ReportId::try_from(req.value) {
|
||||
Ok(id) => self.request_handler.as_mut().and_then(|x| x.get_report(id, buf)),
|
||||
Err(_) => None,
|
||||
};
|
||||
|
||||
if let Some(size) = size {
|
||||
Some(InResponse::Accepted(&buf[0..size]))
|
||||
} else {
|
||||
Some(InResponse::Rejected)
|
||||
}
|
||||
}
|
||||
HID_REQ_GET_IDLE => {
|
||||
if let Some(handler) = self.request_handler.as_mut() {
|
||||
let id = req.value as u8;
|
||||
let id = (id != 0).then_some(ReportId::In(id));
|
||||
if let Some(dur) = handler.get_idle_ms(id) {
|
||||
let dur = u8::try_from(dur / 4).unwrap_or(0);
|
||||
buf[0] = dur;
|
||||
Some(InResponse::Accepted(&buf[0..1]))
|
||||
} else {
|
||||
Some(InResponse::Rejected)
|
||||
}
|
||||
} else {
|
||||
Some(InResponse::Rejected)
|
||||
}
|
||||
}
|
||||
HID_REQ_GET_PROTOCOL => {
|
||||
// UNSUPPORTED: Boot Protocol
|
||||
buf[0] = 1;
|
||||
Some(InResponse::Accepted(&buf[0..1]))
|
||||
}
|
||||
_ => Some(InResponse::Rejected),
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+227
@@ -0,0 +1,227 @@
|
||||
//! MIDI class implementation.
|
||||
|
||||
use crate::driver::{Driver, Endpoint, EndpointError, EndpointIn, EndpointOut};
|
||||
use crate::Builder;
|
||||
|
||||
/// This should be used as `device_class` when building the `UsbDevice`.
|
||||
pub const USB_AUDIO_CLASS: u8 = 0x01;
|
||||
|
||||
const USB_AUDIOCONTROL_SUBCLASS: u8 = 0x01;
|
||||
const USB_MIDISTREAMING_SUBCLASS: u8 = 0x03;
|
||||
const MIDI_IN_JACK_SUBTYPE: u8 = 0x02;
|
||||
const MIDI_OUT_JACK_SUBTYPE: u8 = 0x03;
|
||||
const EMBEDDED: u8 = 0x01;
|
||||
const EXTERNAL: u8 = 0x02;
|
||||
const CS_INTERFACE: u8 = 0x24;
|
||||
const CS_ENDPOINT: u8 = 0x25;
|
||||
const HEADER_SUBTYPE: u8 = 0x01;
|
||||
const MS_HEADER_SUBTYPE: u8 = 0x01;
|
||||
const MS_GENERAL: u8 = 0x01;
|
||||
const PROTOCOL_NONE: u8 = 0x00;
|
||||
const MIDI_IN_SIZE: u8 = 0x06;
|
||||
const MIDI_OUT_SIZE: u8 = 0x09;
|
||||
|
||||
/// Packet level implementation of a USB MIDI device.
|
||||
///
|
||||
/// This class can be used directly and it has the least overhead due to directly reading and
|
||||
/// writing USB packets with no intermediate buffers, but it will not act like a stream-like port.
|
||||
/// The following constraints must be followed if you use this class directly:
|
||||
///
|
||||
/// - `read_packet` must be called with a buffer large enough to hold `max_packet_size` bytes.
|
||||
/// - `write_packet` must not be called with a buffer larger than `max_packet_size` bytes.
|
||||
/// - If you write a packet that is exactly `max_packet_size` bytes long, it won't be processed by the
|
||||
/// host operating system until a subsequent shorter packet is sent. A zero-length packet (ZLP)
|
||||
/// can be sent if there is no other data to send. This is because USB bulk transactions must be
|
||||
/// terminated with a short packet, even if the bulk endpoint is used for stream-like data.
|
||||
pub struct MidiClass<'d, D: Driver<'d>> {
|
||||
read_ep: D::EndpointOut,
|
||||
write_ep: D::EndpointIn,
|
||||
}
|
||||
|
||||
impl<'d, D: Driver<'d>> MidiClass<'d, D> {
|
||||
/// Creates a new `MidiClass` with the provided UsbBus, number of input and output jacks and `max_packet_size` in bytes.
|
||||
/// For full-speed devices, `max_packet_size` has to be one of 8, 16, 32 or 64.
|
||||
pub fn new(builder: &mut Builder<'d, D>, n_in_jacks: u8, n_out_jacks: u8, max_packet_size: u16) -> Self {
|
||||
let mut func = builder.function(USB_AUDIO_CLASS, USB_AUDIOCONTROL_SUBCLASS, PROTOCOL_NONE);
|
||||
|
||||
// Audio control interface
|
||||
let mut iface = func.interface();
|
||||
let audio_if = iface.interface_number();
|
||||
let midi_if = u8::from(audio_if) + 1;
|
||||
let mut alt = iface.alt_setting(USB_AUDIO_CLASS, USB_AUDIOCONTROL_SUBCLASS, PROTOCOL_NONE, None);
|
||||
alt.descriptor(CS_INTERFACE, &[HEADER_SUBTYPE, 0x00, 0x01, 0x09, 0x00, 0x01, midi_if]);
|
||||
|
||||
// MIDIStreaming interface
|
||||
let mut iface = func.interface();
|
||||
let _midi_if = iface.interface_number();
|
||||
let mut alt = iface.alt_setting(USB_AUDIO_CLASS, USB_MIDISTREAMING_SUBCLASS, PROTOCOL_NONE, None);
|
||||
|
||||
let midi_streaming_total_length = 7
|
||||
+ (n_in_jacks + n_out_jacks) as usize * (MIDI_IN_SIZE + MIDI_OUT_SIZE) as usize
|
||||
+ 7
|
||||
+ (4 + n_out_jacks as usize)
|
||||
+ 7
|
||||
+ (4 + n_in_jacks as usize);
|
||||
|
||||
alt.descriptor(
|
||||
CS_INTERFACE,
|
||||
&[
|
||||
MS_HEADER_SUBTYPE,
|
||||
0x00,
|
||||
0x01,
|
||||
(midi_streaming_total_length & 0xFF) as u8,
|
||||
((midi_streaming_total_length >> 8) & 0xFF) as u8,
|
||||
],
|
||||
);
|
||||
|
||||
// Calculates the index'th external midi in jack id
|
||||
let in_jack_id_ext = |index| 2 * index + 1;
|
||||
// Calculates the index'th embedded midi out jack id
|
||||
let out_jack_id_emb = |index| 2 * index + 2;
|
||||
// Calculates the index'th external midi out jack id
|
||||
let out_jack_id_ext = |index| 2 * n_in_jacks + 2 * index + 1;
|
||||
// Calculates the index'th embedded midi in jack id
|
||||
let in_jack_id_emb = |index| 2 * n_in_jacks + 2 * index + 2;
|
||||
|
||||
for i in 0..n_in_jacks {
|
||||
alt.descriptor(CS_INTERFACE, &[MIDI_IN_JACK_SUBTYPE, EXTERNAL, in_jack_id_ext(i), 0x00]);
|
||||
}
|
||||
|
||||
for i in 0..n_out_jacks {
|
||||
alt.descriptor(CS_INTERFACE, &[MIDI_IN_JACK_SUBTYPE, EMBEDDED, in_jack_id_emb(i), 0x00]);
|
||||
}
|
||||
|
||||
for i in 0..n_out_jacks {
|
||||
alt.descriptor(
|
||||
CS_INTERFACE,
|
||||
&[
|
||||
MIDI_OUT_JACK_SUBTYPE,
|
||||
EXTERNAL,
|
||||
out_jack_id_ext(i),
|
||||
0x01,
|
||||
in_jack_id_emb(i),
|
||||
0x01,
|
||||
0x00,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
for i in 0..n_in_jacks {
|
||||
alt.descriptor(
|
||||
CS_INTERFACE,
|
||||
&[
|
||||
MIDI_OUT_JACK_SUBTYPE,
|
||||
EMBEDDED,
|
||||
out_jack_id_emb(i),
|
||||
0x01,
|
||||
in_jack_id_ext(i),
|
||||
0x01,
|
||||
0x00,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
let mut endpoint_data = [
|
||||
MS_GENERAL, 0, // Number of jacks
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Jack mappings
|
||||
];
|
||||
endpoint_data[1] = n_out_jacks;
|
||||
for i in 0..n_out_jacks {
|
||||
endpoint_data[2 + i as usize] = in_jack_id_emb(i);
|
||||
}
|
||||
let read_ep = alt.endpoint_bulk_out(max_packet_size);
|
||||
alt.descriptor(CS_ENDPOINT, &endpoint_data[0..2 + n_out_jacks as usize]);
|
||||
|
||||
endpoint_data[1] = n_in_jacks;
|
||||
for i in 0..n_in_jacks {
|
||||
endpoint_data[2 + i as usize] = out_jack_id_emb(i);
|
||||
}
|
||||
let write_ep = alt.endpoint_bulk_in(max_packet_size);
|
||||
alt.descriptor(CS_ENDPOINT, &endpoint_data[0..2 + n_in_jacks as usize]);
|
||||
|
||||
MidiClass { read_ep, write_ep }
|
||||
}
|
||||
|
||||
/// Gets the maximum packet size in bytes.
|
||||
pub fn max_packet_size(&self) -> u16 {
|
||||
// The size is the same for both endpoints.
|
||||
self.read_ep.info().max_packet_size
|
||||
}
|
||||
|
||||
/// Writes a single packet into the IN endpoint.
|
||||
pub async fn write_packet(&mut self, data: &[u8]) -> Result<(), EndpointError> {
|
||||
self.write_ep.write(data).await
|
||||
}
|
||||
|
||||
/// Reads a single packet from the OUT endpoint.
|
||||
pub async fn read_packet(&mut self, data: &mut [u8]) -> Result<usize, EndpointError> {
|
||||
self.read_ep.read(data).await
|
||||
}
|
||||
|
||||
/// Waits for the USB host to enable this interface
|
||||
pub async fn wait_connection(&mut self) {
|
||||
self.read_ep.wait_enabled().await;
|
||||
}
|
||||
|
||||
/// Split the class into a sender and receiver.
|
||||
///
|
||||
/// This allows concurrently sending and receiving packets from separate tasks.
|
||||
pub fn split(self) -> (Sender<'d, D>, Receiver<'d, D>) {
|
||||
(
|
||||
Sender {
|
||||
write_ep: self.write_ep,
|
||||
},
|
||||
Receiver { read_ep: self.read_ep },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Midi class packet sender.
|
||||
///
|
||||
/// You can obtain a `Sender` with [`MidiClass::split`]
|
||||
pub struct Sender<'d, D: Driver<'d>> {
|
||||
write_ep: D::EndpointIn,
|
||||
}
|
||||
|
||||
impl<'d, D: Driver<'d>> Sender<'d, D> {
|
||||
/// Gets the maximum packet size in bytes.
|
||||
pub fn max_packet_size(&self) -> u16 {
|
||||
// The size is the same for both endpoints.
|
||||
self.write_ep.info().max_packet_size
|
||||
}
|
||||
|
||||
/// Writes a single packet.
|
||||
pub async fn write_packet(&mut self, data: &[u8]) -> Result<(), EndpointError> {
|
||||
self.write_ep.write(data).await
|
||||
}
|
||||
|
||||
/// Waits for the USB host to enable this interface
|
||||
pub async fn wait_connection(&mut self) {
|
||||
self.write_ep.wait_enabled().await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Midi class packet receiver.
|
||||
///
|
||||
/// You can obtain a `Receiver` with [`MidiClass::split`]
|
||||
pub struct Receiver<'d, D: Driver<'d>> {
|
||||
read_ep: D::EndpointOut,
|
||||
}
|
||||
|
||||
impl<'d, D: Driver<'d>> Receiver<'d, D> {
|
||||
/// Gets the maximum packet size in bytes.
|
||||
pub fn max_packet_size(&self) -> u16 {
|
||||
// The size is the same for both endpoints.
|
||||
self.read_ep.info().max_packet_size
|
||||
}
|
||||
|
||||
/// Reads a single packet.
|
||||
pub async fn read_packet(&mut self, data: &mut [u8]) -> Result<usize, EndpointError> {
|
||||
self.read_ep.read(data).await
|
||||
}
|
||||
|
||||
/// Waits for the USB host to enable this interface
|
||||
pub async fn wait_connection(&mut self) {
|
||||
self.read_ep.wait_enabled().await;
|
||||
}
|
||||
}
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
//! Implementations of well-known USB classes.
|
||||
pub mod cdc_acm;
|
||||
pub mod cdc_ncm;
|
||||
pub mod hid;
|
||||
pub mod midi;
|
||||
pub mod web_usb;
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
//! WebUSB API capability implementation.
|
||||
//!
|
||||
//! See https://wicg.github.io/webusb
|
||||
|
||||
use core::mem::MaybeUninit;
|
||||
|
||||
use crate::control::{InResponse, Recipient, Request, RequestType};
|
||||
use crate::descriptor::capability_type;
|
||||
use crate::driver::Driver;
|
||||
use crate::{Builder, Handler};
|
||||
|
||||
const USB_CLASS_VENDOR: u8 = 0xff;
|
||||
const USB_SUBCLASS_NONE: u8 = 0x00;
|
||||
const USB_PROTOCOL_NONE: u8 = 0x00;
|
||||
|
||||
const WEB_USB_REQUEST_GET_URL: u16 = 0x02;
|
||||
const WEB_USB_DESCRIPTOR_TYPE_URL: u8 = 0x03;
|
||||
|
||||
/// URL descriptor for WebUSB landing page.
|
||||
///
|
||||
/// An ecoded URL descriptor to point to a website that is suggested to the user when the device is connected.
|
||||
pub struct Url<'d>(&'d str, u8);
|
||||
|
||||
impl<'d> Url<'d> {
|
||||
/// Create a new WebUSB URL descriptor.
|
||||
pub fn new(url: &'d str) -> Self {
|
||||
let (prefix, stripped_url) = if let Some(stripped) = url.strip_prefix("https://") {
|
||||
(1, stripped)
|
||||
} else if let Some(stripped) = url.strip_prefix("http://") {
|
||||
(0, stripped)
|
||||
} else {
|
||||
(255, url)
|
||||
};
|
||||
assert!(
|
||||
stripped_url.len() <= 252,
|
||||
"URL too long. ({} bytes). Maximum length is 252 bytes.",
|
||||
stripped_url.len()
|
||||
);
|
||||
Self(stripped_url, prefix)
|
||||
}
|
||||
|
||||
fn as_bytes(&self) -> &[u8] {
|
||||
self.0.as_bytes()
|
||||
}
|
||||
|
||||
fn scheme(&self) -> u8 {
|
||||
self.1
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for WebUSB.
|
||||
pub struct Config<'d> {
|
||||
/// Maximum packet size in bytes for the data endpoints.
|
||||
///
|
||||
/// Valid values depend on the speed at which the bus is enumerated.
|
||||
/// - low speed: 8
|
||||
/// - full speed: 8, 16, 32, or 64
|
||||
/// - high speed: 64
|
||||
pub max_packet_size: u16,
|
||||
/// URL to navigate to when the device is connected.
|
||||
///
|
||||
/// If defined, shows a landing page which the device manufacturer would like the user to visit in order to control their device.
|
||||
pub landing_url: Option<Url<'d>>,
|
||||
/// Vendor code for the WebUSB request.
|
||||
///
|
||||
/// This value defines the request id (bRequest) the device expects the host to use when issuing control transfers these requests. This can be an arbitrary u8 and is not to be confused with the USB Vendor ID.
|
||||
pub vendor_code: u8,
|
||||
}
|
||||
|
||||
struct Control<'d> {
|
||||
ep_buf: [u8; 128],
|
||||
vendor_code: u8,
|
||||
landing_url: Option<&'d Url<'d>>,
|
||||
}
|
||||
|
||||
impl<'d> Control<'d> {
|
||||
fn new(config: &'d Config<'d>) -> Self {
|
||||
Control {
|
||||
ep_buf: [0u8; 128],
|
||||
vendor_code: config.vendor_code,
|
||||
landing_url: config.landing_url.as_ref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'d> Handler for Control<'d> {
|
||||
fn control_in(&mut self, req: Request, _data: &mut [u8]) -> Option<InResponse> {
|
||||
let landing_value = if self.landing_url.is_some() { 1 } else { 0 };
|
||||
if req.request_type == RequestType::Vendor
|
||||
&& req.recipient == Recipient::Device
|
||||
&& req.request == self.vendor_code
|
||||
&& req.value == landing_value
|
||||
&& req.index == WEB_USB_REQUEST_GET_URL
|
||||
{
|
||||
if let Some(url) = self.landing_url {
|
||||
let url_bytes = url.as_bytes();
|
||||
let len = url_bytes.len();
|
||||
|
||||
self.ep_buf[0] = len as u8 + 3;
|
||||
self.ep_buf[1] = WEB_USB_DESCRIPTOR_TYPE_URL;
|
||||
self.ep_buf[2] = url.scheme();
|
||||
self.ep_buf[3..3 + len].copy_from_slice(url_bytes);
|
||||
|
||||
return Some(InResponse::Accepted(&self.ep_buf[..3 + len]));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal state for WebUSB
|
||||
pub struct State<'d> {
|
||||
control: MaybeUninit<Control<'d>>,
|
||||
}
|
||||
|
||||
impl<'d> Default for State<'d> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'d> State<'d> {
|
||||
/// Create a new `State`.
|
||||
pub const fn new() -> Self {
|
||||
State {
|
||||
control: MaybeUninit::uninit(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// WebUSB capability implementation.
|
||||
///
|
||||
/// WebUSB is a W3C standard that allows a web page to communicate with USB devices.
|
||||
/// See See https://wicg.github.io/webusb for more information and the browser API.
|
||||
/// This implementation provides one read and one write endpoint.
|
||||
pub struct WebUsb<'d, D: Driver<'d>> {
|
||||
_driver: core::marker::PhantomData<&'d D>,
|
||||
}
|
||||
|
||||
impl<'d, D: Driver<'d>> WebUsb<'d, D> {
|
||||
/// Builder for the WebUSB capability implementation.
|
||||
///
|
||||
/// Pass in a USB `Builder`, a `State`, which holds the the control endpoint state, and a `Config` for the WebUSB configuration.
|
||||
pub fn configure(builder: &mut Builder<'d, D>, state: &'d mut State<'d>, config: &'d Config<'d>) {
|
||||
let mut func = builder.function(USB_CLASS_VENDOR, USB_SUBCLASS_NONE, USB_PROTOCOL_NONE);
|
||||
let mut iface = func.interface();
|
||||
let mut alt = iface.alt_setting(USB_CLASS_VENDOR, USB_SUBCLASS_NONE, USB_PROTOCOL_NONE, None);
|
||||
|
||||
alt.bos_capability(
|
||||
capability_type::PLATFORM,
|
||||
&[
|
||||
// PlatformCapabilityUUID (3408b638-09a9-47a0-8bfd-a0768815b665)
|
||||
0x0,
|
||||
0x38,
|
||||
0xb6,
|
||||
0x08,
|
||||
0x34,
|
||||
0xa9,
|
||||
0x09,
|
||||
0xa0,
|
||||
0x47,
|
||||
0x8b,
|
||||
0xfd,
|
||||
0xa0,
|
||||
0x76,
|
||||
0x88,
|
||||
0x15,
|
||||
0xb6,
|
||||
0x65,
|
||||
// bcdVersion of WebUSB (1.0)
|
||||
0x00,
|
||||
0x01,
|
||||
// bVendorCode
|
||||
config.vendor_code,
|
||||
// iLandingPage
|
||||
if config.landing_url.is_some() { 1 } else { 0 },
|
||||
],
|
||||
);
|
||||
|
||||
let control = state.control.write(Control::new(config));
|
||||
|
||||
drop(func);
|
||||
|
||||
builder.handler(control);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user