use std::time::Duration;
use nusb::transfer::{Control, ControlType, Queue, RequestBuffer};
use nusb::{DeviceInfo, Interface};
use super::record::Recorder;
use super::{Transport, CLASS_VENDOR_SPECIFIC, EP_IN, EP_OUT};
use crate::deadline::with_timeout;
use crate::error::{Error, Result};
pub use super::{PRODUCT_ID_ELECTRO5, VENDOR_ID};
pub use nusb::transfer::Recipient;
const REAP_LIMIT: Duration = Duration::from_secs(2);
fn map_err<E: std::fmt::Display>(what: &str) -> impl FnOnce(E) -> Error + '_ {
move |e| Error::Transport(format!("{what}: {e}"))
}
fn open_error(e: std::io::Error) -> Error {
let hint = if cfg!(target_os = "linux") && e.kind() == std::io::ErrorKind::PermissionDenied {
format!(
" — no write access to the device node; what is usually missing is a udev rule \
granting it for vendor {VENDOR_ID:04x}"
)
} else {
String::new()
};
Error::Transport(format!("opening device: {e}{hint}"))
}
pub fn list() -> Result<Vec<DeviceInfo>> {
Ok(nusb::list_devices()
.map_err(map_err("listing usb devices"))?
.filter(|d| d.vendor_id() == VENDOR_ID)
.collect())
}
pub struct UsbTransport {
interface: Interface,
product: Option<String>,
record: Option<Recorder>,
read_queue: Queue<RequestBuffer>,
}
impl UsbTransport {
pub fn open_first() -> Result<Self> {
let info = list()?
.into_iter()
.next()
.ok_or_else(|| Error::Transport("no Clavia device found".into()))?;
Self::open(&info)
}
pub fn open(info: &DeviceInfo) -> Result<Self> {
let iface_num = info
.interfaces()
.find(|i| i.class() == CLASS_VENDOR_SPECIFIC)
.map(|i| i.interface_number())
.ok_or_else(|| {
Error::Transport(
"device exposes no vendor-specific interface; is this a Nord?".into(),
)
})?;
let device = info.open().map_err(open_error)?;
let interface = device.claim_interface(iface_num).map_err(map_err(
"claiming the vendor interface (another application holding it — Nord Sound \
Manager, or a WebUSB page — will block this)",
))?;
let read_queue = interface.bulk_in_queue(EP_IN);
Ok(Self {
interface,
product: info.product_string().map(str::to_owned),
record: None,
read_queue,
})
}
pub fn recording_to(mut self, path: &std::path::Path) -> Result<Self> {
self.record = Some(Recorder::create(path, self.describe().as_deref())?);
Ok(self)
}
fn describe(&self) -> Option<String> {
let product = self.product.as_deref()?;
Some(match self.identity() {
Ok(id) => format!(
"{product}, firmware v{}.{:02} build {}",
id.firmware / 100,
id.firmware % 100,
id.build
),
Err(_) => product.to_string(),
})
}
pub fn mark_intent(&mut self, intent: &str) {
if let Some(r) = self.record.as_mut() {
r.intent(intent);
}
}
pub fn mark(&mut self, what: &str) {
if let Some(r) = self.record.as_mut() {
r.comment(what);
}
}
pub fn mark_expect(&mut self, e: &Error) {
if let Some(r) = self.record.as_mut() {
r.expect(e);
}
}
pub fn recording_result(&mut self) -> Result<()> {
match self.record.as_mut() {
Some(r) => r.check(),
None => Ok(()),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Identity {
pub firmware: u16,
pub max_transfer: u32,
pub kind: u16,
pub build: u16,
}
impl UsbTransport {
pub fn identity(&self) -> Result<Identity> {
let limit = Duration::from_millis(500);
let word = |request: u8| -> Result<u16> {
let b = self.vendor_control_in(Recipient::Device, request, 0, 0, 2, limit)?;
if b.len() < 2 {
return Err(Error::Transport(format!(
"vendor request {request:#04x} returned {} bytes, expected 2",
b.len()
)));
}
Ok(u16::from_le_bytes([b[0], b[1]]))
};
let max = self.vendor_control_in(Recipient::Device, 0x08, 0, 0, 4, limit)?;
if max.len() < 4 {
return Err(Error::Transport(format!(
"vendor request 0x08 returned {} bytes, expected 4",
max.len()
)));
}
Ok(Identity {
kind: word(0x00)?,
firmware: word(0x04)?,
build: word(0x05)?,
max_transfer: u32::from_le_bytes([max[0], max[1], max[2], max[3]]),
})
}
pub async fn interrupt_read(
&mut self,
len: usize,
timeout: Duration,
) -> Result<Option<Vec<u8>>> {
use crate::deadline::with_timeout;
let buf = nusb::transfer::RequestBuffer::new(len);
match with_timeout(self.interface.interrupt_in(0x81, buf), timeout).await {
Some(completion) => {
completion.status.map_err(map_err("interrupt read"))?;
Ok(Some(completion.data))
}
None => Ok(None),
}
}
pub fn vendor_control_in(
&self,
recipient: Recipient,
request: u8,
value: u16,
index: u16,
len: usize,
timeout: Duration,
) -> Result<Vec<u8>> {
let mut buf = vec![0u8; len];
let control = Control {
control_type: ControlType::Vendor,
recipient,
request,
value,
index,
};
let n = self
.interface
.control_in_blocking(control, &mut buf, timeout)
.map_err(map_err("vendor control read"))?;
buf.truncate(n);
Ok(buf)
}
}
impl Transport for UsbTransport {
async fn write(&mut self, buf: &[u8]) -> Result<()> {
let completion = self.interface.bulk_out(EP_OUT, buf.to_vec()).await;
completion.status.map_err(map_err("bulk write"))?;
if let Some(r) = self.record.as_mut() {
r.out(buf);
}
Ok(())
}
async fn read(&mut self, max: usize) -> Result<Vec<u8>> {
self.read_queue.submit(RequestBuffer::new(max));
let completion = self.read_queue.next_complete().await;
completion.status.map_err(map_err("bulk read"))?;
if let Some(r) = self.record.as_mut() {
r.r#in(&completion.data);
}
Ok(completion.data)
}
async fn write_timeout(&mut self, buf: &[u8], limit: Duration) -> Result<bool> {
match with_timeout(self.interface.bulk_out(EP_OUT, buf.to_vec()), limit).await {
Some(completion) => {
completion.status.map_err(map_err("bulk write"))?;
if let Some(r) = self.record.as_mut() {
r.out(buf);
}
Ok(true)
}
None => Ok(false),
}
}
async fn read_timeout(&mut self, max: usize, limit: Duration) -> Result<Option<Vec<u8>>> {
self.read_queue.submit(RequestBuffer::new(max));
if let Some(completion) = with_timeout(self.read_queue.next_complete(), limit).await {
completion.status.map_err(map_err("bulk read"))?;
if let Some(r) = self.record.as_mut() {
r.r#in(&completion.data);
}
return Ok(Some(completion.data));
}
self.read_queue.cancel_all();
match with_timeout(self.read_queue.next_complete(), REAP_LIMIT).await {
Some(_) => Ok(None),
None => Err(Error::Transport(
"read timed out and the transfer could not be cancelled; \
the connection is out of step and the instrument needs a power cycle"
.into(),
)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::{Error as IoError, ErrorKind};
#[test]
#[cfg_attr(not(target_os = "linux"), ignore = "the hint is Linux-only")]
fn permission_denied_names_the_udev_rule() {
let msg = open_error(IoError::from(ErrorKind::PermissionDenied)).to_string();
assert!(msg.contains("udev"), "{msg}");
assert!(msg.contains("0ffc"), "{msg}");
}
#[test]
fn other_failures_do_not_mention_udev() {
for kind in [ErrorKind::NotFound, ErrorKind::ResourceBusy] {
let msg = open_error(IoError::from(kind)).to_string();
assert!(!msg.contains("udev"), "{kind:?}: {msg}");
}
}
}