webusb-web 0.5.1

WebUSB on the web 🕸️ — Access USB devices from the web browser.
Documentation
use futures_util::StreamExt;
use tokio::sync::oneshot;
use wasm_bindgen_futures::spawn_local;
use wasm_bindgen_test::wasm_bindgen_test;

mod util;
use util::{wait_for_interaction, ResultExt};

use webusb_web::*;

#[wasm_bindgen_test]
async fn test() {
    log!("Getting WebUSB API");
    let usb = Usb::new().expect_log("cannot get WebUSB API");
    log!("Obtained WebUSB API");

    let mut filter = UsbDeviceFilter::new();
    filter.vendor_id = Some(0x06);
    filter.product_id = Some(0x11);

    log!("Starting event stream");
    let mut events = usb.events();
    let (disconnected_tx, disconnected_rx) = oneshot::channel();
    spawn_local(async move {
        let mut disconnected_tx = Some(disconnected_tx);
        while let Some(event) = events.next().await {
            log!("WebUSB event: {event:?}");

            match &event {
                UsbEvent::Disconnected(dev)
                    if dev.vendor_id() == filter.vendor_id.unwrap_log()
                        && dev.product_id() == filter.product_id.unwrap_log() =>
                {
                    if let Some(disconnected_tx) = disconnected_tx.take() {
                        disconnected_tx.send(()).unwrap();
                    }
                }
                _ => (),
            }
        }
    });
    log!("Event stream started");

    log!("Enumerating devices. This is expected to be empty when no devices are paried.");
    let devices = usb.devices().await;
    for device in devices {
        log!("Enumerated USB device: {device:?}");
    }
    log!("Enumeration complete");

    wait_for_interaction(
        "\
        Please connect a Linux device supporting USB gadget mode and run the \
        <pre>custom_interface_device</pre> example from the \
        usb-gadget repository at <pre>https://github.com/surban/usb-gadget</pre>\
        <br>\
        Then click here to enable permission requests and continue.\
        Then select the USB device from the popup shown by your browser.\
    ",
    )
    .await;

    log!("Requesting device with filter {filter:?}");
    let res = usb.request_device([filter.clone()]).await;
    log!("Request result: {res:?}");

    let Ok(dev) = res else {
        log!("No device obtained, exiting.");
        panic!("No device obtained");
    };
    assert!(
        dev.vendor_id() == filter.vendor_id.unwrap_log() && dev.product_id() == filter.product_id.unwrap_log()
    );

    let dev2 = dev.clone();
    assert_eq!(dev2, dev);

    log!("Enumerating devices. The device selected should now be visible.");
    let devices = usb.devices().await;
    let mut found = false;
    for device in devices {
        log!("Enumerated USB device: {device:?}");
        if device.vendor_id() == filter.vendor_id.unwrap_log()
            && device.product_id() == filter.product_id.unwrap_log()
        {
            found = true;
            log!("Device found!");

            assert_eq!(device, dev);
            log!("Device equal!");
        } else {
            assert_ne!(device, dev);
        }
    }
    assert!(found, "device not enumerated after paired");
    log!("Enumeration complete");

    let cfg = dev.configuration().expect_log("device has no active configuration");
    log!("Active device configuration: {cfg:?}");

    let iface = cfg.interfaces.first().unwrap_log();
    let alt = &iface.alternate;
    assert_eq!(alt.alternate_setting, 0);
    assert_eq!(alt.interface_class, 255);
    assert_eq!(alt.interface_subclass, 1);
    assert_eq!(alt.interface_protocol, 2);

    let mut in_packet_size = None;
    let mut in_ep = None;
    let mut out_packet_size = None;
    let mut out_ep = None;
    for ep in &alt.endpoints {
        match ep.direction {
            UsbDirection::In => {
                log!("Found in endpoint: {ep:?}");
                in_ep = Some(ep.endpoint_number);
                in_packet_size = Some(ep.packet_size);
            }
            UsbDirection::Out => {
                log!("Found out endpoint: {ep:?}");
                out_ep = Some(ep.endpoint_number);
                out_packet_size = Some(ep.packet_size);
            }
        }
    }
    let in_packet_size = in_packet_size.unwrap_log();
    let in_ep = in_ep.unwrap_log();
    let out_packet_size = out_packet_size.unwrap_log();
    let out_ep = out_ep.unwrap_log();

    log!("Opening device");
    let open = dev.open().await.expect_log("device failed to open");

    log!("Claiming interface");
    open.claim_interface(0).await.expect_log("failed to claim interface");
    log!("Interface claimed");

    let send_task = async {
        let mut b = 0;
        for _ in 0..1024 {
            log!("Sending {out_packet_size} bytes of {b:x}");
            let data = vec![b; out_packet_size as usize];
            let n = open.transfer_out(out_ep, &data).await.unwrap_log();
            assert_eq!(n, out_packet_size);

            b = b.wrapping_add(1);
        }
    };

    let mut recved = 0;
    let recv_task = async {
        let mut b = None;
        loop {
            let data = open.transfer_in(in_ep, in_packet_size).await.unwrap_log().to_vec();
            log!("Received {} bytes: {:x?}...", data.len(), &data[..16]);
            assert_eq!(data.len(), in_packet_size as usize);

            let b = b.get_or_insert(data[0]);
            assert!(data.iter().all(|x| x == b));
            *b = b.wrapping_add(1);

            recved += 1;
        }
    };

    let ctrl_task = async {
        for i in 0x00..0xff {
            let data = vec![i; i as usize];
            let control = UsbControlRequest::new(
                UsbRequestType::Class,
                UsbRecipient::Interface,
                i,
                0xfffe,
                iface.interface_number.into(),
            );
            let n = open.control_transfer_out(&control, &data).await.unwrap_log();
            assert_eq!(n as usize, data.len());
            log!("Sent control request {i:x} of size {} bytes", data.len());

            let back = open.control_transfer_in(&control, i as _).await.unwrap_log().to_vec();
            log!("Received data from control request {i:x}");

            assert_eq!(back, data);
            log!("Received data matches sent data");
        }
    };

    tokio::select! {
        () = recv_task => (),
        _ = async { tokio::join!(send_task, ctrl_task) } => (),
    }

    assert!(recved >= 100, "not enough packets received");

    log!("Clearing halt");
    open.clear_halt(UsbDirection::In, in_ep).await.unwrap_log();
    open.clear_halt(UsbDirection::Out, out_ep).await.unwrap_log();

    log!("Closing device");
    open.close().await.unwrap_log();

    log!("Reopening device");
    let open = dev.open().await.expect_log("device failed to open");

    log!("Reclaiming interface");
    open.claim_interface(0).await.expect_log("failed to claim interface");
    log!("Interface claimed");

    log!("Terminating USB gadget");
    let control = UsbControlRequest::new(
        UsbRequestType::Class,
        UsbRecipient::Interface,
        0xff,
        0,
        iface.interface_number.into(),
    );
    open.control_transfer_out(&control, &[]).await.unwrap_log();

    log!("Waiting for disconnect event");
    disconnected_rx.await.unwrap_log();

    log!("Device disconnected");
    dev.forget().await;
}