Skip to main content

embassy_usb/class/
web_usb.rs

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 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}