crazyradio_webusb/
lib.rs

1//! # Crazyradio driver for Rust using the WebUSB API.
2//!
3//! This driver is intended to be used when targetting the web browser with Wasm.
4//! It replicates the async API subset of the native [crazyradio crate](https://crates.io/crates/crazyradio).
5//!
6//! The main intention of this crate is to be used as a compile-time replacement
7//! for the native [crazyradio](https://github.com/ataffanel/crazyradio-rs) crate in
8//! the [crazyflie-link](https://github.com/ataffanel/crazyflie-link-rs) crate.
9//! The async functions to create the crazyradio can be used and then
10//! the [Crazyradio] object must be passed and used though the [SharedCrazyradio]
11//! object:
12//!
13//! ``` no_run
14//! # use crazyradio_webusb::{Crazyradio, SharedCrazyradio, Error};
15//! # async fn test() -> Result<(), Error> {
16//! let radio = Crazyradio::open_nth_async(0).await?;
17//! let shared_radio = SharedCrazyradio::new(radio);
18//! # Ok(())
19//! # }
20//! ```
21
22use std::convert::TryInto;
23
24use wasm_bindgen::{prelude::*, JsCast};
25
26use wasm_bindgen_futures::JsFuture;
27
28use futures_util::lock::Mutex;
29use web_sys::UsbDevice;
30
31type Result<T> = std::result::Result<T, Error>;
32
33/// Ack status
34#[derive(Default, Debug, Copy, Clone)]
35pub struct Ack {
36    /// At true if an ack packet has been received
37    pub received: bool,
38    /// Value of the nRF24 power detector when receiving the ack packet
39    pub power_detector: bool,
40    /// Number of time the packet was sent before an ack was received
41    pub retry: usize,
42    /// Length of the ack payload
43    pub length: usize,
44}
45
46/// Shared Crazyradio
47pub struct SharedCrazyradio {
48    radio: Mutex<Crazyradio>,
49}
50
51/// Crazyradio sharable between uTasks/connections
52///
53/// Implement the same API as the Crazyradio crate's [SharedCrazyradio].
54///
55/// [SharedCrazyradio]: https://docs.rs/crazyradio/0.2.0/crazyradio/struct.SharedCrazyradio.html#impl-1
56impl SharedCrazyradio {
57    pub fn new(radio: Crazyradio) -> Self {
58        Self {
59            radio: Mutex::new(radio),
60        }
61    }
62
63    pub async fn send_packet_async(
64        &self,
65        channel: Channel,
66        address: [u8; 5],
67        payload: Vec<u8>,
68    ) -> Result<(Ack, Vec<u8>)> {
69        let mut radio = self.radio.lock().await;
70        radio.set_channel_async(channel).await?;
71        radio.set_address_async(&address).await?;
72        radio.send_packet_async(payload.clone()).await
73    }
74
75    pub async fn scan_async(
76        &self,
77        start: Channel,
78        stop: Channel,
79        address: [u8; 5],
80        payload: Vec<u8>,
81    ) -> Result<Vec<Channel>> {
82        let mut found = Vec::new();
83
84        let start: u8 = start.into();
85        let stop: u8 = stop.into();
86
87        for channel in start..=stop {
88            let mut radio = self.radio.lock().await;
89            radio.set_address_async(&address).await?;
90            radio.set_channel_async(channel.try_into()?).await?;
91            let (ack, _) = radio.send_packet_async(payload.clone()).await?;
92            if ack.received {
93                found.push(channel.try_into()?);
94            }
95        }
96
97        Ok(found)
98    }
99}
100
101/// Crazyradio dongle
102///
103/// This struct only contains function to create a new `Crazyradio` object. It
104/// needs to be passed to the [SharedCrazyradio] to be used.
105pub struct Crazyradio {
106    device: web_sys::UsbDevice,
107    current_channel: Option<Channel>,
108    current_address: Option<[u8; 5]>,
109}
110
111const SET_RADIO_CHANNEL: u8 = 0x01;
112const SET_RADIO_ADDRESS: u8 = 0x02;
113
114impl Crazyradio {
115    /// Open first available Crazyradio
116    ///
117    /// If no radio is paired in the current browser's tab, a pairing request will
118    /// be requested to the browser.
119    ///
120    /// ## Note
121    ///
122    /// If a pairing request is required, this function must be called from a user
123    /// interaction (eg. in response to a button/link click event). The [Crazyradio::list_serials_async()]
124    /// funtion can be called to find out if any radio is currently paired
125    pub async fn open_first_async() -> Result<Crazyradio> {
126        Self::open_nth_async(0).await
127    }
128
129     /// Open nth available Crazyradio
130    ///
131    /// If no radio is paired in the current browser's tab, a pairing request will
132    /// be requested to the browser.
133    ///
134    /// ## Limitation
135    /// This function can currently be called only with the argument `0`, any other
136    /// value will return the error [Error::InvalidArgument]
137    ///
138    /// ## Note
139    /// If a pairing request is required, this function must be called from a user
140    /// interaction (eg. in response to a button/link click event). The [Crazyradio::list_serials_async()]
141    /// funtion can be called to find out if any radio is currently paired
142    pub async fn open_nth_async(nth: usize) -> Result<Crazyradio> {
143        if nth != 0 {
144            return Err(Error::InvalidArgument);
145        }
146
147        let window = web_sys::window().expect("No global 'window' exists!");
148        let navigator: web_sys::Navigator = window.navigator();
149        let usb = navigator.usb();
150
151        let filter: serde_json::Value =
152            serde_json::from_str(r#"{ "filters": [{ "vendorId": 6421 }] }"#).unwrap();
153        let filter = JsValue::from_serde(&filter).unwrap();
154
155        let devices: js_sys::Array = JsFuture::from(usb.get_devices()).await?.into();
156
157        // Open radio if one is already paired and plugged
158        // Otherwise ask the user to pair a new radio
159        let device: web_sys::UsbDevice = if devices.length() > 0 {
160            devices.get(0).dyn_into().unwrap()
161        } else {
162            JsFuture::from(usb.request_device(&filter.into()))
163                .await?
164                .dyn_into()
165                .map_err(|e| Error::BrowserError(format!("{:?}", e)))?
166        };
167
168        JsFuture::from(device.open()).await?;
169        JsFuture::from(device.claim_interface(0)).await?;
170
171        Ok(Self {
172            device,
173            current_channel: None,
174            current_address: None,
175        })
176    }
177
178    /// Open radio by serial number
179    ///
180    /// This function will never trigger the browser to show the device pairing window.
181    pub async fn open_by_serial_async(serial: &str) -> Result<Crazyradio> {
182        let window = web_sys::window().expect("No global 'window' exists!");
183        let navigator: web_sys::Navigator = window.navigator();
184        let usb = navigator.usb();
185
186        let devices: js_sys::Array = JsFuture::from(usb.get_devices()).await?.into();
187
188        // Find the serial number in the list of already paired radios
189        let device: web_sys::UsbDevice = devices
190            .find(&mut |device: JsValue, _, _| {
191                device.dyn_into::<UsbDevice>().unwrap().serial_number() == Some(serial.to_owned())
192            })
193            .dyn_into()
194            .map_err(|_| Error::NotFound)?;
195
196        JsFuture::from(device.open()).await?;
197        JsFuture::from(device.claim_interface(0)).await?;
198
199        Ok(Self {
200            device,
201            current_channel: None,
202            current_address: None,
203        })
204    }
205
206    /// Return the list of serial number from Crazyradio that are both paired and currently connected
207    pub async fn list_serials_async() -> Result<Vec<String>> {
208        let window = web_sys::window().expect("No global 'window' exists!");
209        let navigator: web_sys::Navigator = window.navigator();
210        let usb = navigator.usb();
211
212        let devices: js_sys::Array = JsFuture::from(usb.get_devices()).await?.into();
213
214        let mut serials = Vec::new();
215
216        for device in devices.iter() {
217            let device = device.dyn_into::<web_sys::UsbDevice>().unwrap();
218            if let Some(serial) = device.serial_number() {
219                serials.push(serial);
220            }
221        }
222
223        Ok(serials)
224    }
225
226    async fn set_address_async(&mut self, address: &[u8; 5]) -> Result<()> {
227        if self.current_address != Some(*address) {
228            let parameter = web_sys::UsbControlTransferParameters::new(
229                0,
230                web_sys::UsbRecipient::Device,
231                SET_RADIO_ADDRESS,
232                web_sys::UsbRequestType::Vendor,
233                0,
234            );
235
236            let mut data = *address;
237            let transfer = self
238                .device
239                .control_transfer_out_with_u8_array(&parameter, &mut data);
240
241            let _ = JsFuture::from(transfer)
242                .await?
243                .dyn_into::<web_sys::UsbOutTransferResult>()
244                .unwrap();
245
246            self.current_address = Some(*address);
247        }
248
249        Ok(())
250    }
251
252    async fn set_channel_async(&mut self, channel: Channel) -> Result<()> {
253        if self.current_channel != Some(channel) {
254            let parameter = web_sys::UsbControlTransferParameters::new(
255                0,
256                web_sys::UsbRecipient::Device,
257                SET_RADIO_CHANNEL,
258                web_sys::UsbRequestType::Vendor,
259                channel.into(),
260            );
261
262            let mut data = [];
263            let transfer = self
264                .device
265                .control_transfer_out_with_u8_array(&parameter, &mut data);
266
267            let _ = JsFuture::from(transfer)
268                .await?
269                .dyn_into::<web_sys::UsbOutTransferResult>()
270                .unwrap();
271
272            self.current_channel = Some(channel);
273        }
274
275        Ok(())
276    }
277
278    async fn send_packet_async(&self, packet: Vec<u8>) -> Result<(Ack, Vec<u8>)> {
279        let mut packet = packet;
280        JsFuture::from(self.device.transfer_out_with_u8_array(0x01, &mut packet)).await?;
281
282        let data = JsFuture::from(self.device.transfer_in(0x01, 64))
283            .await?
284            .dyn_into::<web_sys::UsbInTransferResult>()
285            .unwrap();
286
287        let mut pk = Vec::new();
288        for i in 1..data.data().unwrap().byte_length() {
289            pk.push(data.data().unwrap().get_uint8(i));
290        }
291
292        let mut ack = Ack::default();
293        if data.data().unwrap().get_uint8(0) != 0 {
294            ack.received = true;
295            ack.length = pk.len();
296        }
297
298        Ok((ack, pk))
299    }
300}
301
302#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
303#[cfg_attr(feature = "serde_support", derive(Serialize))]
304pub struct Channel(u8);
305
306impl Channel {
307    pub fn from_number(channel: u8) -> Result<Self> {
308        if channel < 126 {
309            Ok(Channel(channel))
310        } else {
311            Err(Error::InvalidArgument)
312        }
313    }
314}
315
316// U8 -> Channel is not safe so we only implement Channel -> U8
317#[allow(clippy::from_over_into)]
318impl Into<u8> for Channel {
319    fn into(self) -> u8 {
320        self.0
321    }
322}
323
324// U16 -> Channel is not safe so we only implement Channel -> U16
325#[allow(clippy::from_over_into)]
326impl Into<u16> for Channel {
327    fn into(self) -> u16 {
328        self.0.into()
329    }
330}
331
332impl TryInto<Channel> for u8 {
333    type Error = Error;
334
335    fn try_into(self) -> std::result::Result<Channel, Self::Error> {
336        Channel::from_number(self)
337    }
338}
339
340#[derive(thiserror::Error, Debug, Clone)]
341pub enum Error {
342    #[error("Crazyradio not found")]
343    NotFound,
344    #[error("Invalid arguments")]
345    InvalidArgument,
346    #[error("Crazyradio version not supported")]
347    DongleVersionNotSupported,
348    #[error("Browser error")]
349    BrowserError(String),
350}
351
352impl From<JsValue> for Error {
353    fn from(e: JsValue) -> Self {
354        Self::BrowserError(format!("{:?}", e))
355    }
356}