use std::cell::RefCell;
use std::collections::VecDeque;
use std::rc::Rc;
use std::sync::mpsc::Sender;
use eframe::egui;
use js_sys::Promise;
use nord_usb::transport::{web::WebUsbTransport, VENDOR_ID};
use wasm_bindgen::closure::Closure;
use wasm_bindgen::{JsCast as _, JsValue};
use wasm_bindgen_futures::{spawn_local, JsFuture};
use web_sys::{UsbConnectionEvent, UsbDevice, UsbDeviceFilter, UsbDeviceRequestOptions};
use super::worker::{self, Emit, Flow};
use super::{DeviceCard, DeviceCmd, DeviceEvent};
#[derive(Default)]
struct Inner {
transport: Option<WebUsbTransport>,
device: Option<UsbDevice>,
queue: VecDeque<DeviceCmd>,
busy: bool,
lost: bool,
}
pub struct Link {
emit: Emit,
inner: Rc<RefCell<Inner>>,
watch: Option<Closure<dyn FnMut(UsbConnectionEvent)>>,
}
impl Link {
pub fn new(ctx: egui::Context, events: Sender<DeviceEvent>) -> Link {
Link {
emit: Emit::new(events, ctx),
inner: Rc::new(RefCell::new(Inner::default())),
watch: None,
}
}
pub fn connect(&mut self) {
let request = match request_device() {
Ok(request) => request,
Err(e) => {
self.emit.send(DeviceEvent::ConnectFailed(describe(&e)));
return;
}
};
self.inner.borrow_mut().lost = false;
if self.watch.is_none() {
self.watch = watch_for_unplug(&self.inner, &self.emit);
}
let emit = self.emit.clone();
let inner = self.inner.clone();
spawn_local(async move {
let device = match JsFuture::from(request).await {
Ok(device) => device,
Err(e) => {
emit.send(DeviceEvent::ConnectFailed(format!(
"no device chosen: {}",
describe(&e)
)));
return;
}
};
let card = DeviceCard {
build: None,
firmware: None,
interface: None,
kind: None,
manufacturer: device.manufacturer_name(),
max_transfer: None,
product: device
.product_name()
.unwrap_or_else(|| "unnamed device".into()),
product_id: device.product_id(),
serial: device.serial_number(),
vendor_id: device.vendor_id(),
};
let chosen = device.clone();
match WebUsbTransport::open(device).await {
Ok(transport) => {
let mut state = inner.borrow_mut();
state.transport = Some(transport);
state.device = Some(chosen);
drop(state);
emit.send(DeviceEvent::Connected(card));
}
Err(e) => emit.send(DeviceEvent::ConnectFailed(e.to_string())),
}
pump(&inner, &emit);
});
}
pub fn disconnect(&mut self) {
self.send(DeviceCmd::Disconnect);
}
pub fn send(&mut self, cmd: DeviceCmd) {
self.inner.borrow_mut().queue.push_back(cmd);
pump(&self.inner, &self.emit);
}
}
fn pump(inner: &Rc<RefCell<Inner>>, emit: &Emit) {
let (transport, cmd) = {
let mut state = inner.borrow_mut();
if state.busy || state.transport.is_none() {
return;
}
let Some(cmd) = state.queue.pop_front() else {
return;
};
state.busy = true;
(state.transport.take(), cmd)
};
let Some(mut transport) = transport else {
return;
};
let inner = inner.clone();
let emit = emit.clone();
spawn_local(async move {
let flow = worker::run(&mut transport, cmd, &emit).await;
if flow == Flow::Continue && !inner.borrow().lost {
{
let mut state = inner.borrow_mut();
state.transport = Some(transport);
state.busy = false;
}
return pump(&inner, &emit);
}
if flow == Flow::Released {
if let Err(e) = transport.close().await {
emit.send(DeviceEvent::OpFailed(e.to_string()));
}
}
let said = {
let mut state = inner.borrow_mut();
state.busy = false;
state.queue.clear();
state.device = None;
let said = state.lost;
state.lost = said || flow == Flow::Lost;
said
};
if !said {
emit.send(DeviceEvent::Disconnected {
lost: flow == Flow::Lost,
});
}
});
}
fn watch_for_unplug(
inner: &Rc<RefCell<Inner>>,
emit: &Emit,
) -> Option<Closure<dyn FnMut(UsbConnectionEvent)>> {
let usb = web_sys::window()?.navigator().usb();
let held = inner.clone();
let emit = emit.clone();
let watch = Closure::wrap(Box::new(move |event: UsbConnectionEvent| {
let went = event.device();
if held.borrow().device.as_ref() != Some(&went) {
return;
}
let mut state = held.borrow_mut();
state.queue.clear();
state.device = None;
state.transport = None;
let said = std::mem::replace(&mut state.lost, true);
drop(state);
if !said {
emit.send(DeviceEvent::Disconnected { lost: true });
}
}) as Box<dyn FnMut(UsbConnectionEvent)>);
usb.set_ondisconnect(Some(watch.as_ref().unchecked_ref()));
Some(watch)
}
fn request_device() -> Result<Promise<UsbDevice>, JsValue> {
let usb = web_sys::window()
.ok_or_else(|| JsValue::from_str("no window"))?
.navigator()
.usb();
let filter = UsbDeviceFilter::new();
filter.set_vendor_id(VENDOR_ID);
Ok(usb.request_device(&UsbDeviceRequestOptions::new(&[filter])))
}
fn describe(err: &JsValue) -> String {
let field = |k: &str| {
js_sys::Reflect::get(err, &JsValue::from_str(k))
.ok()
.and_then(|v| v.as_string())
};
match (field("name"), field("message")) {
(Some(name), Some(message)) => format!("{name}: {message}"),
(Some(only), None) | (None, Some(only)) => only,
(None, None) => err.as_string().unwrap_or_else(|| format!("{err:?}")),
}
}