mirror of
https://github.com/embassy-rs/embassy.git
synced 2024-11-25 08:12:30 +00:00
feature: WebUSB capability implementation
This adds the WebUSB implementation as per https://wicg.github.io/webusb/, using one in-endpoint and one out-endpoint as well as an example for the RP2040 to illustrate this capability.
This commit is contained in:
parent
da86c08651
commit
095af92791
@ -417,6 +417,11 @@ impl<'a, 'd, D: Driver<'d>> InterfaceAltBuilder<'a, 'd, D> {
|
|||||||
self.builder.config_descriptor.write(descriptor_type, descriptor);
|
self.builder.config_descriptor.write(descriptor_type, descriptor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add a custom Binary Object Store (BOS) descriptor to this alternate setting.
|
||||||
|
pub fn bos_capability(&mut self, capability_type: u8, capability: &[u8]) {
|
||||||
|
self.builder.bos_descriptor.capability(capability_type, capability);
|
||||||
|
}
|
||||||
|
|
||||||
fn endpoint_in(&mut self, ep_type: EndpointType, max_packet_size: u16, interval_ms: u8) -> D::EndpointIn {
|
fn endpoint_in(&mut self, ep_type: EndpointType, max_packet_size: u16, interval_ms: u8) -> D::EndpointIn {
|
||||||
let ep = self
|
let ep = self
|
||||||
.builder
|
.builder
|
||||||
|
@ -3,3 +3,4 @@ pub mod cdc_acm;
|
|||||||
pub mod cdc_ncm;
|
pub mod cdc_ncm;
|
||||||
pub mod hid;
|
pub mod hid;
|
||||||
pub mod midi;
|
pub mod midi;
|
||||||
|
pub mod web_usb;
|
||||||
|
186
embassy-usb/src/class/web_usb.rs
Normal file
186
embassy-usb/src/class/web_usb.rs
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
137
examples/rp/src/bin/usb_webusb.rs
Normal file
137
examples/rp/src/bin/usb_webusb.rs
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
//! This example shows how to use USB (Universal Serial Bus) in the RP2040 chip.
|
||||||
|
//!
|
||||||
|
//! This creates a WebUSB capable device that echoes data back to the host.
|
||||||
|
//!
|
||||||
|
//! To test this in the browser (ideally host this on localhost:8080, to test the landing page
|
||||||
|
//! feature):
|
||||||
|
//! ```js
|
||||||
|
//! (async () => {
|
||||||
|
//! const device = await navigator.usb.requestDevice({ filters: [{ vendorId: 0xf569 }] });
|
||||||
|
//! await device.open();
|
||||||
|
//! await device.claimInterface(1);
|
||||||
|
//! device.transferIn(1, 64).then(data => console.log(data));
|
||||||
|
//! await device.transferOut(1, new Uint8Array([1,2,3]));
|
||||||
|
//! })();
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
#![no_std]
|
||||||
|
#![no_main]
|
||||||
|
|
||||||
|
use defmt::info;
|
||||||
|
use embassy_executor::Spawner;
|
||||||
|
use embassy_futures::join::join;
|
||||||
|
use embassy_rp::bind_interrupts;
|
||||||
|
use embassy_rp::peripherals::USB;
|
||||||
|
use embassy_rp::usb::{Driver as UsbDriver, InterruptHandler};
|
||||||
|
use embassy_usb::class::web_usb::{Config as WebUsbConfig, State, Url, WebUsb};
|
||||||
|
use embassy_usb::driver::{Driver, Endpoint, EndpointIn, EndpointOut};
|
||||||
|
use embassy_usb::{Builder, Config};
|
||||||
|
use {defmt_rtt as _, panic_probe as _};
|
||||||
|
|
||||||
|
bind_interrupts!(struct Irqs {
|
||||||
|
USBCTRL_IRQ => InterruptHandler<USB>;
|
||||||
|
});
|
||||||
|
|
||||||
|
#[embassy_executor::main]
|
||||||
|
async fn main(_spawner: Spawner) {
|
||||||
|
let p = embassy_rp::init(Default::default());
|
||||||
|
|
||||||
|
// Create the driver, from the HAL.
|
||||||
|
let driver = UsbDriver::new(p.USB, Irqs);
|
||||||
|
|
||||||
|
// Create embassy-usb Config
|
||||||
|
let mut config = Config::new(0xf569, 0x0001);
|
||||||
|
config.manufacturer = Some("Embassy");
|
||||||
|
config.product = Some("WebUSB example");
|
||||||
|
config.serial_number = Some("12345678");
|
||||||
|
config.max_power = 100;
|
||||||
|
config.max_packet_size_0 = 64;
|
||||||
|
|
||||||
|
// Required for windows compatibility.
|
||||||
|
// https://developer.nordicsemi.com/nRF_Connect_SDK/doc/1.9.1/kconfig/CONFIG_CDC_ACM_IAD.html#help
|
||||||
|
config.device_class = 0xff;
|
||||||
|
config.device_sub_class = 0x00;
|
||||||
|
config.device_protocol = 0x00;
|
||||||
|
|
||||||
|
// Create embassy-usb DeviceBuilder using the driver and config.
|
||||||
|
// It needs some buffers for building the descriptors.
|
||||||
|
let mut config_descriptor = [0; 256];
|
||||||
|
let mut bos_descriptor = [0; 256];
|
||||||
|
let mut control_buf = [0; 64];
|
||||||
|
|
||||||
|
let webusb_config = WebUsbConfig {
|
||||||
|
max_packet_size: 64,
|
||||||
|
vendor_code: 1,
|
||||||
|
// If defined, shows a landing page which the device manufacturer would like the user to visit in order to control their device. Suggest the user to navigate to this URL when the device is connected.
|
||||||
|
landing_url: Some(Url::new("http://localhost:8080")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut state = State::new();
|
||||||
|
|
||||||
|
let mut builder = Builder::new(
|
||||||
|
driver,
|
||||||
|
config,
|
||||||
|
&mut config_descriptor,
|
||||||
|
&mut bos_descriptor,
|
||||||
|
&mut [], // no msos descriptors
|
||||||
|
&mut control_buf,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create classes on the builder (WebUSB just needs some setup, but doesn't return anything)
|
||||||
|
WebUsb::configure(&mut builder, &mut state, &webusb_config);
|
||||||
|
// Create some USB bulk endpoints for testing.
|
||||||
|
let mut endpoints = WebEndpoints::new(&mut builder, &webusb_config);
|
||||||
|
|
||||||
|
// Build the builder.
|
||||||
|
let mut usb = builder.build();
|
||||||
|
|
||||||
|
// Run the USB device.
|
||||||
|
let usb_fut = usb.run();
|
||||||
|
|
||||||
|
// Do some WebUSB transfers.
|
||||||
|
let webusb_fut = async {
|
||||||
|
loop {
|
||||||
|
endpoints.wait_connected().await;
|
||||||
|
info!("Connected");
|
||||||
|
endpoints.echo().await;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Run everything concurrently.
|
||||||
|
// If we had made everything `'static` above instead, we could do this using separate tasks instead.
|
||||||
|
join(usb_fut, webusb_fut).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WebEndpoints<'d, D: Driver<'d>> {
|
||||||
|
write_ep: D::EndpointIn,
|
||||||
|
read_ep: D::EndpointOut,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'d, D: Driver<'d>> WebEndpoints<'d, D> {
|
||||||
|
fn new(builder: &mut Builder<'d, D>, config: &'d WebUsbConfig<'d>) -> Self {
|
||||||
|
let mut func = builder.function(0xff, 0x00, 0x00);
|
||||||
|
let mut iface = func.interface();
|
||||||
|
let mut alt = iface.alt_setting(0xff, 0x00, 0x00, None);
|
||||||
|
|
||||||
|
let write_ep = alt.endpoint_bulk_in(config.max_packet_size);
|
||||||
|
let read_ep = alt.endpoint_bulk_out(config.max_packet_size);
|
||||||
|
|
||||||
|
WebEndpoints { write_ep, read_ep }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait until the device's endpoints are enabled.
|
||||||
|
async fn wait_connected(&mut self) {
|
||||||
|
self.read_ep.wait_enabled().await
|
||||||
|
}
|
||||||
|
|
||||||
|
// Echo data back to the host.
|
||||||
|
async fn echo(&mut self) {
|
||||||
|
let mut buf = [0; 64];
|
||||||
|
loop {
|
||||||
|
let n = self.read_ep.read(&mut buf).await.unwrap();
|
||||||
|
let data = &buf[..n];
|
||||||
|
info!("Data read: {:x}", data);
|
||||||
|
self.write_ep.write(data).await.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user