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::device::Device;
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)]
enum Slot {
#[default]
Absent,
Idle(Device<WebUsbTransport>),
Busy,
Gone,
}
impl Slot {
fn take(&mut self) -> Option<Device<WebUsbTransport>> {
match std::mem::replace(self, Slot::Busy) {
Slot::Idle(device) => Some(device),
held => {
*self = held;
None
}
}
}
}
#[derive(Default)]
struct Inner {
slot: Slot,
chosen: Option<UsbDevice>,
queue: VecDeque<DeviceCmd>,
generation: u64,
}
impl Inner {
fn start(&mut self) -> Option<(Device<WebUsbTransport>, DeviceCmd)> {
let device = self.slot.take()?;
match self.queue.pop_front() {
Some(cmd) => Some((device, cmd)),
None => {
self.slot = Slot::Idle(device);
None
}
}
}
}
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;
}
};
let generation = {
let mut state = self.inner.borrow_mut();
state.queue.clear();
state.slot = Slot::Absent;
state.generation = state.generation.wrapping_add(1);
state.generation
};
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 chosen = match JsFuture::from(request).await {
Ok(chosen) => chosen,
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: chosen.manufacturer_name(),
max_transfer: None,
product: chosen
.product_name()
.unwrap_or_else(|| "unnamed device".into()),
product_id: chosen.product_id(),
serial: chosen.serial_number(),
vendor_id: chosen.vendor_id(),
};
match WebUsbTransport::open(chosen.clone()).await {
Ok(transport) => {
let mut device = Device::new(transport);
emit.send(DeviceEvent::Connected(card));
let flow = worker::announce(&mut device, &emit).await;
let keep = flow == Flow::Continue && inner.borrow().generation == generation;
match keep {
true => {
let mut state = inner.borrow_mut();
state.slot = Slot::Idle(device);
state.chosen = Some(chosen);
}
false => retire(&inner, &emit, device, flow, generation).await,
}
}
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 (started, generation) = {
let mut state = inner.borrow_mut();
let generation = state.generation;
(state.start(), generation)
};
let Some((mut device, cmd)) = started else {
return;
};
let inner = inner.clone();
let emit = emit.clone();
spawn_local(async move {
let flow = worker::run(&mut device, cmd, &emit).await;
let carry_on = {
let state = inner.borrow();
flow == Flow::Continue
&& state.generation == generation
&& !matches!(state.slot, Slot::Gone)
};
if carry_on {
inner.borrow_mut().slot = Slot::Idle(device);
return pump(&inner, &emit);
}
retire(&inner, &emit, device, flow, generation).await;
});
}
async fn retire(
inner: &Rc<RefCell<Inner>>,
emit: &Emit,
device: Device<WebUsbTransport>,
flow: Flow,
generation: u64,
) {
let closed = device.into_transport().close().await;
let said = {
let mut state = inner.borrow_mut();
if state.generation != generation {
return;
}
state.queue.clear();
state.chosen = None;
let said = matches!(state.slot, Slot::Gone);
state.slot = Slot::Gone;
said
};
if let (Err(e), false) = (closed, flow == Flow::Lost) {
emit.send(DeviceEvent::OpFailed(e.to_string()));
}
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().chosen.as_ref() != Some(&went) {
return;
}
let mut state = held.borrow_mut();
state.queue.clear();
state.chosen = None;
let said = matches!(state.slot, Slot::Gone);
state.slot = Slot::Gone;
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:?}")),
}
}