use std::sync::mpsc::{self, Sender};
use std::thread::JoinHandle;
use std::time::{Duration, Instant};
use eframe::egui;
use nord_usb::device::Device;
use nord_usb::transport::{usb, UsbTransport, CLASS_VENDOR_SPECIFIC};
use super::worker::{self, Emit, Flow};
use super::{DeviceCard, DeviceCmd, DeviceEvent};
const SETTLE: Duration = Duration::from_millis(10);
pub struct Link {
ctx: egui::Context,
events: Sender<DeviceEvent>,
commands: Option<Sender<DeviceCmd>>,
worker: Option<JoinHandle<()>>,
}
impl Link {
pub fn new(ctx: egui::Context, events: Sender<DeviceEvent>) -> Link {
Link {
ctx,
events,
commands: None,
worker: None,
}
}
pub fn connect(&mut self) {
let (tx, rx) = mpsc::channel::<DeviceCmd>();
self.commands = Some(tx);
let emit = Emit::new(self.events.clone(), self.ctx.clone());
self.worker = Some(std::thread::spawn(move || {
let mut device = match open() {
Ok((card, device)) => {
emit.send(DeviceEvent::Connected(card));
device
}
Err(why) => {
emit.send(DeviceEvent::ConnectFailed(why));
return;
}
};
let mut flow = nord_usb::block_on(worker::announce(&mut device, &emit));
if flow == Flow::Continue {
flow = Flow::Released;
while let Ok(cmd) = rx.recv() {
flow = nord_usb::block_on(worker::run(&mut device, cmd, &emit));
if flow != Flow::Continue {
break;
}
}
}
drop(device);
emit.send(DeviceEvent::Disconnected {
lost: flow == Flow::Lost,
});
}));
}
pub fn disconnect(&mut self) {
if let Some(tx) = self.commands.take() {
let _ = tx.send(DeviceCmd::Disconnect);
}
}
pub fn join(&mut self, wait: Duration) {
let Some(worker) = self.worker.take() else {
return;
};
let since = Instant::now();
while !worker.is_finished() && since.elapsed() < wait {
std::thread::sleep(SETTLE);
}
if worker.is_finished() {
let _ = worker.join();
}
}
pub fn send(&mut self, cmd: DeviceCmd) {
if let Some(tx) = &self.commands {
let _ = tx.send(cmd);
}
}
}
fn open() -> Result<(DeviceCard, Device<UsbTransport>), String> {
let devices = usb::list().map_err(|e| e.to_string())?;
let info = devices
.into_iter()
.next()
.ok_or("no Clavia device found — is the instrument awake and on a data cable?")?;
let Some(interface) = info
.interfaces()
.find(|i| i.class() == CLASS_VENDOR_SPECIFIC)
else {
return Err(format!(
"{} exposes no vendor interface; this tool cannot drive it",
info.product_string().unwrap_or("the attached device"),
));
};
let interface = interface.interface_number();
let transport = UsbTransport::open(&info).map_err(|e| e.to_string())?;
let identity = transport.identity().ok();
let card = DeviceCard {
build: identity.map(|id| id.build),
firmware: identity.map(|id| id.firmware),
interface: Some(interface),
kind: identity.map(|id| id.kind),
manufacturer: info.manufacturer_string().map(str::to_string),
max_transfer: identity.map(|id| id.max_transfer),
product: info
.product_string()
.unwrap_or("unnamed device")
.to_string(),
product_id: info.product_id(),
serial: info.serial_number().map(str::to_string),
vendor_id: info.vendor_id(),
};
let device = Device::new(transport);
Ok((card, device))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn waiting_for_the_worker_is_bounded() {
let mut link = Link::new(egui::Context::default(), mpsc::channel().0);
let (stop, held) = mpsc::channel::<()>();
link.worker = Some(std::thread::spawn(move || {
let _ = held.recv();
}));
let wait = Duration::from_millis(50);
let started = Instant::now();
link.join(wait);
assert!(started.elapsed() < wait * 10, "{:?}", started.elapsed());
assert!(link.worker.is_none(), "and the handle is let go either way");
drop(stop);
}
}