aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris Maniewski <[email protected]>2024-04-22 01:37:24 +0200
committerChris Maniewski <[email protected]>2024-04-27 23:14:16 +0200
commit095af927910b06f7af291f76c5236e0da8322402 (patch)
tree5911faf9cc2b4469a2ddd805acb81e2e832c2f69
parentda86c086510490602ffdd688760fb59cc7a1e524 (diff)
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.
-rw-r--r--embassy-usb/src/builder.rs5
-rw-r--r--embassy-usb/src/class/mod.rs1
-rw-r--r--embassy-usb/src/class/web_usb.rs186
-rw-r--r--examples/rp/src/bin/usb_webusb.rs137
4 files changed, 329 insertions, 0 deletions
diff --git a/embassy-usb/src/builder.rs b/embassy-usb/src/builder.rs
index 387b780de..7168e077c 100644
--- a/embassy-usb/src/builder.rs
+++ b/embassy-usb/src/builder.rs
@@ -417,6 +417,11 @@ impl<'a, 'd, D: Driver<'d>> InterfaceAltBuilder<'a, 'd, D> {
417 self.builder.config_descriptor.write(descriptor_type, descriptor); 417 self.builder.config_descriptor.write(descriptor_type, descriptor);
418 } 418 }
419 419
420 /// Add a custom Binary Object Store (BOS) descriptor to this alternate setting.
421 pub fn bos_capability(&mut self, capability_type: u8, capability: &[u8]) {
422 self.builder.bos_descriptor.capability(capability_type, capability);
423 }
424
420 fn endpoint_in(&mut self, ep_type: EndpointType, max_packet_size: u16, interval_ms: u8) -> D::EndpointIn { 425 fn endpoint_in(&mut self, ep_type: EndpointType, max_packet_size: u16, interval_ms: u8) -> D::EndpointIn {
421 let ep = self 426 let ep = self
422 .builder 427 .builder
diff --git a/embassy-usb/src/class/mod.rs b/embassy-usb/src/class/mod.rs
index 452eedf17..b883ed4e5 100644
--- a/embassy-usb/src/class/mod.rs
+++ b/embassy-usb/src/class/mod.rs
@@ -3,3 +3,4 @@ pub mod cdc_acm;
3pub mod cdc_ncm; 3pub mod cdc_ncm;
4pub mod hid; 4pub mod hid;
5pub mod midi; 5pub mod midi;
6pub mod web_usb;
diff --git a/embassy-usb/src/class/web_usb.rs b/embassy-usb/src/class/web_usb.rs
new file mode 100644
index 000000000..10ebf318d
--- /dev/null
+++ b/embassy-usb/src/class/web_usb.rs
@@ -0,0 +1,186 @@
1//! WebUSB API capability implementation.
2//!
3//! See https://wicg.github.io/webusb
4
5use core::mem::MaybeUninit;
6
7use crate::control::{InResponse, Recipient, Request, RequestType};
8use crate::descriptor::capability_type;
9use crate::driver::Driver;
10use crate::{Builder, Handler};
11
12const USB_CLASS_VENDOR: u8 = 0xff;
13const USB_SUBCLASS_NONE: u8 = 0x00;
14const USB_PROTOCOL_NONE: u8 = 0x00;
15
16const WEB_USB_REQUEST_GET_URL: u16 = 0x02;
17const WEB_USB_DESCRIPTOR_TYPE_URL: u8 = 0x03;
18
19/// URL descriptor for WebUSB landing page.
20///
21/// An ecoded URL descriptor to point to a website that is suggested to the user when the device is connected.
22pub struct Url<'d>(&'d str, u8);
23
24impl<'d> Url<'d> {
25 /// Create a new WebUSB URL descriptor.
26 pub fn new(url: &'d str) -> Self {
27 let (prefix, stripped_url) = if let Some(stripped) = url.strip_prefix("https://") {
28 (1, stripped)
29 } else if let Some(stripped) = url.strip_prefix("http://") {
30 (0, stripped)
31 } else {
32 (255, url)
33 };
34 assert!(
35 stripped_url.len() <= 252,
36 "URL too long. ({} bytes). Maximum length is 252 bytes.",
37 stripped_url.len()
38 );
39 Self(stripped_url, prefix)
40 }
41
42 fn as_bytes(&self) -> &[u8] {
43 self.0.as_bytes()
44 }
45
46 fn scheme(&self) -> u8 {
47 self.1
48 }
49}
50
51/// Configuration for WebUSB.
52pub struct Config<'d> {
53 /// Maximum packet size in bytes for the data endpoints.
54 ///
55 /// Valid values depend on the speed at which the bus is enumerated.
56 /// - low speed: 8
57 /// - full speed: 8, 16, 32, or 64
58 /// - high speed: 64
59 pub max_packet_size: u16,
60 /// URL to navigate to when the device is connected.
61 ///
62 /// If defined, shows a landing page which the device manufacturer would like the user to visit in order to control their device.
63 pub landing_url: Option<Url<'d>>,
64 /// Vendor code for the WebUSB request.
65 ///
66 /// 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.
67 pub vendor_code: u8,
68}
69
70struct Control<'d> {
71 ep_buf: [u8; 128],
72 vendor_code: u8,
73 landing_url: Option<&'d Url<'d>>,
74}
75
76impl<'d> Control<'d> {
77 fn new(config: &'d Config<'d>) -> Self {
78 Control {
79 ep_buf: [0u8; 128],
80 vendor_code: config.vendor_code,
81 landing_url: config.landing_url.as_ref(),
82 }
83 }
84}
85
86impl<'d> Handler for Control<'d> {
87 fn control_in(&mut self, req: Request, _data: &mut [u8]) -> Option<InResponse> {
88 let landing_value = if self.landing_url.is_some() { 1 } else { 0 };
89 if req.request_type == RequestType::Vendor
90 && req.recipient == Recipient::Device
91 && req.request == self.vendor_code
92 && req.value == landing_value
93 && req.index == WEB_USB_REQUEST_GET_URL
94 {
95 if let Some(url) = self.landing_url {
96 let url_bytes = url.as_bytes();
97 let len = url_bytes.len();
98
99 self.ep_buf[0] = len as u8 + 3;
100 self.ep_buf[1] = WEB_USB_DESCRIPTOR_TYPE_URL;
101 self.ep_buf[2] = url.scheme();
102 self.ep_buf[3..3 + len].copy_from_slice(url_bytes);
103
104 return Some(InResponse::Accepted(&self.ep_buf[..3 + len]));
105 }
106 }
107 None
108 }
109}
110
111/// Internal state for WebUSB
112pub struct State<'d> {
113 control: MaybeUninit<Control<'d>>,
114}
115
116impl<'d> Default for State<'d> {
117 fn default() -> Self {
118 Self::new()
119 }
120}
121
122impl<'d> State<'d> {
123 /// Create a new `State`.
124 pub const fn new() -> Self {
125 State {
126 control: MaybeUninit::uninit(),
127 }
128 }
129}
130
131/// WebUSB capability implementation.
132///
133/// WebUSB is a W3C standard that allows a web page to communicate with USB devices.
134/// See See https://wicg.github.io/webusb for more information and the browser API.
135/// This implementation provides one read and one write endpoint.
136pub struct WebUsb<'d, D: Driver<'d>> {
137 _driver: core::marker::PhantomData<&'d D>,
138}
139
140impl<'d, D: Driver<'d>> WebUsb<'d, D> {
141 /// Builder for the WebUSB capability implementation.
142 ///
143 /// Pass in a USB `Builder`, a `State`, which holds the the control endpoint state, and a `Config` for the WebUSB configuration.
144 pub fn configure(builder: &mut Builder<'d, D>, state: &'d mut State<'d>, config: &'d Config<'d>) {
145 let mut func = builder.function(USB_CLASS_VENDOR, USB_SUBCLASS_NONE, USB_PROTOCOL_NONE);
146 let mut iface = func.interface();
147 let mut alt = iface.alt_setting(USB_CLASS_VENDOR, USB_SUBCLASS_NONE, USB_PROTOCOL_NONE, None);
148
149 alt.bos_capability(
150 capability_type::PLATFORM,
151 &[
152 // PlatformCapabilityUUID (3408b638-09a9-47a0-8bfd-a0768815b665)
153 0x0,
154 0x38,
155 0xb6,
156 0x08,
157 0x34,
158 0xa9,
159 0x09,
160 0xa0,
161 0x47,
162 0x8b,
163 0xfd,
164 0xa0,
165 0x76,
166 0x88,
167 0x15,
168 0xb6,
169 0x65,
170 // bcdVersion of WebUSB (1.0)
171 0x00,
172 0x01,
173 // bVendorCode
174 config.vendor_code,
175 // iLandingPage
176 if config.landing_url.is_some() { 1 } else { 0 },
177 ],
178 );
179
180 let control = state.control.write(Control::new(config));
181
182 drop(func);
183
184 builder.handler(control);
185 }
186}
diff --git a/examples/rp/src/bin/usb_webusb.rs b/examples/rp/src/bin/usb_webusb.rs
new file mode 100644
index 000000000..09f2c1cfd
--- /dev/null
+++ b/examples/rp/src/bin/usb_webusb.rs
@@ -0,0 +1,137 @@
1//! This example shows how to use USB (Universal Serial Bus) in the RP2040 chip.
2//!
3//! This creates a WebUSB capable device that echoes data back to the host.
4//!
5//! To test this in the browser (ideally host this on localhost:8080, to test the landing page
6//! feature):
7//! ```js
8//! (async () => {
9//! const device = await navigator.usb.requestDevice({ filters: [{ vendorId: 0xf569 }] });
10//! await device.open();
11//! await device.claimInterface(1);
12//! device.transferIn(1, 64).then(data => console.log(data));
13//! await device.transferOut(1, new Uint8Array([1,2,3]));
14//! })();
15//! ```
16
17#![no_std]
18#![no_main]
19
20use defmt::info;
21use embassy_executor::Spawner;
22use embassy_futures::join::join;
23use embassy_rp::bind_interrupts;
24use embassy_rp::peripherals::USB;
25use embassy_rp::usb::{Driver as UsbDriver, InterruptHandler};
26use embassy_usb::class::web_usb::{Config as WebUsbConfig, State, Url, WebUsb};
27use embassy_usb::driver::{Driver, Endpoint, EndpointIn, EndpointOut};
28use embassy_usb::{Builder, Config};
29use {defmt_rtt as _, panic_probe as _};
30
31bind_interrupts!(struct Irqs {
32 USBCTRL_IRQ => InterruptHandler<USB>;
33});
34
35#[embassy_executor::main]
36async fn main(_spawner: Spawner) {
37 let p = embassy_rp::init(Default::default());
38
39 // Create the driver, from the HAL.
40 let driver = UsbDriver::new(p.USB, Irqs);
41
42 // Create embassy-usb Config
43 let mut config = Config::new(0xf569, 0x0001);
44 config.manufacturer = Some("Embassy");
45 config.product = Some("WebUSB example");
46 config.serial_number = Some("12345678");
47 config.max_power = 100;
48 config.max_packet_size_0 = 64;
49
50 // Required for windows compatibility.
51 // https://developer.nordicsemi.com/nRF_Connect_SDK/doc/1.9.1/kconfig/CONFIG_CDC_ACM_IAD.html#help
52 config.device_class = 0xff;
53 config.device_sub_class = 0x00;
54 config.device_protocol = 0x00;
55
56 // Create embassy-usb DeviceBuilder using the driver and config.
57 // It needs some buffers for building the descriptors.
58 let mut config_descriptor = [0; 256];
59 let mut bos_descriptor = [0; 256];
60 let mut control_buf = [0; 64];
61
62 let webusb_config = WebUsbConfig {
63 max_packet_size: 64,
64 vendor_code: 1,
65 // 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.
66 landing_url: Some(Url::new("http://localhost:8080")),
67 };
68
69 let mut state = State::new();
70
71 let mut builder = Builder::new(
72 driver,
73 config,
74 &mut config_descriptor,
75 &mut bos_descriptor,
76 &mut [], // no msos descriptors
77 &mut control_buf,
78 );
79
80 // Create classes on the builder (WebUSB just needs some setup, but doesn't return anything)
81 WebUsb::configure(&mut builder, &mut state, &webusb_config);
82 // Create some USB bulk endpoints for testing.
83 let mut endpoints = WebEndpoints::new(&mut builder, &webusb_config);
84
85 // Build the builder.
86 let mut usb = builder.build();
87
88 // Run the USB device.
89 let usb_fut = usb.run();
90
91 // Do some WebUSB transfers.
92 let webusb_fut = async {
93 loop {
94 endpoints.wait_connected().await;
95 info!("Connected");
96 endpoints.echo().await;
97 }
98 };
99
100 // Run everything concurrently.
101 // If we had made everything `'static` above instead, we could do this using separate tasks instead.
102 join(usb_fut, webusb_fut).await;
103}
104
105struct WebEndpoints<'d, D: Driver<'d>> {
106 write_ep: D::EndpointIn,
107 read_ep: D::EndpointOut,
108}
109
110impl<'d, D: Driver<'d>> WebEndpoints<'d, D> {
111 fn new(builder: &mut Builder<'d, D>, config: &'d WebUsbConfig<'d>) -> Self {
112 let mut func = builder.function(0xff, 0x00, 0x00);
113 let mut iface = func.interface();
114 let mut alt = iface.alt_setting(0xff, 0x00, 0x00, None);
115
116 let write_ep = alt.endpoint_bulk_in(config.max_packet_size);
117 let read_ep = alt.endpoint_bulk_out(config.max_packet_size);
118
119 WebEndpoints { write_ep, read_ep }
120 }
121
122 // Wait until the device's endpoints are enabled.
123 async fn wait_connected(&mut self) {
124 self.read_ep.wait_enabled().await
125 }
126
127 // Echo data back to the host.
128 async fn echo(&mut self) {
129 let mut buf = [0; 64];
130 loop {
131 let n = self.read_ep.read(&mut buf).await.unwrap();
132 let data = &buf[..n];
133 info!("Data read: {:x}", data);
134 self.write_ep.write(data).await.unwrap();
135 }
136 }
137}