use js_sys::{Reflect, Uint8Array};
use wasm_bindgen::JsValue;
use wasm_bindgen_futures::JsFuture;
use web_sys::{UsbDevice, UsbTransferStatus};
use super::{Transport, CLASS_VENDOR_SPECIFIC, EP_IN, EP_OUT};
use crate::error::{Error, Result};
const fn endpoint_number(address: u8) -> u8 {
address & 0x0f
}
fn describe(err: &JsValue) -> String {
let field = |k: &str| {
Reflect::get(err, &JsValue::from_str(k))
.ok()
.and_then(|v| v.as_string())
};
match (field("name"), field("message")) {
(Some(name), Some(msg)) => format!("{name}: {msg}"),
(Some(text), None) | (None, Some(text)) => text,
(None, None) => err.as_string().unwrap_or_else(|| format!("{err:?}")),
}
}
fn map_err(what: &str) -> impl FnOnce(JsValue) -> Error + '_ {
move |e| Error::Transport(format!("{what}: {}", describe(&e)))
}
fn check_status(status: UsbTransferStatus, what: &str) -> Result<()> {
match status {
UsbTransferStatus::Ok => Ok(()),
UsbTransferStatus::Stall => Err(Error::Transport(format!(
"{what}: endpoint stalled; the device must be reconnected or clearHalt() called"
))),
UsbTransferStatus::Babble => Err(Error::Transport(format!(
"{what}: device sent more than the transfer asked for"
))),
other => Err(Error::Transport(format!(
"{what}: unexpected transfer status {other:?}"
))),
}
}
pub struct WebUsbTransport {
device: UsbDevice,
interface_number: Option<u8>,
}
impl WebUsbTransport {
pub fn new(device: UsbDevice) -> Self {
Self {
device,
interface_number: None,
}
}
pub async fn open(device: UsbDevice) -> Result<Self> {
JsFuture::from(device.open())
.await
.map_err(map_err("opening the device"))?;
if device.configuration().is_none() {
JsFuture::from(device.select_configuration(1))
.await
.map_err(map_err("selecting configuration 1"))?;
}
let configuration = device.configuration().ok_or_else(|| {
Error::Transport("device reports no active configuration after selecting one".into())
})?;
let interface_number = configuration
.interfaces()
.iter()
.find(|iface| iface.alternate().interface_class() == CLASS_VENDOR_SPECIFIC)
.map(|iface| iface.interface_number())
.ok_or_else(|| {
Error::Transport(
"device exposes no vendor-specific interface; is this a Nord?".into(),
)
})?;
JsFuture::from(device.claim_interface(interface_number))
.await
.map_err(map_err(
"claiming the vendor interface (another application holding it — \
Nord Sound Manager — will block this)",
))?;
Ok(Self {
device,
interface_number: Some(interface_number),
})
}
pub fn device(&self) -> &UsbDevice {
&self.device
}
pub async fn close(self) -> Result<()> {
if let Some(number) = self.interface_number {
JsFuture::from(self.device.release_interface(number))
.await
.map_err(map_err("releasing the vendor interface"))?;
}
JsFuture::from(self.device.close())
.await
.map_err(map_err("closing the device"))?;
Ok(())
}
}
impl Transport for WebUsbTransport {
async fn write(&mut self, buf: &[u8]) -> Result<()> {
let data = Uint8Array::from(buf);
let transfer = self
.device
.transfer_out_with_u8_array(endpoint_number(EP_OUT), &data)
.map_err(map_err("bulk write"))?;
let result = JsFuture::from(transfer)
.await
.map_err(map_err("bulk write"))?;
check_status(result.status(), "bulk write")?;
let written = result.bytes_written() as usize;
if written != buf.len() {
return Err(Error::Transport(format!(
"bulk write sent {written} of {} bytes",
buf.len()
)));
}
Ok(())
}
async fn read(&mut self, max: usize) -> Result<Vec<u8>> {
let result = JsFuture::from(self.device.transfer_in(endpoint_number(EP_IN), max as u32))
.await
.map_err(map_err("bulk read"))?;
check_status(result.status(), "bulk read")?;
let view = result
.data()
.ok_or_else(|| Error::Transport("bulk read returned no data".into()))?;
Ok(Uint8Array::new_with_byte_offset_and_length(
&view.buffer(),
view.byte_offset() as u32,
view.byte_length() as u32,
)
.to_vec())
}
}