From 90268b04b09233bbd155452c4ba660c743ccfa3a Mon Sep 17 00:00:00 2001 From: Artem Egorkine Date: Thu, 25 Jul 2024 01:46:44 +0300 Subject: [PATCH 1/6] Implementing USB support * Start USB on app start (feature-specific); * Fix hotplug and device tracking, fix "usb init done" tracking; * Add registering USB devices to a global hash map; * Add listing USB devices in command-line options; --- .idea/dictionaries/arteme.xml | 1 + Cargo.lock | 17 ++- gui/src/main.rs | 18 ++- gui/src/opts.rs | 21 ++++ usb/Cargo.toml | 1 - usb/src/dev_handler.rs | 208 ++++++++++++++++++++++++++++++---- usb/src/devices.rs | 25 ++-- usb/src/endpoint.rs | 9 +- usb/src/event.rs | 8 +- usb/src/lib.rs | 101 ++++++++++++----- usb/src/util.rs | 4 + 11 files changed, 339 insertions(+), 74 deletions(-) create mode 100644 usb/src/util.rs diff --git a/.idea/dictionaries/arteme.xml b/.idea/dictionaries/arteme.xml index 7148b8c..043eb78 100644 --- a/.idea/dictionaries/arteme.xml +++ b/.idea/dictionaries/arteme.xml @@ -8,6 +8,7 @@ flanger flextone hotplug + iface midir objs pixbuf diff --git a/Cargo.lock b/Cargo.lock index 1529bc0..bc13bd9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -172,9 +172,9 @@ checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" [[package]] name = "bytes" -version = "1.3.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" +checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" [[package]] name = "cairo-rs" @@ -976,9 +976,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.139" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libusb1-sys" @@ -1446,7 +1446,6 @@ name = "pod-usb" version = "0.0.0" dependencies = [ "anyhow", - "arrayref", "log", "once_cell", "rusb", @@ -1501,18 +1500,18 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.50" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.23" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] diff --git a/gui/src/main.rs b/gui/src/main.rs index 26701e2..14e8ed4 100644 --- a/gui/src/main.rs +++ b/gui/src/main.rs @@ -575,8 +575,18 @@ fn start_usb() { ); } +#[cfg(feature = "usb")] +fn usb_list_devices() -> Vec { + pod_usb::usb_list_devices() +} + +#[cfg(not(feature = "usb"))] +fn start_usb() { +} + #[cfg(not(feature = "usb"))] -fn start_usb() -> Result<()> { +fn usb_list_devices() -> Vec { + vec![] } #[tokio::main] @@ -641,7 +651,11 @@ async fn main() -> Result<()> { } }); - app.run_with_args::(&[]); + let exit_code = app.run_with_args::(&[]); + // HACK: instead of dealing with USB thread clean-up (and in case there are any other + // threads still running), we just call `process::exit` and let libraries clean up + // after themselves + std::process::exit(exit_code); Ok(()) } diff --git a/gui/src/opts.rs b/gui/src/opts.rs index 0efebb1..14a042f 100644 --- a/gui/src/opts.rs +++ b/gui/src/opts.rs @@ -3,6 +3,7 @@ use anyhow::Result; use std::fmt::Write; use pod_core::config::configs; use pod_core::midi_io::{MidiIn, MidiOut, MidiPorts}; +use crate::usb_list_devices; #[derive(Parser, Clone)] pub struct Opts { @@ -31,6 +32,18 @@ pub struct Opts { /// This setting may not be relevant for all different devices supported. pub channel: Option, + /// Select the USB device to be connected as MIDI input/output. must be + /// an integer index of a recognized USB device present on this system. This + /// can also be an :
pair, such as "5:8". + /// When `-u` is provided, neither `-i` nor `-o` can be provided, or an error + /// will be reported. + #[cfg(feature = "usb")] + #[clap(short, long)] + pub usb: Option, + + #[cfg(not(feature = "usb"))] + pub usb: Option, + #[clap(short, long)] /// Select the model of the device. must be either an /// integer index of a supported device model or a string name @@ -66,5 +79,13 @@ pub fn generate_help_text() -> Result { } writeln!(s, "")?; + if cfg!(feature = "usb") { + writeln!(s, "USB devices (-u):")?; + for (i, n) in usb_list_devices().iter().enumerate() { + writeln!(s, "{}[{}] {}", tab, i, n)?; + } + writeln!(s, "")?; + } + Ok(s) } diff --git a/usb/Cargo.toml b/usb/Cargo.toml index db1e353..2526d25 100644 --- a/usb/Cargo.toml +++ b/usb/Cargo.toml @@ -12,4 +12,3 @@ log = "*" # defined in pod-core tokio = "*" # defined in pod-core anyhow = "*" once_cell = "*" # defined in pod-code -arrayref = "*" # defined in pod-code diff --git a/usb/src/dev_handler.rs b/usb/src/dev_handler.rs index 9bb4096..cddf5a9 100644 --- a/usb/src/dev_handler.rs +++ b/usb/src/dev_handler.rs @@ -1,19 +1,160 @@ use anyhow::*; use core::result::Result::Ok; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, Weak}; use std::time::Duration; use log::{error, info, trace}; -use rusb::{DeviceHandle, Direction, Error, UsbContext}; +use rusb::{DeviceHandle, Direction, Error, TransferType, UsbContext}; +use tokio::sync::mpsc; use crate::devices::UsbDevice; use crate::endpoint::{configure_endpoint, Endpoint, find_endpoint}; use crate::line6::line6_read_serial; +use crate::util::usb_address_string; + +pub struct Device { + pub name: String, + handle: Arc>, + read_ep: Endpoint, + write_ep: Endpoint, + inner: Weak> +} + +pub struct DeviceInner { + handle: Arc>, +} + +pub struct DeviceInput { + inner: Arc>, + rx: mpsc::UnboundedReceiver> +} + +pub struct DeviceOutput { + inner: Arc> +} pub struct DevHandler { - handle: Arc>>, + handle: Arc>, read_ep: Endpoint, - write_ep: Endpoint + write_ep: Endpoint, + tx: mpsc::UnboundedSender>, + rx: mpsc::UnboundedReceiver> +} + + +impl Device { + pub fn new(handle: DeviceHandle, usb_dev: &UsbDevice) -> Result { + let serial = line6_read_serial(&handle).ok() + .map(|s| format!(" {}", s)) + .unwrap_or("".to_string()); + + let address = usb_address_string(handle.device().bus_number(), handle.device().address()); + let name = format!("{}{} [{}]", &usb_dev.name, serial, address); + info!("Found: {}", name); + + let desc = handle.device().device_descriptor()?; + + // TODO: replace with .expect? + let Some(read_ep) = find_endpoint(&mut handle.device(), &desc, Direction::In, usb_dev.transfer_type, usb_dev.read_ep, usb_dev.alt_setting) else { + bail!("Read end-point not found") + }; + let Some(write_ep) = find_endpoint(&mut handle.device(), &desc, Direction::Out, usb_dev.transfer_type, usb_dev.write_ep, usb_dev.alt_setting) else { + bail!("Write end-point not found") + }; + + Ok(Device { + name, + handle: Arc::new(handle), + read_ep, + write_ep, + inner: Weak::new() + }) + } + + pub fn open(&mut self) -> Result<(DeviceInput, DeviceOutput)> { + if self.inner.upgrade().is_some() { + bail!("Devide already open") + } + + let inner = Arc::new(DeviceInner { + handle: self.handle.clone() + }); + self.inner = Arc::downgrade(&inner); + + let (tx, rx) = mpsc::unbounded_channel(); + let input = DeviceInput { + inner: inner.clone(), + rx + }; + + let output = DeviceOutput { + inner: inner.clone() + }; + + Ok((input, output)) + } +} + +impl DeviceInner { + fn new(handle: Arc>, read_ep: Endpoint, tx: mpsc::UnboundedSender>) -> Self { + let handle_ret = handle.clone(); + let has_kernel_driver = match handle.kernel_driver_active(read_ep.iface) { + Ok(true) => { + handle.detach_kernel_driver(read_ep.iface).ok(); + true + } + _ => false + }; + + configure_endpoint(&handle, &read_ep).ok(); + + tokio::spawn(async move { + let mut buf = [0u8; 1024]; + loop { + let res = match read_ep.transfer_type { + TransferType::Bulk => { + handle.read_bulk(read_ep.address, &mut buf, Duration::MAX) + } + TransferType::Interrupt => { + handle.read_interrupt(read_ep.address, &mut buf, Duration::MAX) + } + tt => { + error!("Transfer type {:?} not supported!", tt); + break; + } + + }; + match res { + Ok(len) => { + let b = buf.chunks(len).next().unwrap(); + trace!("<< {:02x?} len={}", &b, len); + tx.send(b.to_vec()).ok(); + } + Err(e) => { + error!("USB read failed: {}", e); + match e { + Error::Busy | Error::Timeout | Error::Overflow => { continue } + _ => { break } + } + } + } + } + }); + + DeviceInner { + handle: handle_ret + } + } } +impl Drop for DeviceInner { + fn drop(&mut self) { + // TODO: we consider that there is ever only one device, so interrupting + // the handle_events will only affect one DeviceInner... Can do better + // using own explicit context for each DeviceInner + self.handle.context().interrupt_handle_events(); + } +} + + impl DevHandler { pub fn new(handle: DeviceHandle, usb_dev: &UsbDevice) -> Result { let serial = line6_read_serial(&handle).ok() @@ -21,7 +162,7 @@ impl DevHandler { .unwrap_or("".to_string()); let name = format!( - "{}{} [usb:{:#04x}:{:#04x}]", + "{}{} [usb:{}:{}]", &usb_dev.name, serial, handle.device().bus_number(), handle.device().address() ); @@ -29,42 +170,61 @@ impl DevHandler { let desc = handle.device().device_descriptor()?; - let Some(read_ep) = find_endpoint(&mut handle.device(), &desc, Direction::In, usb_dev.read_ep, usb_dev.alt_setting) else { + // TODO: replace with .expect? + let Some(read_ep) = find_endpoint(&mut handle.device(), &desc, Direction::In, usb_dev.transfer_type, usb_dev.read_ep, usb_dev.alt_setting) else { bail!("Read end-point not found") }; - let Some(write_ep) = find_endpoint(&mut handle.device(), &desc, Direction::Out, usb_dev.write_ep, usb_dev.alt_setting) else { + let Some(write_ep) = find_endpoint(&mut handle.device(), &desc, Direction::Out, usb_dev.transfer_type, usb_dev.write_ep, usb_dev.alt_setting) else { bail!("Write end-point not found") }; + let (tx, rx) = mpsc::unbounded_channel(); + Ok(DevHandler { - handle: Arc::new(Mutex::new(handle)), + handle: Arc::new(handle), read_ep, - write_ep + write_ep, + tx, + rx }) } pub fn start(&mut self) { - let handle = self.handle.clone(); + let mut handle = self.handle.clone(); let read_ep = self.read_ep.clone(); + let tx = self.tx.clone(); - tokio::spawn(async move { - let mut handle = handle.lock().unwrap(); + let has_kernel_driver = match handle.kernel_driver_active(read_ep.iface) { + Ok(true) => { + handle.detach_kernel_driver(read_ep.iface).ok(); + true + } + _ => false + }; - let has_kernel_driver = match handle.kernel_driver_active(read_ep.iface) { - Ok(true) => { - handle.detach_kernel_driver(read_ep.iface).ok(); - true - } - _ => false - }; + configure_endpoint(&handle, &read_ep).ok(); - configure_endpoint(&mut handle, &read_ep).ok(); + tokio::spawn(async move { let mut buf = [0u8; 1024]; loop { - match handle.read_interrupt(read_ep.address, &mut buf, Duration::MAX) { + let res = match read_ep.transfer_type { + TransferType::Bulk => { + handle.read_bulk(read_ep.address, &mut buf, Duration::MAX) + } + TransferType::Interrupt => { + handle.read_interrupt(read_ep.address, &mut buf, Duration::MAX) + } + tt => { + error!("Transfer type {:?} not supported!", tt); + break; + } + + }; + match res { Ok(len) => { let b = buf.chunks(len).next().unwrap(); trace!("<< {:02x?} len={}", &b, len); + tx.send(b.to_vec()).ok(); } Err(e) => { error!("USB read failed: {}", e); @@ -78,4 +238,10 @@ impl DevHandler { }); } + pub fn stop(&mut self) { + // TODO: we consider that there is ever only one device + self.handle.context().interrupt_handle_events(); + + } + } \ No newline at end of file diff --git a/usb/src/devices.rs b/usb/src/devices.rs index e39f375..424d4d5 100644 --- a/usb/src/devices.rs +++ b/usb/src/devices.rs @@ -1,4 +1,5 @@ use once_cell::sync::Lazy; +use rusb::TransferType; pub enum UsbId { Device { vid: u16, pid: u16 }, @@ -40,7 +41,8 @@ pub struct UsbDevice { pub name: String, pub alt_setting: u8, pub read_ep: u8, - pub write_ep: u8 + pub write_ep: u8, + pub transfer_type: TransferType, } // based on: https://github.com/torvalds/linux/blob/8508fa2e7472f673edbeedf1b1d2b7a6bb898ecc/sound/usb/line6/pod.c @@ -48,31 +50,38 @@ static USB_DEVICES: Lazy> = Lazy::new(|| { vec![ UsbDevice { id: id!(0x0e41, 0x5044), name: "POD XT".into(), - alt_setting: 5, read_ep: 0x84, write_ep: 0x03 + alt_setting: 5, read_ep: 0x84, write_ep: 0x03, + transfer_type: TransferType::Interrupt }, UsbDevice { id: id!(0x0e41, 0x5050), name: "POD XT Pro".into(), - alt_setting: 5, read_ep: 0x84, write_ep: 0x03 + alt_setting: 5, read_ep: 0x84, write_ep: 0x03, + transfer_type: TransferType::Interrupt }, UsbDevice { id: id!(0x0e41, 0x4650, 0), name: "POD XT Live".into(), - alt_setting: 1, read_ep: 0x84, write_ep: 0x03 + alt_setting: 1, read_ep: 0x84, write_ep: 0x03, + transfer_type: TransferType::Interrupt }, UsbDevice { id: id!(0x0e41, 0x4250), name: "Bass POD XT".into(), - alt_setting: 5, read_ep: 0x84, write_ep: 0x03 + alt_setting: 5, read_ep: 0x84, write_ep: 0x03, + transfer_type: TransferType::Interrupt }, UsbDevice { id: id!(0x0e41, 0x4252), name: "Bass POD XT Pro".into(), - alt_setting: 5, read_ep: 0x84, write_ep: 0x03 + alt_setting: 5, read_ep: 0x84, write_ep: 0x03, + transfer_type: TransferType::Interrupt }, UsbDevice { id: id!(0x0e41, 0x4642), name: "Bass POD XT Live".into(), - alt_setting: 1, read_ep: 0x84, write_ep: 0x03 + alt_setting: 1, read_ep: 0x84, write_ep: 0x03, + transfer_type: TransferType::Interrupt }, UsbDevice { id: id!(0x0e41, 0x5051, 1), name: "Pocket POD".into(), - alt_setting: 1, read_ep: 0x82, write_ep: 0x02 + alt_setting: 0, read_ep: 0x82, write_ep: 0x02, + transfer_type: TransferType::Bulk }, ] }); diff --git a/usb/src/endpoint.rs b/usb/src/endpoint.rs index 9d946ed..dc4dc3f 100644 --- a/usb/src/endpoint.rs +++ b/usb/src/endpoint.rs @@ -10,10 +10,11 @@ pub struct Endpoint { pub iface: u8, pub setting: u8, pub address: u8, + pub transfer_type: TransferType } pub fn configure_endpoint( - handle: &mut DeviceHandle, + handle: &DeviceHandle, endpoint: &Endpoint, ) -> Result<()> { handle.set_active_configuration(endpoint.config)?; @@ -25,9 +26,10 @@ pub fn configure_endpoint( pub fn find_endpoint( - device: &mut Device, + device: &Device, device_desc: &DeviceDescriptor, direction: Direction, + transfer_type: TransferType, address: u8, setting: u8, ) -> Option { @@ -40,7 +42,7 @@ pub fn find_endpoint( for endpoint_desc in interface_desc.endpoint_descriptors() { if endpoint_desc.direction() == direction && - endpoint_desc.transfer_type() == TransferType::Interrupt && + endpoint_desc.transfer_type() == transfer_type && endpoint_desc.address() == address { return Some(Endpoint { @@ -48,6 +50,7 @@ pub fn find_endpoint( iface: interface_desc.interface_number(), setting: interface_desc.setting_number(), address: endpoint_desc.address(), + transfer_type: endpoint_desc.transfer_type(), }); } } diff --git a/usb/src/event.rs b/usb/src/event.rs index 205f09b..41ae680 100644 --- a/usb/src/event.rs +++ b/usb/src/event.rs @@ -1,13 +1,17 @@ #[derive(Clone, Debug)] pub struct DeviceAddedEvent { pub vid: u16, - pub pid: u16 + pub pid: u16, + pub bus: u8, + pub address: u8, } #[derive(Clone, Debug)] pub struct DeviceRemovedEvent { pub vid: u16, - pub pid: u16 + pub pid: u16, + pub bus: u8, + pub address: u8, } #[derive(Clone, Debug)] diff --git a/usb/src/lib.rs b/usb/src/lib.rs index 8975c35..1f55a8b 100644 --- a/usb/src/lib.rs +++ b/usb/src/lib.rs @@ -3,58 +3,74 @@ mod devices; mod line6; mod dev_handler; mod endpoint; +mod util; use log::{debug, error, info}; use anyhow::*; use core::result::Result::Ok; -use std::sync::Arc; +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; use std::sync::atomic::{AtomicBool, Ordering}; use once_cell::sync::Lazy; -use rusb::{Context, Device, Hotplug, HotplugBuilder, UsbContext}; +use rusb::{Context, Device as UsbDevice, GlobalContext, Hotplug, HotplugBuilder, UsbContext}; use tokio::sync::{broadcast, Notify}; use tokio::sync::broadcast::error::RecvError; -use crate::dev_handler::DevHandler; +use crate::dev_handler::Device; use crate::devices::find_device; use crate::event::*; +use crate::util::usb_address_string; struct HotplugHandler { event_tx: broadcast::Sender, - num_devices: Option + init_devices: Option +} + +impl HotplugHandler { + /// Notify the hotplug handler that `num` devices have been initialized. + /// This is used for `UsbEvent::InitDone` event tracking. + fn device_init_notify(&mut self, added: isize) { + if let Some(mut num) = self.init_devices.take() { + num -= added; + self.init_devices = if num > 1 { + Some(num) + } else { + self.event_tx.send(UsbEvent::InitDone).unwrap(); + None + }; + } + } } impl Hotplug for HotplugHandler { - fn device_arrived(&mut self, device: Device) { + fn device_arrived(&mut self, device: UsbDevice) { let Ok(desc) = device.device_descriptor() else { return }; + debug!("device added: {:?} ??", device); if find_device(desc.vendor_id(), desc.product_id()).is_some() { debug!("device added: {:?}", device); let e = DeviceAddedEvent { vid: desc.vendor_id(), - pid: desc.product_id() + pid: desc.product_id(), + bus: device.bus_number(), + address: device.address(), }; self.event_tx.send(UsbEvent::DeviceAdded(e)).unwrap(); } - if let Some(mut num) = self.num_devices.take() { - num -= 1; - self.num_devices = if num > 1 { - Some(num) - } else { - self.event_tx.send(UsbEvent::InitDone).unwrap(); - None - }; - } + self.device_init_notify(1); } - fn device_left(&mut self, device: Device) { + fn device_left(&mut self, device: UsbDevice) { let Ok(desc) = device.device_descriptor() else { return }; if find_device(desc.vendor_id(), desc.product_id()).is_some() { debug!("device removed: {:?}", device); let e = DeviceRemovedEvent { vid: desc.vendor_id(), - pid: desc.product_id() + pid: desc.product_id(), + bus: device.bus_number(), + address: device.address(), }; self.event_tx.send(UsbEvent::DeviceRemoved(e)).unwrap(); } @@ -65,6 +81,9 @@ static mut INIT_DONE: AtomicBool = AtomicBool::new(false); static INIT_DONE_NOTIFY: Lazy> = Lazy::new(|| { Arc::new(Notify::new()) }); +static DEVICES: Lazy>>>> = Lazy::new(|| { + Arc::new(Mutex::new(HashMap::new())) +}); pub fn usb_start() -> Result<()> { if !rusb::has_hotplug() { @@ -74,16 +93,18 @@ pub fn usb_start() -> Result<()> { let (event_tx, mut event_rx) = broadcast::channel::(512); let ctx = Context::new()?; - let hh = HotplugHandler { + let num_devices = ctx.devices()?.len() as isize; + let mut hh = HotplugHandler { event_tx: event_tx.clone(), - num_devices: Some(ctx.devices()?.len() as isize) + init_devices: Some(num_devices) }; + hh.device_init_notify(0); let hotplug = HotplugBuilder::new() .enumerate(true) .register(&ctx, Box::new(hh))?; tokio::spawn(async move { - info!("Starting USB hotplug"); + info!("USB hotplug thread start"); let mut reg = Some(hotplug); loop { ctx.handle_events(None).unwrap(); @@ -91,11 +112,13 @@ pub fn usb_start() -> Result<()> { ctx.unregister_callback(reg); } } + info!("USB hotplug thread finish"); }); - let ctx = Context::new()?; + let devices = DEVICES.clone(); tokio::spawn(async move { + info!("USB message RX thread start"); loop { let msg = match event_rx.recv().await { Ok(msg) => { msg } @@ -110,26 +133,31 @@ pub fn usb_start() -> Result<()> { }; match msg { - UsbEvent::DeviceAdded(DeviceAddedEvent{ vid, pid }) => { + UsbEvent::DeviceAdded(DeviceAddedEvent{ vid, pid, bus, address }) => { let usb_dev = find_device(vid, pid).unwrap(); //let Some(h) = rusb::open_device_with_vid_pid(vid, pid) else { continue }; let Some(h) = rusb::open_device_with_vid_pid(vid, pid) else { continue }; - let mut handler = match DevHandler::new(h, usb_dev) { + let handler = match Device::new(h, usb_dev) { Ok(h) => { h } Err(e) => { error!("Filed to initialize device {:?}: {}", usb_dev.name, e); continue } }; - handler.start(); + let address = usb_address_string(bus, address); + usb_add_device(address, handler); + } + UsbEvent::DeviceRemoved(DeviceRemovedEvent{ bus, address, .. }) => { + let address = usb_address_string(bus, address); + usb_remove_device(address); + } - UsbEvent::DeviceRemoved(_) => {} UsbEvent::InitDone => { - debug!("USB init done"); usb_init_set_done(); } } } + info!("USB message RX thread finish"); }); Ok(()) @@ -138,6 +166,7 @@ pub fn usb_start() -> Result<()> { fn usb_init_set_done() { unsafe { INIT_DONE.store(true, Ordering::Relaxed) } + debug!("USB init done"); INIT_DONE_NOTIFY.notify_waiters() } @@ -145,11 +174,27 @@ fn usb_init_done() -> bool { unsafe { INIT_DONE.load(Ordering::Relaxed) } } -pub async fn usb_init_wait() -> () { +pub async fn usb_init_wait() { if usb_init_done() { return; } - INIT_DONE_NOTIFY.notified().await + debug!("Waiting for USB init..."); + INIT_DONE_NOTIFY.notified().await; + debug!("Waiting for USB init over"); +} + +pub fn usb_list_devices() -> Vec { + let devices = DEVICES.lock().unwrap(); + devices.values().map(|i| i.name.clone()).collect() } +fn usb_add_device(key: String, device: Device) { + let mut devices = DEVICES.lock().unwrap(); + devices.insert(key, device); +} + +fn usb_remove_device(key: String) { + let mut devices = DEVICES.lock().unwrap(); + devices.remove(&key); +} diff --git a/usb/src/util.rs b/usb/src/util.rs new file mode 100644 index 0000000..0e8a1f0 --- /dev/null +++ b/usb/src/util.rs @@ -0,0 +1,4 @@ + +pub fn usb_address_string(bus: u8, address: u8) -> String { + format!("usb:{}:{}", bus, address) +} \ No newline at end of file From 12f36bd9e1b60f0199df082ca5d2ea4e0e01cb0a Mon Sep 17 00:00:00 2001 From: Artem Egorkine Date: Sun, 28 Jul 2024 22:47:45 +0300 Subject: [PATCH 2/6] Implement proper USB I/O and support for it in MIDI threads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Updated tokio, async-strem, unicycle deps to latest versions; * Split `MidiIn` and `MidiOut` into MIDI-port handling `MidiInPort`/`MidiOutPort` and MIDI I/O traits `MidiIn`/`MidiOut` that are impemented for `MidiInPort`/`MidiOutPort`; * Added boxed traits for convenience or boxing `dyn MidiIn`/`dyn MidiOut`: `BoxedMidiIn`/`BoxedMidiOut`; * All code that previously used `MidiIn`/`MidiOut` now uses boxed `BoxedMidiIn`/`BoxedMidiOut`; * Added `-u` command-line option for selecting USB devices, adjusted start-up (autodetect logic) for when `-u` option is given; * USB: implement USB decive lookup dy id/address (:
); * USB: Reåplace `DevHandler` with separate in/out objects `DeviceInput`/`DeviceOutput` for which also `MidiIn`/`MidiOut` are implemented; * USB: fix tokio runtime getting stuck on executing long blocking tasks by switching such mode from `tokio::spawn` to `tokio::task::spawn_blocking`; --- Cargo.lock | 265 ++++++++++++++++++++++++++++++----------- core/Cargo.toml | 7 +- core/src/midi_io.rs | 130 +++++++++++++------- gui/src/autodetect.rs | 47 +++++--- gui/src/main.rs | 24 +++- gui/src/opts.rs | 6 +- gui/src/settings.rs | 20 ++-- usb/Cargo.toml | 4 + usb/src/dev_handler.rs | 156 ++++++++++-------------- usb/src/lib.rs | 31 ++++- usb/src/util.rs | 2 +- 11 files changed, 448 insertions(+), 244 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bc13bd9..c57bd59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,23 +65,35 @@ checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" [[package]] name = "async-stream" -version = "0.3.3" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dad5c83079eae9969be7fadefe640a1c566901f05ff91ab221de4b6f68d9507e" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" dependencies = [ "async-stream-impl", "futures-core", + "pin-project-lite", ] [[package]] name = "async-stream-impl" -version = "0.3.3" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.72", +] + +[[package]] +name = "async-trait" +version = "0.1.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", ] [[package]] @@ -232,7 +244,7 @@ dependencies = [ "bitflags", "clap_derive", "clap_lex", - "indexmap", + "indexmap 1.9.2", "once_cell", "strsim", "termcolor", @@ -250,7 +262,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -377,6 +389,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "fastrand" version = "1.8.0" @@ -465,9 +483,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.25" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" @@ -494,7 +512,7 @@ checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -655,7 +673,7 @@ dependencies = [ "proc-macro-hack", "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -692,7 +710,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -768,14 +786,14 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] name = "h2" -version = "0.3.15" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ "bytes", "fnv", @@ -783,7 +801,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap", + "indexmap 2.2.6", "slab", "tokio", "tokio-util", @@ -796,6 +814,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "heck" version = "0.4.0" @@ -820,6 +844,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + [[package]] name = "hex" version = "0.4.3" @@ -849,9 +879,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.8" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ "bytes", "fnv", @@ -883,9 +913,9 @@ checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" [[package]] name = "hyper" -version = "0.14.23" +version = "0.14.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c" +checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" dependencies = [ "bytes", "futures-channel", @@ -935,7 +965,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown 0.14.5", ] [[package]] @@ -1080,14 +1120,14 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.5" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" +checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" dependencies = [ + "hermit-abi 0.3.9", "libc", - "log", "wasi", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -1194,7 +1234,7 @@ checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -1279,7 +1319,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-sys", + "windows-sys 0.42.0", ] [[package]] @@ -1300,9 +1340,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -1323,6 +1363,7 @@ dependencies = [ "anyhow", "arrayref", "async-stream", + "async-trait", "bitflags", "futures", "futures-util", @@ -1446,8 +1487,11 @@ name = "pod-usb" version = "0.0.0" dependencies = [ "anyhow", + "async-trait", "log", "once_cell", + "pod-core", + "regex", "rusb", "tokio", ] @@ -1477,7 +1521,7 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", - "syn", + "syn 1.0.107", "version_check", ] @@ -1692,7 +1736,7 @@ version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" dependencies = [ - "windows-sys", + "windows-sys 0.42.0", ] [[package]] @@ -1860,7 +1904,7 @@ checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -1896,7 +1940,7 @@ dependencies = [ "colored", "log", "time", - "windows-sys", + "windows-sys 0.42.0", ] [[package]] @@ -1916,12 +1960,12 @@ checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" [[package]] name = "socket2" -version = "0.4.7" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -1956,6 +2000,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "system-deps" version = "6.0.3" @@ -2028,7 +2083,7 @@ checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -2077,31 +2132,29 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.24.2" +version = "1.39.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a12a59981d9e3c38d216785b0c37399f6e415e8d0712047620f189371b0bb" +checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" dependencies = [ - "autocfg", + "backtrace", "bytes", "libc", - "memchr", "mio", - "num_cpus", "pin-project-lite", "socket2", "tokio-macros", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" -version = "1.8.2" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.72", ] [[package]] @@ -2116,16 +2169,15 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.4" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", - "tracing", ] [[package]] @@ -2149,7 +2201,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "729bfd096e40da9c001f778f5cdecbd2957929a24e10e5883d9392220a751581" dependencies = [ - "indexmap", + "indexmap 1.9.2", "nom8", "toml_datetime", ] @@ -2224,9 +2276,9 @@ dependencies = [ [[package]] name = "unicycle" -version = "0.8.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93c11786bcd90b4e0a60e0e2556104277aaea3d47e7e68707535df4c2d3a7288" +checksum = "6ca7c60c63c67acf573ef612b410c42b351c1028216085fd72fb43e2b1abd2fc" dependencies = [ "futures-core", "lock_api", @@ -2236,9 +2288,9 @@ dependencies = [ [[package]] name = "uniset" -version = "0.2.1" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "609bc97e6b3b49efba40490d2e81d33492ef06a4b57adfab6e809895f0bb694f" +checksum = "40789245bbff5f31eb773c9ac4ee5c4e15eab9640d975e124d6ce4c34a6410d7" [[package]] name = "ureq" @@ -2330,7 +2382,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 1.0.107", "wasm-bindgen-shared", ] @@ -2364,7 +2416,7 @@ checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2422,13 +2474,13 @@ version = "0.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04662ed0e3e5630dfa9b26e4cb823b817f1a9addda855d973a9458c236556244" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.42.1", + "windows_aarch64_msvc 0.42.1", + "windows_i686_gnu 0.42.1", + "windows_i686_msvc 0.42.1", + "windows_x86_64_gnu 0.42.1", + "windows_x86_64_gnullvm 0.42.1", + "windows_x86_64_msvc 0.42.1", ] [[package]] @@ -2437,13 +2489,38 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.42.1", + "windows_aarch64_msvc 0.42.1", + "windows_i686_gnu 0.42.1", + "windows_i686_msvc 0.42.1", + "windows_x86_64_gnu 0.42.1", + "windows_x86_64_gnullvm 0.42.1", + "windows_x86_64_msvc 0.42.1", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -2452,42 +2529,90 @@ version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + [[package]] name = "windows_aarch64_msvc" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + [[package]] name = "windows_i686_gnu" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + [[package]] name = "windows_i686_msvc" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + [[package]] name = "windows_x86_64_gnu" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + [[package]] name = "windows_x86_64_msvc" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "winreg" version = "0.10.1" diff --git a/core/Cargo.toml b/core/Cargo.toml index 62df2cf..7a5b2a2 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -6,17 +6,18 @@ edition.workspace = true rust-version.workspace = true [dependencies] +async-trait = "0.1.81" bitflags = "1.3.2" once_cell = "1.14.0" midir = "0.9.0" log = "0.4.8" anyhow = { version = "1.0", features = ["backtrace"] } arrayref = "0.3.6" -tokio = { version = "1.15.0", features = ["sync", "macros", "rt", "rt-multi-thread", "time"] } -async-stream = "0.3.2" +tokio = { version = "1.39.2", features = ["sync", "macros", "rt", "rt-multi-thread", "time"] } +async-stream = "0.3.5" regex = "1.6.0" strfmt = "0.1.6" futures = "0.3.19" futures-util = "0.3.19" result = "1.0.0" -unicycle = { version = "0.8.0", features = ['futures-rs'] } \ No newline at end of file +unicycle = { version = "0.10.1", features = ['futures-rs'] } \ No newline at end of file diff --git a/core/src/midi_io.rs b/core/src/midi_io.rs index 84fc71e..b761a1d 100644 --- a/core/src/midi_io.rs +++ b/core/src/midi_io.rs @@ -4,6 +4,7 @@ use regex::Regex; use std::str::FromStr; use std::time::Duration; use async_stream::stream; +use async_trait::async_trait; use futures_util::StreamExt; use tokio::time::sleep; use log::*; @@ -15,13 +16,38 @@ use crate::model::Config; use tokio::sync::mpsc; use unicycle::IndexedStreamsUnordered; -pub struct MidiIn { - pub name: String, +#[async_trait] +pub trait MidiIn { + fn name(&self) -> String; + async fn recv(&mut self) -> Option>; + fn close(&mut self); +} + +#[async_trait] +pub trait MidiOut { + fn name(&self) -> String; + fn send(&mut self, bytes: &[u8]) -> Result<()>; + fn close(&mut self); +} + +pub type BoxedMidiIn = Box; +pub type BoxedMidiOut = Box; + +pub fn box_midi_in(x: T) -> BoxedMidiIn { + Box::new(x) +} + +pub fn box_midi_out(x: T) -> BoxedMidiOut { + Box::new(x) +} + +pub struct MidiInPort { + name: String, conn: Option>, rx: mpsc::UnboundedReceiver> } -impl MidiIn { +impl MidiInPort { fn _new() -> Result { let mut midi_in = MidiInput::new("pod midi in")?; midi_in.ignore(Ignore::None); @@ -46,19 +72,24 @@ impl MidiIn { .unwrap_or_else(|_| { error!("midi input ({}): failed to send data to the application", n); }); - }, ()) .map_err(|e| anyhow!("Midi connection error: {:?}", e))?; - Ok(MidiIn { name, conn: Some(conn), rx }) + Ok(MidiInPort { name, conn: Some(conn), rx }) } +} - pub async fn recv(&mut self) -> Option> - { +#[async_trait] +impl MidiIn for MidiInPort { + fn name(&self) -> String { + self.name.clone() + } + + async fn recv(&mut self) -> Option> { self.rx.recv().await } - pub fn close(&mut self) { + fn close(&mut self) { self.conn.take().map(|conn| { debug!("closing in"); conn.close(); @@ -68,19 +99,19 @@ impl MidiIn { } } -impl Drop for MidiIn { +impl Drop for MidiInPort { fn drop(&mut self) { self.close(); } } -pub struct MidiOut { - pub name: String, +pub struct MidiOutPort { + name: String, conn: Option, } -impl MidiOut { +impl MidiOutPort { fn _new() -> Result { let midi_out = MidiOutput::new("pod midi out")?; @@ -97,10 +128,17 @@ impl MidiOut { let conn = midi_out.connect(&port, "pod midi out conn") .map_err(|e| anyhow!("Midi connection error: {:?}", e))?; - Ok(MidiOut { name, conn: Some(conn) }) + Ok(MidiOutPort { name, conn: Some(conn) }) + } +} + +#[async_trait] +impl MidiOut for MidiOutPort { + fn name(&self) -> String { + self.name.clone() } - pub fn send(&mut self, bytes: &[u8]) -> Result<()> { + fn send(&mut self, bytes: &[u8]) -> Result<()> { trace!(">> {:02x?} len={}", bytes, bytes.len()); if let Some(conn) = self.conn.as_mut() { conn.send(bytes) @@ -110,7 +148,7 @@ impl MidiOut { } } - pub fn close(&mut self) { + fn close(&mut self) { self.conn.take().map(|conn| { debug!("closing out"); conn.close(); @@ -119,7 +157,7 @@ impl MidiOut { } } -impl Drop for MidiOut { +impl Drop for MidiOutPort { fn drop(&mut self) { self.close() } @@ -192,33 +230,33 @@ pub trait MidiOpen { } } -impl MidiOpen for MidiIn { +impl MidiOpen for MidiInPort { type Class = MidiInput; type Port = MidiInputPort; - type Out = MidiIn; + type Out = MidiInPort; const DIR: &'static str = "input"; fn _new() -> Result { - MidiIn::_new() + MidiInPort::_new() } fn _new_for_port(class: Self::Class, port: Self::Port) -> Result { - MidiIn::_new_for_port(class, port) + MidiInPort::_new_for_port(class, port) } } -impl MidiOpen for MidiOut { +impl MidiOpen for MidiOutPort { type Class = MidiOutput; type Port = MidiOutputPort; - type Out = MidiOut; + type Out = MidiOutPort; const DIR: &'static str = "output"; fn _new() -> Result { - MidiOut::_new() + MidiOutPort::_new() } fn _new_for_port(class: Self::Class, port: Self::Port) -> Result { - MidiOut::_new_for_port(class, port) + MidiOutPort::_new_for_port(class, port) } } @@ -228,9 +266,9 @@ pub trait MidiPorts { fn ports() -> Result>; } -impl MidiPorts for MidiIn { +impl MidiPorts for MidiInPort { fn all_ports() -> Result> { - let midi = MidiIn::_new()?; + let midi = MidiInPort::_new()?; list_ports(midi) } @@ -243,9 +281,9 @@ impl MidiPorts for MidiIn { } } -impl MidiPorts for MidiOut { +impl MidiPorts for MidiOutPort { fn all_ports() -> Result> { - let midi = MidiOut::_new()?; + let midi = MidiOutPort::_new()?; list_ports(midi) } @@ -268,13 +306,13 @@ fn list_ports(midi: T) -> Result> { const DETECT_DELAY: Duration = Duration::from_millis(1000); -async fn detect(in_ports: &mut [MidiIn], out_ports: &mut [MidiOut]) -> Result<(Vec<(usize, &'static Config)>, Option)> { +async fn detect(in_ports: &mut [BoxedMidiIn], out_ports: &mut [BoxedMidiOut]) -> Result<(Vec<(usize, &'static Config)>, Option)> { detect_with_channel(in_ports, out_ports, Channel::all()).await } -async fn detect_with_channel(in_ports: &mut [MidiIn], out_ports: &mut [MidiOut], channel: u8) -> Result<(Vec<(usize, &'static Config)>, Option)> { +async fn detect_with_channel(in_ports: &mut [BoxedMidiIn], out_ports: &mut [BoxedMidiOut], channel: u8) -> Result<(Vec<(usize, &'static Config)>, Option)> { - let in_names = in_ports.iter().map(|p| p.name.clone()).collect::>(); + let in_names = in_ports.iter().map(|p| p.name()).collect::>(); let udi = MidiMessage::UniversalDeviceInquiry { channel }.to_bytes(); let mut streams = IndexedStreamsUnordered::new(); @@ -327,7 +365,7 @@ async fn detect_with_channel(in_ports: &mut [MidiIn], out_ports: &mut [MidiOut], Ok((replied_midi_in, error)) } -async fn detect_channel(in_port: &mut MidiIn, out_port: &mut MidiOut) -> Result> { +async fn detect_channel(in_port: &mut BoxedMidiIn, out_port: &mut BoxedMidiOut) -> Result> { let udi = (0u8..=15).into_iter().map(|n| { MidiMessage::UniversalDeviceInquiry { channel: Channel::num(n) }.to_bytes() @@ -379,30 +417,32 @@ async fn detect_channel(in_port: &mut MidiIn, out_port: &mut MidiOut) -> Result< Ok(channel) } -pub async fn autodetect(channel: Option) -> Result<(MidiIn, MidiOut, u8, &'static Config)> { +pub async fn autodetect(channel: Option) -> Result<(BoxedMidiIn, BoxedMidiOut, u8, &'static Config)> { - let in_port_names = MidiIn::ports()?; + let in_port_names = MidiInPort::ports()?; let mut in_port_errors = vec![]; let in_ports = in_port_names.iter().enumerate() .flat_map(|(i, name)| { - MidiIn::new(Some(i)).map_err(|e| { + MidiInPort::new(Some(i)).map_err(|e| { let error = format!("Failed to open MIDI in port {:?}: {}", name, e); warn!("{}", error); in_port_errors.push(error); }).ok() }) + .map(box_midi_in) .collect::>(); - let out_port_names = MidiOut::ports()?; + let out_port_names = MidiOutPort::ports()?; let mut out_port_errors = vec![]; - let out_ports = out_port_names.iter().enumerate() + let out_ports: Vec = out_port_names.iter().enumerate() .flat_map(|(i, name)| { - MidiOut::new(Some(i)).map_err(|e| { + MidiOutPort::new(Some(i)).map_err(|e| { let error = format!("Failed to open MIDI out port {:?}: {}", name, e); warn!("{}", error); out_port_errors.push(error); }).ok() }) + .map(box_midi_out) .collect::>(); if in_ports.len() < 1 { @@ -423,8 +463,8 @@ pub async fn autodetect(channel: Option) -> Result<(MidiIn, MidiOut, u8, &'s autodetect_with_ports(in_ports, out_ports, channel).await } -pub async fn autodetect_with_ports(in_ports: Vec, out_ports: Vec, - channel: Option) -> Result<(MidiIn, MidiOut, u8, &'static Config)> { +pub async fn autodetect_with_ports(in_ports: Vec, out_ports: Vec, + channel: Option) -> Result<(BoxedMidiIn, BoxedMidiOut, u8, &'static Config)> { let config: Option<&Config>; let mut in_ports = in_ports.into_iter().collect::>(); let mut out_ports = out_ports.into_iter().collect::>(); @@ -510,11 +550,11 @@ pub async fn autodetect_with_ports(in_ports: Vec, out_ports: Vec Result<(MidiIn, MidiOut, u8)> { - let in_port = MidiIn::new_for_name(in_name)?; - let out_port = MidiOut::new_for_name(out_name)?; - let mut in_ports = vec![in_port]; - let mut out_ports = vec![out_port]; +pub async fn test(in_name: &str, out_name: &str, channel: u8, config: &Config) -> Result<(BoxedMidiIn, BoxedMidiOut, u8)> { + let in_port = MidiInPort::new_for_name(in_name)?; + let out_port = MidiOutPort::new_for_name(out_name)?; + let mut in_ports = vec![box_midi_in(in_port)]; + let mut out_ports = vec![box_midi_out(out_port)]; let (rep, error) = detect_with_channel( in_ports.as_mut_slice(), out_ports.as_mut_slice(), channel diff --git a/gui/src/autodetect.rs b/gui/src/autodetect.rs index 771e6b4..3f8347f 100644 --- a/gui/src/autodetect.rs +++ b/gui/src/autodetect.rs @@ -8,7 +8,7 @@ use pod_core::midi_io::*; use pod_core::model::Config; use pod_gtk::prelude::*; use crate::opts::Opts; -use crate::{set_midi_in_out, State}; +use crate::{set_midi_in_out, State, usb_open}; fn config_for_str(config_str: &str) -> Result<&'static Config> { use std::str::FromStr; @@ -38,31 +38,48 @@ fn config_for_str(config_str: &str) -> Result<&'static Config> { Ok(found.unwrap()) } -pub fn detect(state: Arc>, opts: Opts, window: >k::Window) -> Result<()> { - let mut ports = None; +pub fn detect(state: Arc>, opts: Opts, window: >k::Window) -> Result<()> +{ + let mut ports: Option<(BoxedMidiIn, BoxedMidiOut)> = None; let mut config = None; // autodetect/open midi - let autodetect = match (&opts.input, &opts.output, &opts.model) { - (None, None, None) => true, - (None, None, Some(_)) => { + let autodetect = match (&opts.input, &opts.output, &opts.usb, &opts.model) { + (None, None, None, None) => true, + (None, None, None, Some(_)) => { warn!("Model set on command line, but not input/output ports. \ The model parameter will be ignored!"); true } - (Some(_), None, _) | (None, Some(_), _) => { + (Some(_), None, None, _) | (None, Some(_), None, _) => { bail!("Both input and output port need to be set on command line to skip autodetect!") } - (Some(i), Some(o), None) => { - let midi_in = MidiIn::new_for_address(i)?; - let midi_out = MidiOut::new_for_address(o)?; - ports = Some((midi_in, midi_out)); + (Some(_), _, Some(_), _) | (_, Some(_), Some(_), _) => { + bail!("MIDI and USB inputs cannot be set on command line together, use either MIDI or USB!") + } + // MIDI + (Some(i), Some(o), None, None) => { + let midi_in = MidiInPort::new_for_address(i)?; + let midi_out = MidiOutPort::new_for_address(o)?; + ports = Some((Box::new(midi_in), Box::new(midi_out))); + true + } + (Some(i), Some(o), None, Some(m)) => { + let midi_in = MidiInPort::new_for_address(i)?; + let midi_out = MidiOutPort::new_for_address(o)?; + ports = Some((Box::new(midi_in), Box::new(midi_out))); + config = Some(config_for_str(m)?); + false + } + // USB + (None, None, Some(u), None) => { + let (midi_in, midi_out) = usb_open(u)?; + ports = Some((Box::new(midi_in), Box::new(midi_out))); true } - (Some(i), Some(o), Some(m)) => { - let midi_in = MidiIn::new_for_address(i)?; - let midi_out = MidiOut::new_for_address(o)?; - ports = Some((midi_in, midi_out)); + (None, None, Some(u), Some(m)) => { + let (midi_in, midi_out) = usb_open(u)?; + ports = Some((Box::new(midi_in), Box::new(midi_out))); config = Some(config_for_str(m)?); false } diff --git a/gui/src/main.rs b/gui/src/main.rs index 14e8ed4..caf45c7 100644 --- a/gui/src/main.rs +++ b/gui/src/main.rs @@ -146,9 +146,10 @@ pub fn midi_in_out_stop(state: &mut State) -> JoinAll> { } pub fn midi_in_out_start(state: &mut State, - midi_in: Option, midi_out: Option, + midi_in: Option, midi_out: Option, midi_channel: u8, quirks: MidiQuirks, - config_changed: bool) { + config_changed: bool) +{ let notify = |state: &mut State| { sentry_set_midi_tags(state.midi_in_name.as_ref(), state.midi_out_name.as_ref()); @@ -178,10 +179,10 @@ pub fn midi_in_out_start(state: &mut State, let (in_cancel_tx, in_cancel_rx) = oneshot::channel::<()>(); let (out_cancel_tx, out_cancel_rx) = oneshot::channel::<()>(); - state.midi_in_name = Some(midi_in.name.clone()); + state.midi_in_name = Some(midi_in.name()); state.midi_in_cancel = Some(in_cancel_tx); - state.midi_out_name = Some(midi_out.name.clone()); + state.midi_out_name = Some(midi_out.name()); state.midi_out_cancel = Some(out_cancel_tx); state.midi_channel_num = midi_channel; @@ -275,8 +276,9 @@ pub fn midi_in_out_start(state: &mut State, state.midi_out_handle = Some(midi_out_handle); } -pub fn set_midi_in_out(state: &mut State, midi_in: Option, midi_out: Option, - midi_channel: u8, config: Option<&'static Config>) -> bool { +pub fn set_midi_in_out(state: &mut State, midi_in: Option, midi_out: Option, + midi_channel: u8, config: Option<&'static Config>) -> bool +{ if state.midi_in_cancel.is_some() || state.midi_out_cancel.is_some() { error!("Midi still running when entering send_midi_in_out"); // Not sure if we ever end up in this situation anymore, @@ -580,6 +582,11 @@ fn usb_list_devices() -> Vec { pod_usb::usb_list_devices() } +#[cfg(feature = "usb")] +fn usb_open(addr: &str) -> Result<(impl MidiIn, impl MidiOut)> { + pod_usb::usb_device_for_address(addr) +} + #[cfg(not(feature = "usb"))] fn start_usb() { } @@ -589,6 +596,11 @@ fn usb_list_devices() -> Vec { vec![] } +#[cfg(not(feature = "usb"))] +fn usb_open(addr: &str) -> Result<(impl MidiIn, impl MidiOut)> { + todo!() +} + #[tokio::main] async fn main() -> Result<()> { let _guard = sentry::init((option_env!("SENTRY_DSN"), sentry::ClientOptions { diff --git a/gui/src/opts.rs b/gui/src/opts.rs index 14a042f..8b73da3 100644 --- a/gui/src/opts.rs +++ b/gui/src/opts.rs @@ -2,7 +2,7 @@ use clap::Parser; use anyhow::Result; use std::fmt::Write; use pod_core::config::configs; -use pod_core::midi_io::{MidiIn, MidiOut, MidiPorts}; +use pod_core::midi_io::{MidiInPort, MidiOutPort, MidiPorts}; use crate::usb_list_devices; #[derive(Parser, Clone)] @@ -69,12 +69,12 @@ pub fn generate_help_text() -> Result { } writeln!(s, "")?; writeln!(s, "MIDI input ports (-i):")?; - for (i, n) in MidiIn::ports().ok().unwrap_or_default().iter().enumerate() { + for (i, n) in MidiInPort::ports().ok().unwrap_or_default().iter().enumerate() { writeln!(s, "{}[{}] {}", tab, i, n)?; } writeln!(s, "")?; writeln!(s, "MIDI output ports (-o):")?; - for (i, n) in MidiOut::ports().ok().unwrap_or_default().iter().enumerate() { + for (i, n) in MidiOutPort::ports().ok().unwrap_or_default().iter().enumerate() { writeln!(s, "{}[{}] {}", tab, i, n)?; } writeln!(s, "")?; diff --git a/gui/src/settings.rs b/gui/src/settings.rs index 6ed66f0..f8c9ea4 100644 --- a/gui/src/settings.rs +++ b/gui/src/settings.rs @@ -112,7 +112,7 @@ fn populate_midi_combos(settings: &SettingsDialog, in_name: &Option, out_name: &Option) { // populate "midi in" combo box settings.midi_in_combo.remove_all(); - let in_ports = MidiIn::ports().ok().unwrap_or_default(); + let in_ports = MidiInPort::ports().ok().unwrap_or_default(); in_ports.iter().for_each(|i| settings.midi_in_combo.append_text(i)); settings.midi_in_combo.set_active(None); @@ -127,7 +127,7 @@ fn populate_midi_combos(settings: &SettingsDialog, // populate "midi out" combo box settings.midi_out_combo.remove_all(); - let out_ports = MidiOut::ports().ok().unwrap_or_default(); + let out_ports = MidiOutPort::ports().ok().unwrap_or_default(); out_ports.iter().for_each(|i| settings.midi_out_combo.append_text(i)); settings.midi_out_combo.set_active(None); @@ -179,7 +179,7 @@ fn wire_autodetect_button(settings: &SettingsDialog) { // update in/out port selection, channel, device populate_midi_combos(&settings, - &Some(in_.name.clone()), &Some(out_.name.clone())); + &Some(in_.name()), &Some(out_.name())); let index = midi_channel_to_combo_index(channel); settings.midi_channel_combo.set_active(index); populate_model_combo(&settings, &Some(config.name.clone())); @@ -237,7 +237,7 @@ fn wire_test_button(settings: &SettingsDialog) { // update in/out port selection // TODO: do we need to update the combo here at all? populate_midi_combos(&settings, - &Some(in_.name.clone()), &Some(out_.name.clone())); + &Some(in_.name()), &Some(out_.name())); } Err(e) => { error!("Settings MIDI test failed: {}", e); @@ -325,7 +325,7 @@ pub fn create_settings_action(state: Arc>, ui: >k::Builder) -> gi let midi_in = settings.midi_in_combo.active_text() .and_then(|name| { let name = name.as_str(); - match MidiIn::new_for_name(name) { + match MidiInPort::new_for_name(name) { Ok(midi) => { Some(midi) } Err(err) => { error!("Failed to open MIDI after settings dialog closed: {}", err); @@ -336,7 +336,7 @@ pub fn create_settings_action(state: Arc>, ui: >k::Builder) -> gi let midi_out = settings.midi_out_combo.active_text() .and_then(|name| { let name = name.as_str(); - match MidiOut::new_for_name(name) { + match MidiOutPort::new_for_name(name) { Ok(midi) => { Some(midi) } Err(err) => { error!("Failed to open MIDI after settings dialog closed: {}", err); @@ -351,6 +351,8 @@ pub fn create_settings_action(state: Arc>, ui: >k::Builder) -> gi let midi_channel = settings.midi_channel_combo.active(); let midi_channel = midi_channel_from_combo_index(midi_channel); + let midi_in = midi_in.map(box_midi_in); + let midi_out = midi_out.map(box_midi_out); set_midi_in_out(&mut state.lock().unwrap(), midi_in, midi_out, midi_channel, config); } _ => { @@ -361,16 +363,18 @@ pub fn create_settings_action(state: Arc>, ui: >k::Builder) -> gi // restart midi thread after test if let Some((in_name, out_name)) = names { - let midi_in = MidiIn::new_for_name(in_name.as_str()) + let midi_in = MidiInPort::new_for_name(in_name.as_str()) .map_err(|err| { error!("Unable to restart MIDI input thread for {:?}: {}", in_name, err) }).ok(); - let midi_out = MidiOut::new_for_name(out_name.as_str()) + let midi_out = MidiOutPort::new_for_name(out_name.as_str()) .map_err(|err| { error!("Unable to restart MIDI output thread for {:?}: {}", out_name, err) }).ok(); let midi_channel_num = state.midi_channel_num; let quirks = state.config.map(|c| c.midi_quirks).unwrap(); + let midi_in = midi_in.map(box_midi_in); + let midi_out = midi_out.map(box_midi_out); midi_in_out_start(&mut state, midi_in, midi_out, midi_channel_num, quirks, false); } diff --git a/usb/Cargo.toml b/usb/Cargo.toml index 2526d25..f6f85b7 100644 --- a/usb/Cargo.toml +++ b/usb/Cargo.toml @@ -8,7 +8,11 @@ rust-version.workspace = true [dependencies] rusb = "0.9.4" +async-trait = "*" # defined in pod-core log = "*" # defined in pod-core tokio = "*" # defined in pod-core anyhow = "*" once_cell = "*" # defined in pod-code +regex = { version = "*", features = [] } # defined in pod-code + +pod-core = { path = "../core" } diff --git a/usb/src/dev_handler.rs b/usb/src/dev_handler.rs index cddf5a9..3415e73 100644 --- a/usb/src/dev_handler.rs +++ b/usb/src/dev_handler.rs @@ -1,10 +1,12 @@ use anyhow::*; use core::result::Result::Ok; -use std::sync::{Arc, Mutex, Weak}; +use std::sync::{Arc, Weak}; use std::time::Duration; +use async_trait::async_trait; use log::{error, info, trace}; use rusb::{DeviceHandle, Direction, Error, TransferType, UsbContext}; use tokio::sync::mpsc; +use pod_core::midi_io::{MidiIn, MidiOut}; use crate::devices::UsbDevice; use crate::endpoint::{configure_endpoint, Endpoint, find_endpoint}; use crate::line6::line6_read_serial; @@ -19,7 +21,9 @@ pub struct Device { } pub struct DeviceInner { + name: String, handle: Arc>, + write_ep: Endpoint, } pub struct DeviceInput { @@ -71,15 +75,19 @@ impl Device { pub fn open(&mut self) -> Result<(DeviceInput, DeviceOutput)> { if self.inner.upgrade().is_some() { - bail!("Devide already open") + bail!("Device already open") } - let inner = Arc::new(DeviceInner { - handle: self.handle.clone() - }); + let (tx, rx) = mpsc::unbounded_channel(); + let inner = Arc::new(DeviceInner::new( + self.name.clone(), + self.handle.clone(), + self.read_ep.clone(), + self.write_ep.clone(), + tx + )); self.inner = Arc::downgrade(&inner); - let (tx, rx) = mpsc::unbounded_channel(); let input = DeviceInput { inner: inner.clone(), rx @@ -94,7 +102,9 @@ impl Device { } impl DeviceInner { - fn new(handle: Arc>, read_ep: Endpoint, tx: mpsc::UnboundedSender>) -> Self { + fn new(name: String, handle: Arc>, + read_ep: Endpoint, write_ep: Endpoint, + tx: mpsc::UnboundedSender>) -> Self { let handle_ret = handle.clone(); let has_kernel_driver = match handle.kernel_driver_active(read_ep.iface) { Ok(true) => { @@ -106,7 +116,8 @@ impl DeviceInner { configure_endpoint(&handle, &read_ep).ok(); - tokio::spawn(async move { + // libusb's reads DEFINITELY need to go on the blocking tasks queue + tokio::task::spawn_blocking(move || { let mut buf = [0u8; 1024]; loop { let res = match read_ep.transfer_type { @@ -126,7 +137,12 @@ impl DeviceInner { Ok(len) => { let b = buf.chunks(len).next().unwrap(); trace!("<< {:02x?} len={}", &b, len); - tx.send(b.to_vec()).ok(); + match tx.send(b.to_vec()) { + Ok(_) => {} + Err(e) => { + error!("USB read thread tx failed: {}", e); + } + }; } Err(e) => { error!("USB read failed: {}", e); @@ -140,9 +156,29 @@ impl DeviceInner { }); DeviceInner { - handle: handle_ret + name, + handle: handle_ret, + write_ep } } + + fn send(&self, bytes: &[u8]) -> Result<()> { + trace!(">> {:02x?} len={}", bytes, bytes.len()); + let res = match self.write_ep.transfer_type { + TransferType::Bulk => { + self.handle.write_bulk(self.write_ep.address, bytes, Duration::MAX) + } + /* + TransferType::Interrupt => { + self.handle.write_bulk(self.write_ep.address, buf, Duration::MAX) + }*/ + tt => { + bail!("Transfer type {:?} not supported!", tt); + } + }; + + res.map(|_| ()).map_err(|e| anyhow!("USB write failed: {}", e)) + } } impl Drop for DeviceInner { @@ -154,94 +190,30 @@ impl Drop for DeviceInner { } } - -impl DevHandler { - pub fn new(handle: DeviceHandle, usb_dev: &UsbDevice) -> Result { - let serial = line6_read_serial(&handle).ok() - .map(|s| format!(" {}", s)) - .unwrap_or("".to_string()); - - let name = format!( - "{}{} [usb:{}:{}]", - &usb_dev.name, serial, handle.device().bus_number(), handle.device().address() - ); - - info!("Found: {}", name); - - let desc = handle.device().device_descriptor()?; - - // TODO: replace with .expect? - let Some(read_ep) = find_endpoint(&mut handle.device(), &desc, Direction::In, usb_dev.transfer_type, usb_dev.read_ep, usb_dev.alt_setting) else { - bail!("Read end-point not found") - }; - let Some(write_ep) = find_endpoint(&mut handle.device(), &desc, Direction::Out, usb_dev.transfer_type, usb_dev.write_ep, usb_dev.alt_setting) else { - bail!("Write end-point not found") - }; - - let (tx, rx) = mpsc::unbounded_channel(); - - Ok(DevHandler { - handle: Arc::new(handle), - read_ep, - write_ep, - tx, - rx - }) +#[async_trait] +impl MidiIn for DeviceInput { + fn name(&self) -> String { + self.inner.name.clone() } - pub fn start(&mut self) { - let mut handle = self.handle.clone(); - let read_ep = self.read_ep.clone(); - let tx = self.tx.clone(); - - let has_kernel_driver = match handle.kernel_driver_active(read_ep.iface) { - Ok(true) => { - handle.detach_kernel_driver(read_ep.iface).ok(); - true - } - _ => false - }; - - configure_endpoint(&handle, &read_ep).ok(); - - tokio::spawn(async move { - let mut buf = [0u8; 1024]; - loop { - let res = match read_ep.transfer_type { - TransferType::Bulk => { - handle.read_bulk(read_ep.address, &mut buf, Duration::MAX) - } - TransferType::Interrupt => { - handle.read_interrupt(read_ep.address, &mut buf, Duration::MAX) - } - tt => { - error!("Transfer type {:?} not supported!", tt); - break; - } + async fn recv(&mut self) -> Option> { + self.rx.recv().await + } - }; - match res { - Ok(len) => { - let b = buf.chunks(len).next().unwrap(); - trace!("<< {:02x?} len={}", &b, len); - tx.send(b.to_vec()).ok(); - } - Err(e) => { - error!("USB read failed: {}", e); - match e { - Error::Busy | Error::Timeout | Error::Overflow => { continue } - _ => { break } - } - } - } - } - }); + fn close(&mut self) { } +} - pub fn stop(&mut self) { - // TODO: we consider that there is ever only one device - self.handle.context().interrupt_handle_events(); +#[async_trait] +impl MidiOut for DeviceOutput { + fn name(&self) -> String { + self.inner.name.clone() + } + fn send(&mut self, bytes: &[u8]) -> Result<()> { + self.inner.send(bytes) } + fn close(&mut self) { + } } \ No newline at end of file diff --git a/usb/src/lib.rs b/usb/src/lib.rs index 1f55a8b..0b913e8 100644 --- a/usb/src/lib.rs +++ b/usb/src/lib.rs @@ -7,8 +7,10 @@ mod util; use log::{debug, error, info}; use anyhow::*; +use anyhow::Context as _; use core::result::Result::Ok; use std::collections::HashMap; +use std::str::FromStr; use std::sync::{Arc, Mutex}; use std::sync::atomic::{AtomicBool, Ordering}; use once_cell::sync::Lazy; @@ -16,6 +18,8 @@ use once_cell::sync::Lazy; use rusb::{Context, Device as UsbDevice, GlobalContext, Hotplug, HotplugBuilder, UsbContext}; use tokio::sync::{broadcast, Notify}; use tokio::sync::broadcast::error::RecvError; +use pod_core::midi_io::{MidiIn, MidiOut}; +use regex::Regex; use crate::dev_handler::Device; use crate::devices::find_device; use crate::event::*; @@ -103,7 +107,8 @@ pub fn usb_start() -> Result<()> { .enumerate(true) .register(&ctx, Box::new(hh))?; - tokio::spawn(async move { + // libusb's handle_events may need to go on the blocking tasks queue + tokio::task::spawn_blocking(move || { info!("USB hotplug thread start"); let mut reg = Some(hotplug); loop { @@ -198,3 +203,27 @@ fn usb_remove_device(key: String) { let mut devices = DEVICES.lock().unwrap(); devices.remove(&key); } + +pub fn usb_device_for_address(dev_addr: &str) -> Result<(impl MidiIn, impl MidiOut)> { + let mut devices = DEVICES.lock().unwrap(); + + let port_n_re = Regex::new(r"\d+").unwrap(); + let port_id_re = Regex::new(r"\d+:\d+").unwrap(); + + let mut found = None; + if port_id_re.is_match(dev_addr) { + found = devices.get_mut(dev_addr); + } else if port_n_re.is_match(dev_addr) { + let n = usize::from_str(&dev_addr) + .with_context(|| format!("Unrecognized USB device index {:?}", dev_addr))?; + found = devices.values_mut().nth(n); + } else { + bail!("Unrecognized USB device address {:?}", dev_addr); + } + + let Some(dev) = found.take() else { + bail!("USB device for address {:?} not found!", dev_addr); + }; + + dev.open() +} \ No newline at end of file diff --git a/usb/src/util.rs b/usb/src/util.rs index 0e8a1f0..d3ba5e2 100644 --- a/usb/src/util.rs +++ b/usb/src/util.rs @@ -1,4 +1,4 @@ pub fn usb_address_string(bus: u8, address: u8) -> String { - format!("usb:{}:{}", bus, address) + format!("{}:{}", bus, address) } \ No newline at end of file From f55630828bc5002906327662adc459870c2326e5 Mon Sep 17 00:00:00 2001 From: Artem Egorkine Date: Sun, 28 Jul 2024 22:50:42 +0300 Subject: [PATCH 3/6] USB: testing code Added USB test app that uses Linux `usb-gadget` subsystem to emulate a POD device for USB development/testing; --- .gitignore | 1 + testing/usb/Cargo.lock | 244 ++++++++++++++++++++++++++++++++++++++++ testing/usb/Cargo.toml | 15 +++ testing/usb/src/main.rs | 172 ++++++++++++++++++++++++++++ usb/src/devices.rs | 5 + 5 files changed, 437 insertions(+) create mode 100644 testing/usb/Cargo.lock create mode 100644 testing/usb/Cargo.toml create mode 100644 testing/usb/src/main.rs diff --git a/.gitignore b/.gitignore index 1be3ff4..6a5c427 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ .DS_Store .*.sw? *~ +/testing/usb/target* diff --git a/testing/usb/Cargo.lock b/testing/usb/Cargo.lock new file mode 100644 index 0000000..ef29dba --- /dev/null +++ b/testing/usb/Cargo.lock @@ -0,0 +1,244 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" + +[[package]] +name = "cc" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "libusb1-sys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da050ade7ac4ff1ba5379af847a10a10a8e284181e060105bf8d86960ce9ce0f" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "macaddr" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baee0bbc17ce759db233beb01648088061bf678383130602a298e6998eedb2d8" + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "partition-identity" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa925f9becb532d758b0014b472c576869910929cf4c3f8054b386f19ab9e21" +dependencies = [ + "thiserror", +] + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proc-mounts" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d652f8435d0ab70bf4f3590a6a851d59604831a458086541b95238cc51ffcf2" +dependencies = [ + "partition-identity", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rusb" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab9f9ff05b63a786553a4c02943b74b34a988448671001e9a27e2f0565cc05a4" +dependencies = [ + "libc", + "libusb1-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "usb" +version = "0.0.0" +dependencies = [ + "bytes", + "log", + "rusb", + "usb-gadget", +] + +[[package]] +name = "usb-gadget" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f179c052f5053bd7f40460530ae48b7a2a0c250e7aa15411f64a9f16b02fcb12" +dependencies = [ + "bitflags", + "byteorder", + "bytes", + "libc", + "log", + "macaddr", + "nix", + "proc-mounts", + "strum", + "uuid", +] + +[[package]] +name = "uuid" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" diff --git a/testing/usb/Cargo.toml b/testing/usb/Cargo.toml new file mode 100644 index 0000000..5fc9879 --- /dev/null +++ b/testing/usb/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "usb" +version = "0.0.0" +authors = ["Artem Egorkine "] +edition = "2021" +rust-version = "1.73" + +[dependencies] + +bytes = { version = "*", features = [] } +rusb = "*" +usb-gadget = "*" +log = "*" + +[workspace] diff --git a/testing/usb/src/main.rs b/testing/usb/src/main.rs new file mode 100644 index 0000000..58b7395 --- /dev/null +++ b/testing/usb/src/main.rs @@ -0,0 +1,172 @@ +use std::io::ErrorKind; +use std::sync::{Arc, mpsc}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::mpsc::RecvError; +use std::thread; +use std::time::Duration; +use usb_gadget::{Class, Config, default_udc, Gadget, Id, Strings}; +use usb_gadget::function::custom::{Custom, Endpoint, EndpointDirection, Interface}; +use bytes::BytesMut; + +const FAMILY: u16 = 0x0000; +const MEMBER: u16 = 0x0600; + +fn reply(req: &[u8]) -> Option> { + + match req { + &[0xf0, 0x7e, channel, 0x06, 0x01, 0xf7] => { + // UDI + // channel can vary, but in practice, this will always ne 0x7f + let family = u16::to_le_bytes(FAMILY); + let member = u16::to_le_bytes(MEMBER); + let ver = format!("{:4}", "1.01").into_bytes(); + Some([0xf0, 0x7e, channel, 0x06, 0x02, 0x00, 0x01, 0x0c, family[0], family[1], member[0], member[1], + ver[0], ver[1], ver[2], ver[3], 0xf7].to_vec()) + } + &_ => { None } + } +} + +fn main() { + usb_gadget::remove_all().expect("cannot remove all gadgets"); + + let (mut cmd_tx, cmd_rx) = mpsc::channel::>(); + + let (mut ep1_rx, ep1_dir) = EndpointDirection::host_to_device(); + let (mut ep2_tx, ep2_dir) = EndpointDirection::device_to_host(); + + let (mut custom, handle) = Custom::builder() + .with_interface( + Interface::new(Class::vendor_specific(1, 2), "custom interface") + .with_endpoint(Endpoint::bulk(ep1_dir)) + .with_endpoint(Endpoint::bulk(ep2_dir)), + ) + .build(); + + let udc = default_udc().expect("cannot get UDC"); + let reg = Gadget::new( + Class::new(255, 255, 3), + Id::new(0x0010, 0x0001), + Strings::new("POD-UI", "testing device", "serial_number"), + ) + .with_config(Config::new("config").with_function(handle)) + .bind(&udc) + .expect("cannot bind to UDC"); + + println!("Custom function at {}", custom.status().unwrap().path().unwrap().display()); + println!(); + + let ep1_control = ep1_rx.control().unwrap(); + println!("ep1 unclaimed: {:?}", ep1_control.unclaimed_fifo()); + println!("ep1 real address: {}", ep1_control.real_address().unwrap()); + println!("ep1 descriptor: {:?}", ep1_control.descriptor().unwrap()); + println!(); + + let ep2_control = ep2_tx.control().unwrap(); + println!("ep2 unclaimed: {:?}", ep2_control.unclaimed_fifo()); + println!("ep2 real address: {}", ep2_control.real_address().unwrap()); + println!("ep2 descriptor: {:?}", ep2_control.descriptor().unwrap()); + println!(); + + let stop = Arc::new(AtomicBool::new(false)); + + thread::scope(|s| { + thread::Builder::new() + .name("rx".into()) + .spawn_scoped(s, move || { + let size = ep1_rx.max_packet_size().unwrap(); + while !stop.load(Ordering::Relaxed) { + let res = ep1_rx + .recv_timeout(BytesMut::with_capacity(size), Duration::from_secs(1)); + let data = match res { + Ok(v) => { v } + Err(e) if e.raw_os_error() == Some(108) => { + // Ignore this error -- they seem to come while in process of data transfer: + // Os { code: 108, kind: Uncategorized, message: "Cannot send after transport endpoint shutdown" } + continue; + } + Err(e) => { + println!("RX error: {e:?}"); + continue + } + }; + match data { + Some(data) => { + let d = data.as_ref(); + println!("<< {:02x?} len={}", d, d.len()); + if let Some(rep) = reply(d) { + thread::sleep(Duration::from_millis(500)); + cmd_tx.send(rep).ok(); + } + } + None => { + // empty + } + } + } + }).ok(); + + thread::Builder::new() + .name("tx".into()) + .spawn_scoped(s, move || { + let size = ep2_tx.max_packet_size().unwrap(); + loop { + //while !stop.load(Ordering::Relaxed) { + let data = match cmd_rx.recv() { + Ok(data) => { data } + Err(e) => { + println!("Command rx error: {}", e); + continue; + } + }; + + match ep2_tx.send_timeout(data.clone().into(), Duration::from_secs(1)) { + Ok(()) => { + println!(">> {:02x?} len={}", &data, data.len()); + } + Err(err) if err.kind() == ErrorKind::TimedOut => println!("send timeout"), + Err(err) => panic!("send failed: {err}"), + } + } + }).ok(); + + thread::Builder::new() + .name("control".into()) + .spawn_scoped(s, || { + //let mut ctrl_data = Vec::new(); + + loop { + //while !stop.load(Ordering::Relaxed) { + let data = + custom.event_timeout(Duration::from_secs(1)) + .expect("event failed"); + if let Some(event) = data { + println!("Event: {event:?}"); + /* + match event { + Event::SetupHostToDevice(req) => { + if req.ctrl_req().request == 255 { + println!("Stopping"); + stop.store(true, Ordering::Relaxed); + } + ctrl_data = req.recv_all().unwrap(); + println!("Control data: {ctrl_data:x?}"); + } + Event::SetupDeviceToHost(req) => { + println!("Replying with data"); + req.send(&ctrl_data).unwrap(); + } + _ => (), + } + */ + + } + } + }).ok(); + }); + + thread::sleep(Duration::from_secs(1)); + + println!("Unregistering"); + reg.remove().unwrap(); +} diff --git a/usb/src/devices.rs b/usb/src/devices.rs index 424d4d5..275e321 100644 --- a/usb/src/devices.rs +++ b/usb/src/devices.rs @@ -83,6 +83,11 @@ static USB_DEVICES: Lazy> = Lazy::new(|| { alt_setting: 0, read_ep: 0x82, write_ep: 0x02, transfer_type: TransferType::Bulk }, + UsbDevice { + id: id!(0x0010, 0x0001), name: "POD-UI testing device".into(), + alt_setting: 0, read_ep: 0x81, write_ep: 0x02, + transfer_type: TransferType::Bulk + }, ] }); From 90cfd13ea6a166e4fd0685edcc3cbf7af59d80a6 Mon Sep 17 00:00:00 2001 From: Artem Egorkine Date: Mon, 29 Jul 2024 23:44:39 +0300 Subject: [PATCH 4/6] USB: support USB in settings / test functionality * Moved USB proxy code `gui/src/usb`; * Settings: add USB devices to combo box; * Support USB devices in test code; --- Cargo.lock | 1 + core/src/midi_io.rs | 14 +- gui/Cargo.toml | 1 + gui/src/autodetect.rs | 29 +++- gui/src/main.rs | 35 +---- gui/src/opts.rs | 2 +- gui/src/settings.rs | 309 ++++++++++++++++++++++++++++++----------- gui/src/usb.rs | 55 ++++++++ usb/src/dev_handler.rs | 1 - usb/src/lib.rs | 15 +- 10 files changed, 338 insertions(+), 124 deletions(-) create mode 100644 gui/src/usb.rs diff --git a/Cargo.lock b/Cargo.lock index c57bd59..7f94973 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1396,6 +1396,7 @@ name = "pod-gui" version = "0.0.0" dependencies = [ "anyhow", + "bitflags", "clap", "futures", "futures-util", diff --git a/core/src/midi_io.rs b/core/src/midi_io.rs index b761a1d..458d238 100644 --- a/core/src/midi_io.rs +++ b/core/src/midi_io.rs @@ -550,11 +550,9 @@ pub async fn autodetect_with_ports(in_ports: Vec, out_ports: Vec Result<(BoxedMidiIn, BoxedMidiOut, u8)> { - let in_port = MidiInPort::new_for_name(in_name)?; - let out_port = MidiOutPort::new_for_name(out_name)?; - let mut in_ports = vec![box_midi_in(in_port)]; - let mut out_ports = vec![box_midi_out(out_port)]; +pub async fn test_with_ports(in_port: BoxedMidiIn, out_port: BoxedMidiOut, channel: u8, config: &Config) -> Result<(BoxedMidiIn, BoxedMidiOut, u8)> { + let mut in_ports = vec![in_port]; + let mut out_ports = vec![out_port]; let (rep, error) = detect_with_channel( in_ports.as_mut_slice(), out_ports.as_mut_slice(), channel @@ -573,6 +571,12 @@ pub async fn test(in_name: &str, out_name: &str, channel: u8, config: &Config) - Ok((in_ports.remove(0), out_ports.remove(0), channel)) } +pub async fn test(in_name: &str, out_name: &str, channel: u8, config: &Config) -> Result<(BoxedMidiIn, BoxedMidiOut, u8)> { + let in_port = MidiInPort::new_for_name(in_name)?; + let out_port = MidiOutPort::new_for_name(out_name)?; + test_with_ports(box_midi_in(in_port), box_midi_out(out_port), channel, config).await +} + #[cfg(target_os = "linux")] fn check_for_broken_drivers(port_name: &String, bytes: &Vec) -> Option { if port_name.starts_with("PODxt") && diff --git a/gui/Cargo.toml b/gui/Cargo.toml index 6a65790..2b59804 100644 --- a/gui/Cargo.toml +++ b/gui/Cargo.toml @@ -14,6 +14,7 @@ sentry = { version = "0.29.2", features = ["debug-images"] } simple_logger = "=4.0.0" string_template = "0.2.1" +bitflags = "*" # defined in pod-core once_cell = "*" # defined in pod-core maplit = "*" # defined in mod-pod2 log = "*" # defined in pod-core diff --git a/gui/src/autodetect.rs b/gui/src/autodetect.rs index 3f8347f..126f7e7 100644 --- a/gui/src/autodetect.rs +++ b/gui/src/autodetect.rs @@ -8,7 +8,8 @@ use pod_core::midi_io::*; use pod_core::model::Config; use pod_gtk::prelude::*; use crate::opts::Opts; -use crate::{set_midi_in_out, State, usb_open}; +use crate::{set_midi_in_out, State}; +use crate::usb::{usb_open_addr, usb_open_name}; fn config_for_str(config_str: &str) -> Result<&'static Config> { use std::str::FromStr; @@ -73,12 +74,12 @@ pub fn detect(state: Arc>, opts: Opts, window: >k::Window) -> Res } // USB (None, None, Some(u), None) => { - let (midi_in, midi_out) = usb_open(u)?; + let (midi_in, midi_out) = usb_open_addr(u)?; ports = Some((Box::new(midi_in), Box::new(midi_out))); true } (None, None, Some(u), Some(m)) => { - let (midi_in, midi_out) = usb_open(u)?; + let (midi_in, midi_out) = usb_open_addr(u)?; ports = Some((Box::new(midi_in), Box::new(midi_out))); config = Some(config_for_str(m)?); false @@ -155,4 +156,24 @@ pub fn detect(state: Arc>, opts: Opts, window: >k::Window) -> Res }); Ok(()) -} \ No newline at end of file +} + +pub async fn test(in_name: &str, out_name: &str, channel: u8, is_usb: bool, config: &Config) -> Result<(BoxedMidiIn, BoxedMidiOut, u8)> { + let (midi_in, midi_out) = open(in_name, out_name, is_usb)?; + test_with_ports(midi_in, midi_out, channel, config).await +} + +pub fn open(in_name: &str, out_name: &str, is_usb: bool) -> Result<(BoxedMidiIn, BoxedMidiOut)> { + let res = if is_usb { + if in_name != out_name { + bail!("USB device input/output names do not match"); + } + let (midi_in, midi_out) = usb_open_name(in_name)?; + (box_midi_in(midi_in), box_midi_out(midi_out)) + } else { + let midi_in = MidiInPort::new_for_name(in_name)?; + let midi_out = MidiOutPort::new_for_name(out_name)?; + (box_midi_in(midi_in), box_midi_out(midi_out)) + }; + Ok(res) +} diff --git a/gui/src/main.rs b/gui/src/main.rs index caf45c7..b370cdf 100644 --- a/gui/src/main.rs +++ b/gui/src/main.rs @@ -7,6 +7,7 @@ mod widgets; mod autodetect; mod check; mod icon; +mod usb; use std::collections::HashMap; use std::sync::{Arc, atomic, Mutex}; @@ -45,6 +46,7 @@ use crate::panic::*; use crate::registry::*; use crate::settings::*; use crate::util::{next_thread_id, SenderExt as SenderExt2}; +use crate::usb::start_usb; use crate::widgets::*; use crate::widgets::templated::Templated; @@ -77,6 +79,7 @@ pub struct State { pub midi_out_handle: Option>, pub midi_channel_num: u8, + pub midi_is_usb: bool, pub app_event_tx: broadcast::Sender, pub ui_event_tx: glib::Sender, @@ -569,37 +572,6 @@ pub fn ui_modified_handler(ctx: &Ctx, event: &ModifiedEvent, ui_event_tx: &glib: } } -#[cfg(feature = "usb")] -fn start_usb() { - pod_usb::usb_start().unwrap(); - executor::block_on( - pod_usb::usb_init_wait() - ); -} - -#[cfg(feature = "usb")] -fn usb_list_devices() -> Vec { - pod_usb::usb_list_devices() -} - -#[cfg(feature = "usb")] -fn usb_open(addr: &str) -> Result<(impl MidiIn, impl MidiOut)> { - pod_usb::usb_device_for_address(addr) -} - -#[cfg(not(feature = "usb"))] -fn start_usb() { -} - -#[cfg(not(feature = "usb"))] -fn usb_list_devices() -> Vec { - vec![] -} - -#[cfg(not(feature = "usb"))] -fn usb_open(addr: &str) -> Result<(impl MidiIn, impl MidiOut)> { - todo!() -} #[tokio::main] async fn main() -> Result<()> { @@ -682,6 +654,7 @@ fn activate(app: >k::Application, title: &String, opts: Opts, sentry_enabled: midi_out_cancel: None, midi_out_handle: None, midi_channel_num: 0, + midi_is_usb: false, app_event_tx: app_event_tx.clone(), ui_event_tx: ui_event_tx.clone(), config: None, diff --git a/gui/src/opts.rs b/gui/src/opts.rs index 8b73da3..91e5d92 100644 --- a/gui/src/opts.rs +++ b/gui/src/opts.rs @@ -3,7 +3,7 @@ use anyhow::Result; use std::fmt::Write; use pod_core::config::configs; use pod_core::midi_io::{MidiInPort, MidiOutPort, MidiPorts}; -use crate::usb_list_devices; +use crate::usb::*; #[derive(Parser, Clone)] pub struct Opts { diff --git a/gui/src/settings.rs b/gui/src/settings.rs index f8c9ea4..911544c 100644 --- a/gui/src/settings.rs +++ b/gui/src/settings.rs @@ -1,6 +1,7 @@ use std::sync::{Arc, Mutex}; +use std::sync::atomic::{AtomicBool, Ordering}; use anyhow::anyhow; -use pod_core::midi_io::*; +use futures_util::TryFutureExt; use pod_gtk::prelude::*; use gtk::{IconSize, ResponseType}; use crate::{gtk, midi_in_out_start, midi_in_out_stop, set_midi_in_out, State}; @@ -8,12 +9,18 @@ use crate::{gtk, midi_in_out_start, midi_in_out_stop, set_midi_in_out, State}; use log::*; use pod_core::config::configs; use pod_core::midi::Channel; +use pod_core::midi_io::{MidiInPort, MidiOutPort, MidiPorts}; +use pod_gtk::prelude::glib::bitflags::bitflags; +use crate::autodetect::{open, test}; +use crate::usb; #[derive(Clone)] struct SettingsDialog { dialog: gtk::Dialog, - midi_in_combo: gtk::ComboBoxText, - midi_out_combo: gtk::ComboBoxText, + midi_in_combo: gtk::ComboBox, + midi_in_combo_model: gtk::ListStore, + midi_out_combo: gtk::ComboBox, + midi_out_combo_model: gtk::ListStore, midi_channel_combo: gtk::ComboBoxText, model_combo: gtk::ComboBoxText, autodetect_button: gtk::Button, @@ -24,12 +31,102 @@ struct SettingsDialog { spinner: Option } +bitflags! { + pub struct EntryFlags: u8 { + const ENTRY_HEADER = 0x01; + const ENTRY_TEXT = 0x02; + + const ENTRY_USB = 0x10; + } +} + impl SettingsDialog { fn new(ui: >k::Builder) -> Self { + let func = |combo: >k::ComboBox| { + let combo = combo.clone(); + + // track "popup-shown" event for showing entries in a tree structure in the popup + let popup_shown = Arc::new(AtomicBool::new(false)); + combo.connect_popup_shown_notify({ + let popup_shown = popup_shown.clone(); + move |combo| { + popup_shown.store(combo.is_popup_shown(), Ordering::Relaxed); + } + }); + + move |layout: >k::CellLayout, renderer: >k::CellRenderer, model: >k::TreeModel, iter: >k::TreeIter| { + let popup_shown = popup_shown.load(Ordering::Relaxed); + + let entry_type = EntryFlags::from_bits( + model.value(iter, 1).get::().unwrap() + ).unwrap(); + if entry_type.contains(EntryFlags::ENTRY_HEADER) { + // header text + renderer.set_sensitive(false); + renderer.set_visible(popup_shown); + renderer.set_padding(0, 0); + renderer.set_properties(&[ + ("weight", &700) + ]); + } else { + // normal entry or text entry + renderer.set_sensitive(!entry_type.contains(EntryFlags::ENTRY_TEXT)); + renderer.set_visible(true); + let padding = if popup_shown { 10 } else { 0 }; + renderer.set_padding(padding, 0); + renderer.set_properties(&[ + ("weight", &400) + ]); + } + } + }; + + let midi_in_combo = ui.object::("settings_midi_in_combo").unwrap(); + + let renderer = gtk::CellRendererText::new(); + midi_in_combo.clear(); + midi_in_combo.pack_start(&renderer, true); + midi_in_combo.add_attribute(&renderer, "text", 0); + midi_in_combo.set_cell_data_func(&renderer, Some(Box::new(func(&midi_in_combo)))); + + let midi_in_combo_model = gtk::ListStore::new(&[glib::Type::STRING, glib::Type::U8]); + midi_in_combo.set_model(Some(&midi_in_combo_model)); + + let midi_out_combo = ui.object::("settings_midi_out_combo").unwrap(); + + let renderer = gtk::CellRendererText::new(); + midi_out_combo.clear(); + midi_out_combo.pack_start(&renderer, true); + midi_out_combo.add_attribute(&renderer, "text", 0); + midi_out_combo.set_cell_data_func(&renderer, Some(Box::new(func(&midi_out_combo)))); + + let midi_out_combo_model = gtk::ListStore::new(&[glib::Type::STRING, glib::Type::U8]); + midi_out_combo.set_model(Some(&midi_out_combo_model)); + + // attach combo + let combos = vec![ + (midi_in_combo.clone(), midi_out_combo.clone()), + (midi_out_combo.clone(), midi_in_combo.clone()), + ]; + for (src, target) in combos { + src.connect_active_notify(move |combo| { + let Some((name, flags)) = combo_get_active(combo) else { return }; + if !flags.contains(EntryFlags::ENTRY_USB) { return }; + + let model = target.model().unwrap(); + let store = model.dynamic_cast_ref::().unwrap(); + let item = combo_model_find(store, &Some(name)); + target.set_active(item); + }); + + } + SettingsDialog { dialog: ui.object("settings_dialog").unwrap(), - midi_in_combo: ui.object("settings_midi_in_combo").unwrap(), - midi_out_combo: ui.object("settings_midi_out_combo").unwrap(), + midi_in_combo, + midi_in_combo_model, + midi_out_combo, + midi_out_combo_model, midi_channel_combo: ui.object("settings_midi_channel_combo").unwrap(), model_combo: ui.object("settings_model_combo").unwrap(), autodetect_button: ui.object("settings_autodetect_button").unwrap(), @@ -108,37 +205,93 @@ fn populate_midi_channel_combo(settings: &SettingsDialog) { CHANNELS.iter().for_each(|i| settings.midi_channel_combo.append_text(i)); } +fn combo_model_populate(model: >k::ListStore, midi_devices: &Vec, usb_devices: &Vec) { + model.clear(); + + let mut n: u32 = 0; + let mut next = || { let r = n; n += 1; Some(n) }; + + let mut add = |entry: &str, flags: EntryFlags| { + let data: [(u32, &dyn ToValue); 2] = [ (0, &entry), (1, &flags.bits()) ]; + model.insert_with_values(next(), &data); + }; + + add("MIDI", EntryFlags::ENTRY_HEADER); + if !midi_devices.is_empty() { + for name in midi_devices { + add(name, EntryFlags::empty()); + } + } else { + add("No devices found...", EntryFlags::ENTRY_TEXT); + } + + add("USB", EntryFlags::ENTRY_HEADER); + if !usb_devices.is_empty() { + for name in usb_devices { + add(name, EntryFlags::ENTRY_USB); + } + } else { + add("No devices found...", EntryFlags::ENTRY_TEXT); + } +} + +fn combo_model_find(model: >k::ListStore, value: &Option) -> Option { + let iter = model.iter_first(); + if let Some(iter) = iter { + let mut has_value = true; + let mut n = 0; + while has_value { + let flags = EntryFlags::from_bits( + model.value(&iter, 1).get::().unwrap() + ).unwrap(); + if (flags & (EntryFlags::ENTRY_HEADER | EntryFlags::ENTRY_TEXT)) != EntryFlags::empty() { + n += 1; + has_value = model.iter_next(&iter); + continue; + } + let name = model.value(&iter, 0).get::().unwrap(); + if value.is_none() || value.as_ref().map(|v| *v == name).unwrap_or_default() { + return Some(n); + } + + n += 1; + has_value = model.iter_next(&iter); + } + } + + None +} + +fn combo_get_active(combo: >k::ComboBox) -> Option<(String, EntryFlags)> { + let Some(model) = combo.model() else { + return None; + }; + let Some(iter) = combo.active_iter() else { + return None; + }; + + let model = model.dynamic_cast_ref::().unwrap(); + let name = model.value(&iter, 0).get::().unwrap(); + let flags = EntryFlags::from_bits( + model.value(&iter, 1).get::().unwrap() + ).unwrap(); + Some((name, flags)) +} + fn populate_midi_combos(settings: &SettingsDialog, in_name: &Option, out_name: &Option) { // populate "midi in" combo box - settings.midi_in_combo.remove_all(); - let in_ports = MidiInPort::ports().ok().unwrap_or_default(); - in_ports.iter().for_each(|i| settings.midi_in_combo.append_text(i)); - - settings.midi_in_combo.set_active(None); - let current_in_port = in_name.clone().unwrap_or_default(); - if in_ports.len() > 0 { - let v = in_ports.iter().enumerate() - .find(|(_, name)| ¤t_in_port == *name) - .map(|(idx, _)| idx as u32) - .or(Some(0)); - settings.midi_in_combo.set_active(v); - }; + let midi_ports = MidiInPort::ports().ok().unwrap_or_default(); + let usb_ports = usb::usb_list_devices(); + combo_model_populate(&settings.midi_in_combo_model, &midi_ports, &usb_ports); + let active = combo_model_find(&settings.midi_in_combo_model, in_name); + settings.midi_in_combo.set_active(active); // populate "midi out" combo box - settings.midi_out_combo.remove_all(); - let out_ports = MidiOutPort::ports().ok().unwrap_or_default(); - out_ports.iter().for_each(|i| settings.midi_out_combo.append_text(i)); - - settings.midi_out_combo.set_active(None); - let current_out_port = out_name.clone().unwrap_or_default(); - if out_ports.len() > 0 { - let v = out_ports.iter().enumerate() - .find(|(_, name)| ¤t_out_port == *name) - .map(|(idx, _)| idx as u32) - .or(Some(0)); - settings.midi_out_combo.set_active(v); - }; + let midi_ports = MidiOutPort::ports().ok().unwrap_or_default(); + combo_model_populate(&settings.midi_out_combo_model, &midi_ports, &usb_ports); + let active = combo_model_find(&settings.midi_out_combo_model, out_name); + settings.midi_out_combo.set_active(active); } fn populate_model_combo(settings: &SettingsDialog, selected: &Option) { @@ -198,8 +351,10 @@ fn wire_autodetect_button(settings: &SettingsDialog) { fn wire_test_button(settings: &SettingsDialog) { let settings = settings.clone(); settings.test_button.clone().connect_clicked(move |button| { - let midi_in = settings.midi_in_combo.active_text(); - let midi_out = settings.midi_out_combo.active_text(); + let midi_in = combo_get_active(&settings.midi_in_combo); + let midi_in_is_usb = midi_in.as_ref().map(|(_,flags)| flags.contains(EntryFlags::ENTRY_USB)).unwrap_or(false); + let midi_out = combo_get_active(&settings.midi_out_combo); + let midi_out_is_usb = midi_out.as_ref().map(|(_,flags)| flags.contains(EntryFlags::ENTRY_USB)).unwrap_or(false); let midi_channel = settings.midi_channel_combo.active(); let config = settings.model_combo.active_text() .and_then(|name| { @@ -215,13 +370,15 @@ fn wire_test_button(settings: &SettingsDialog) { return; } - let midi_in = midi_in.as_ref().unwrap().to_string(); - let midi_out = midi_out.as_ref().unwrap().to_string(); + let is_usb = midi_in_is_usb || midi_out_is_usb; + let midi_in = midi_in.map(|(n,_)| n).unwrap(); + let midi_out = midi_out.map(|(n,_)| n).unwrap(); let midi_channel = midi_channel_from_combo_index(midi_channel); + let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT); tokio::spawn(async move { - let res = pod_core::midi_io::test(&midi_in, &midi_out, midi_channel, config.unwrap()).await; + let res = test(&midi_in, &midi_out, midi_channel, is_usb, config.unwrap()).await; tx.send(res).ok(); }); @@ -322,62 +479,52 @@ pub fn create_settings_action(state: Arc>, ui: >k::Builder) -> gi match settings.dialog.run() { ResponseType::Ok => { - let midi_in = settings.midi_in_combo.active_text() + let midi_in = combo_get_active(&settings.midi_in_combo); + let midi_in_is_usb = midi_in.as_ref().map(|(_,flags)| flags.contains(EntryFlags::ENTRY_USB)).unwrap_or(false); + let midi_out = combo_get_active(&settings.midi_out_combo); + let midi_out_is_usb = midi_out.as_ref().map(|(_,flags)| flags.contains(EntryFlags::ENTRY_USB)).unwrap_or(false); + let midi_channel = settings.midi_channel_combo.active(); + let config = settings.model_combo.active_text() .and_then(|name| { - let name = name.as_str(); - match MidiInPort::new_for_name(name) { - Ok(midi) => { Some(midi) } - Err(err) => { - error!("Failed to open MIDI after settings dialog closed: {}", err); - None - } - } + configs().iter().find(|c| c.name == name) }); - let midi_out = settings.midi_out_combo.active_text() - .and_then(|name| { - let name = name.as_str(); - match MidiOutPort::new_for_name(name) { - Ok(midi) => { Some(midi) } + + let is_usb = midi_in_is_usb || midi_out_is_usb; + let midi_in = midi_in.map(|(n,_)| n); + let midi_out = midi_out.map(|(n,_)| n); + let midi_channel = midi_channel_from_combo_index(midi_channel); + + let (midi_in, midi_out) = midi_in.zip(midi_out) + .and_then(|(midi_in, midi_out)| { + match open(&midi_in, &midi_out, is_usb) { + Ok(v) => { Some(v) }, Err(err) => { error!("Failed to open MIDI after settings dialog closed: {}", err); None } } - }); - let config = settings.model_combo.active_text() - .and_then(|name| { - configs().iter().find(|c| c.name == name) - }); - - let midi_channel = settings.midi_channel_combo.active(); - let midi_channel = midi_channel_from_combo_index(midi_channel); - let midi_in = midi_in.map(box_midi_in); - let midi_out = midi_out.map(box_midi_out); + }) + .unzip(); set_midi_in_out(&mut state.lock().unwrap(), midi_in, midi_out, midi_channel, config); } _ => { let mut state = state.lock().unwrap(); - let names = state.midi_in_name.as_ref().and_then(|in_name| { - state.midi_out_name.as_ref().map(|out_name| (in_name.clone(), out_name.clone())) - }); - - // restart midi thread after test - if let Some((in_name, out_name)) = names { - let midi_in = MidiInPort::new_for_name(in_name.as_str()) - .map_err(|err| { - error!("Unable to restart MIDI input thread for {:?}: {}", in_name, err) - }).ok(); - let midi_out = MidiOutPort::new_for_name(out_name.as_str()) - .map_err(|err| { - error!("Unable to restart MIDI output thread for {:?}: {}", out_name, err) - }).ok(); - let midi_channel_num = state.midi_channel_num; - let quirks = state.config.map(|c| c.midi_quirks).unwrap(); - let midi_in = midi_in.map(box_midi_in); - let midi_out = midi_out.map(box_midi_out); - midi_in_out_start(&mut state, midi_in, midi_out, midi_channel_num, - quirks, false); - } + let names = state.midi_in_name.as_ref().zip(state.midi_out_name.as_ref()); + let is_usb = state.midi_is_usb; + let (midi_in, midi_out) = names + .and_then(|(midi_in, midi_out)| { + match open(&midi_in, &midi_out, is_usb) { + Ok(v) => { Some(v) }, + Err(err) => { + error!("Failed to restart MIDI after settings dialog canceled: {}", err); + None + } + } + }).unzip(); + let midi_channel_num = state.midi_channel_num; + let quirks = state.config.map(|c| c.midi_quirks).unwrap(); + midi_in_out_start(&mut state, midi_in, midi_out, midi_channel_num, + quirks, false); } } diff --git a/gui/src/usb.rs b/gui/src/usb.rs new file mode 100644 index 0000000..8cf0f40 --- /dev/null +++ b/gui/src/usb.rs @@ -0,0 +1,55 @@ + +#[cfg(feature = "usb")] +pub use imp::*; + +#[cfg(not(feature = "usb"))] +pub use nop::*; + +#[cfg(feature = "usb")] +mod imp { + use anyhow::*; + use futures::executor; + use pod_core::midi_io; + use pod_core::midi_io::{box_midi_in, box_midi_out, BoxedMidiIn, BoxedMidiOut, MidiIn, MidiOut}; + use pod_core::model::Config; + + pub fn start_usb() { + pod_usb::usb_start().unwrap(); + executor::block_on( + pod_usb::usb_init_wait() + ); + } + + pub fn usb_list_devices() -> Vec { + pod_usb::usb_list_devices() + } + + pub fn usb_open_addr(addr: &str) -> Result<(impl MidiIn, impl MidiOut)> { + pod_usb::usb_device_for_address(addr) + } + + pub fn usb_open_name(name: &str) -> Result<(impl MidiIn, impl MidiOut)> { + pod_usb::usb_device_for_name(name) + } +} + +mod nop { + use anyhow::*; + use pod_core::midi_io::{BoxedMidiIn, BoxedMidiOut, MidiInPort, MidiOutPort}; + use pod_core::model::Config; + + fn start_usb() { + } + + fn usb_list_devices() -> Vec { + vec![] + } + + fn usb_open_addr(_addr: &str) -> Result<(MidiInPort, MidiOutPort)> { + unimplemented!() + } + + fn usb_open_name(_addr: &str) -> Result<(MidiInPort, MidiOutPort)> { + unimplemented!() + } +} diff --git a/usb/src/dev_handler.rs b/usb/src/dev_handler.rs index 3415e73..e1960ef 100644 --- a/usb/src/dev_handler.rs +++ b/usb/src/dev_handler.rs @@ -131,7 +131,6 @@ impl DeviceInner { error!("Transfer type {:?} not supported!", tt); break; } - }; match res { Ok(len) => { diff --git a/usb/src/lib.rs b/usb/src/lib.rs index 0b913e8..d4bb29a 100644 --- a/usb/src/lib.rs +++ b/usb/src/lib.rs @@ -226,4 +226,17 @@ pub fn usb_device_for_address(dev_addr: &str) -> Result<(impl MidiIn, impl MidiO }; dev.open() -} \ No newline at end of file +} + +pub fn usb_device_for_name(dev_name: &str) -> Result<(impl MidiIn, impl MidiOut)> { + let mut devices = DEVICES.lock().unwrap(); + + let mut found = devices.values_mut().find(|dev| { + dev.name == dev_name + }); + let Some(dev) = found.take() else { + bail!("USB device for name {:?} not found!", dev_name); + }; + + dev.open() +} From 4fb856e98e67f464bd80fd27250780ffe72aebc0 Mon Sep 17 00:00:00 2001 From: Artem Egorkine Date: Tue, 30 Jul 2024 00:52:26 +0300 Subject: [PATCH 5/6] USB: fix read thread closing, fix "MIDI is usb" tracking in state * USB: DeviceInner: added a `closed` atomic bool to signal that the device is closed, the read thread reads for 500ms at a time while checking the flag. `closed` flag gets set when `DeviceInner` is dropped; * USB: assume hotplug thread never closes. Hotplug dies only if `handle_events` errored out; --- gtk/src/prelude.rs | 2 +- gui/src/autodetect.rs | 10 +++-- gui/src/main.rs | 8 ++-- gui/src/settings.rs | 4 +- usb/src/dev_handler.rs | 95 ++++++++++++++++++++++++++---------------- usb/src/lib.rs | 25 +++++++---- 6 files changed, 88 insertions(+), 56 deletions(-) diff --git a/gtk/src/prelude.rs b/gtk/src/prelude.rs index 55ae452..4215b15 100644 --- a/gtk/src/prelude.rs +++ b/gtk/src/prelude.rs @@ -6,7 +6,7 @@ pub use gtk::prelude::*; pub use gdk::prelude::*; pub use gtk::gio; -pub use gtk::gio::prelude::*; +//pub use gtk::gio::prelude::*; pub use crate::*; diff --git a/gui/src/autodetect.rs b/gui/src/autodetect.rs index 126f7e7..7b9997d 100644 --- a/gui/src/autodetect.rs +++ b/gui/src/autodetect.rs @@ -111,19 +111,21 @@ pub fn detect(state: Arc>, opts: Opts, window: >k::Window) -> Res // autodetect device pod_core::midi_io::autodetect(midi_channel).await }; + let res = res.and_then(|(midi_in, midi_out, chan, config)| + Ok((midi_in, midi_out, chan, false, config))); tx.send(res).ok(); }); } else { // manually configured device let (midi_in, midi_out) = ports.unwrap(); - tx.send(Ok((midi_in, midi_out, midi_channel_u8, config.unwrap()))).ok(); + tx.send(Ok((midi_in, midi_out, midi_channel_u8, false, config.unwrap()))).ok(); } rx.attach(None, move |autodetect| { match autodetect { - Ok((midi_in, midi_out, midi_channel, config)) => { + Ok((midi_in, midi_out, midi_channel, is_usb, config)) => { set_midi_in_out(&mut state.lock().unwrap(), - Some(midi_in), Some(midi_out), midi_channel, Some(config)); + Some(midi_in), Some(midi_out), midi_channel, is_usb, Some(config)); } Err(e) => { error!("MIDI autodetect failed: {}", e); @@ -148,7 +150,7 @@ pub fn detect(state: Arc>, opts: Opts, window: >k::Window) -> Res .and_then(|str| config_for_str(&str).ok()) .or_else(|| configs().iter().next()); set_midi_in_out(&mut state.lock().unwrap(), - None, None, midi_channel_u8, config); + None, None, midi_channel_u8, false, config); } }; diff --git a/gui/src/main.rs b/gui/src/main.rs index b370cdf..aedc917 100644 --- a/gui/src/main.rs +++ b/gui/src/main.rs @@ -150,7 +150,7 @@ pub fn midi_in_out_stop(state: &mut State) -> JoinAll> { pub fn midi_in_out_start(state: &mut State, midi_in: Option, midi_out: Option, - midi_channel: u8, quirks: MidiQuirks, + midi_channel: u8, midi_is_usb: bool, quirks: MidiQuirks, config_changed: bool) { @@ -172,6 +172,7 @@ pub fn midi_in_out_start(state: &mut State, state.midi_out_name = None; state.midi_out_cancel = None; state.midi_channel_num = midi_channel; + state.midi_is_usb = false; notify(state); return; } @@ -189,6 +190,7 @@ pub fn midi_in_out_start(state: &mut State, state.midi_out_cancel = Some(out_cancel_tx); state.midi_channel_num = midi_channel; + state.midi_is_usb = midi_is_usb; notify(state); @@ -280,7 +282,7 @@ pub fn midi_in_out_start(state: &mut State, } pub fn set_midi_in_out(state: &mut State, midi_in: Option, midi_out: Option, - midi_channel: u8, config: Option<&'static Config>) -> bool + midi_channel: u8, midi_is_usb: bool, config: Option<&'static Config>) -> bool { if state.midi_in_cancel.is_some() || state.midi_out_cancel.is_some() { error!("Midi still running when entering send_midi_in_out"); @@ -306,7 +308,7 @@ pub fn set_midi_in_out(state: &mut State, midi_in: Option, midi_out let quirks = state.config.map(|c| c.midi_quirks) .unwrap_or_else(|| MidiQuirks::empty()); - midi_in_out_start(state, midi_in, midi_out, midi_channel, quirks, config_changed); + midi_in_out_start(state, midi_in, midi_out, midi_channel, midi_is_usb, quirks, config_changed); config_changed } diff --git a/gui/src/settings.rs b/gui/src/settings.rs index 911544c..bb7b508 100644 --- a/gui/src/settings.rs +++ b/gui/src/settings.rs @@ -505,7 +505,7 @@ pub fn create_settings_action(state: Arc>, ui: >k::Builder) -> gi } }) .unzip(); - set_midi_in_out(&mut state.lock().unwrap(), midi_in, midi_out, midi_channel, config); + set_midi_in_out(&mut state.lock().unwrap(), midi_in, midi_out, midi_channel, is_usb, config); } _ => { let mut state = state.lock().unwrap(); @@ -524,7 +524,7 @@ pub fn create_settings_action(state: Arc>, ui: >k::Builder) -> gi let midi_channel_num = state.midi_channel_num; let quirks = state.config.map(|c| c.midi_quirks).unwrap(); midi_in_out_start(&mut state, midi_in, midi_out, midi_channel_num, - quirks, false); + is_usb, quirks, false); } } diff --git a/usb/src/dev_handler.rs b/usb/src/dev_handler.rs index e1960ef..0c2a6f9 100644 --- a/usb/src/dev_handler.rs +++ b/usb/src/dev_handler.rs @@ -1,9 +1,10 @@ use anyhow::*; use core::result::Result::Ok; use std::sync::{Arc, Weak}; +use std::sync::atomic::{AtomicBool, Ordering}; use std::time::Duration; use async_trait::async_trait; -use log::{error, info, trace}; +use log::{debug, error, info, trace}; use rusb::{DeviceHandle, Direction, Error, TransferType, UsbContext}; use tokio::sync::mpsc; use pod_core::midi_io::{MidiIn, MidiOut}; @@ -24,6 +25,7 @@ pub struct DeviceInner { name: String, handle: Arc>, write_ep: Endpoint, + closed: Arc, } pub struct DeviceInput { @@ -43,6 +45,7 @@ pub struct DevHandler { rx: mpsc::UnboundedReceiver> } +const READ_DURATION: Duration = Duration::from_millis(500); impl Device { pub fn new(handle: DeviceHandle, usb_dev: &UsbDevice) -> Result { @@ -105,7 +108,6 @@ impl DeviceInner { fn new(name: String, handle: Arc>, read_ep: Endpoint, write_ep: Endpoint, tx: mpsc::UnboundedSender>) -> Self { - let handle_ret = handle.clone(); let has_kernel_driver = match handle.kernel_driver_active(read_ep.iface) { Ok(true) => { handle.detach_kernel_driver(read_ep.iface).ok(); @@ -116,47 +118,62 @@ impl DeviceInner { configure_endpoint(&handle, &read_ep).ok(); + let closed = Arc::new(AtomicBool::new(false)); + // libusb's reads DEFINITELY need to go on the blocking tasks queue - tokio::task::spawn_blocking(move || { - let mut buf = [0u8; 1024]; - loop { - let res = match read_ep.transfer_type { - TransferType::Bulk => { - handle.read_bulk(read_ep.address, &mut buf, Duration::MAX) - } - TransferType::Interrupt => { - handle.read_interrupt(read_ep.address, &mut buf, Duration::MAX) - } - tt => { - error!("Transfer type {:?} not supported!", tt); - break; - } - }; - match res { - Ok(len) => { - let b = buf.chunks(len).next().unwrap(); - trace!("<< {:02x?} len={}", &b, len); - match tx.send(b.to_vec()) { - Ok(_) => {} - Err(e) => { - error!("USB read thread tx failed: {}", e); + tokio::task::spawn_blocking({ + let name = name.clone(); + let handle = handle.clone(); + let closed = closed.clone(); + + move || { + debug!("USB read thread {:?} start", name); + + let mut buf = [0u8; 1024]; + while !closed.load(Ordering::Relaxed) { + let res = match read_ep.transfer_type { + TransferType::Bulk => { + handle.read_bulk(read_ep.address, &mut buf, READ_DURATION) + } + TransferType::Interrupt => { + handle.read_interrupt(read_ep.address, &mut buf, READ_DURATION) + } + tt => { + error!("Transfer type {:?} not supported!", tt); + break; + } + }; + match res { + Ok(len) => { + let b = buf.chunks(len).next().unwrap(); + trace!("<< {:02x?} len={}", &b, len); + match tx.send(b.to_vec()) { + Ok(_) => {} + Err(e) => { + error!("USB read thread tx failed: {}", e); + } + }; + } + Err(e) => { + match e { + Error::Busy | Error::Timeout | Error::Overflow => { continue } + _ => { + error!("USB read failed: {}", e); + break + } } - }; - } - Err(e) => { - error!("USB read failed: {}", e); - match e { - Error::Busy | Error::Timeout | Error::Overflow => { continue } - _ => { break } } } } + + debug!("USB read thread {:?} finish", name); } }); DeviceInner { name, - handle: handle_ret, + handle, + closed, write_ep } } @@ -178,14 +195,16 @@ impl DeviceInner { res.map(|_| ()).map_err(|e| anyhow!("USB write failed: {}", e)) } + + fn close(&self) { + self.closed.store(true, Ordering::Relaxed); + } } impl Drop for DeviceInner { fn drop(&mut self) { - // TODO: we consider that there is ever only one device, so interrupting - // the handle_events will only affect one DeviceInner... Can do better - // using own explicit context for each DeviceInner - self.handle.context().interrupt_handle_events(); + debug!("DeviceInner for {:?} dropped", &self.name); + self.close(); } } @@ -200,6 +219,7 @@ impl MidiIn for DeviceInput { } fn close(&mut self) { + debug!("midi in close"); } } @@ -214,5 +234,6 @@ impl MidiOut for DeviceOutput { } fn close(&mut self) { + debug!("midi out close"); } } \ No newline at end of file diff --git a/usb/src/lib.rs b/usb/src/lib.rs index d4bb29a..fd13007 100644 --- a/usb/src/lib.rs +++ b/usb/src/lib.rs @@ -5,7 +5,7 @@ mod dev_handler; mod endpoint; mod util; -use log::{debug, error, info}; +use log::{debug, error, info, trace}; use anyhow::*; use anyhow::Context as _; use core::result::Result::Ok; @@ -50,9 +50,9 @@ impl Hotplug for HotplugHandler { fn device_arrived(&mut self, device: UsbDevice) { let Ok(desc) = device.device_descriptor() else { return }; - debug!("device added: {:?} ??", device); + trace!("device arrived: {:?}", device); if find_device(desc.vendor_id(), desc.product_id()).is_some() { - debug!("device added: {:?}", device); + trace!("device added: {:?}", device); let e = DeviceAddedEvent { vid: desc.vendor_id(), pid: desc.product_id(), @@ -68,8 +68,9 @@ impl Hotplug for HotplugHandler { fn device_left(&mut self, device: UsbDevice) { let Ok(desc) = device.device_descriptor() else { return }; + trace!("device left: {:?}", device); if find_device(desc.vendor_id(), desc.product_id()).is_some() { - debug!("device removed: {:?}", device); + trace!("device removed: {:?}", device); let e = DeviceRemovedEvent { vid: desc.vendor_id(), pid: desc.product_id(), @@ -112,18 +113,24 @@ pub fn usb_start() -> Result<()> { info!("USB hotplug thread start"); let mut reg = Some(hotplug); loop { - ctx.handle_events(None).unwrap(); - if let Some(reg) = reg.take() { - ctx.unregister_callback(reg); + match ctx.handle_events(None) { + Ok(_) => {} + Err(e) => { + error!("Error in USB hotplug thread: {}", e); + break; + } } } + if let Some(reg) = reg.take() { + ctx.unregister_callback(reg); + } info!("USB hotplug thread finish"); }); let devices = DEVICES.clone(); tokio::spawn(async move { - info!("USB message RX thread start"); + info!("USB event RX thread start"); loop { let msg = match event_rx.recv().await { Ok(msg) => { msg } @@ -162,7 +169,7 @@ pub fn usb_start() -> Result<()> { } } } - info!("USB message RX thread finish"); + info!("USB event RX thread finish"); }); Ok(()) From 924169e59084a165c569158f77d4e251c8152630 Mon Sep 17 00:00:00 2001 From: Artem Egorkine Date: Tue, 30 Jul 2024 12:40:27 +0300 Subject: [PATCH 6/6] USB: add de-framing for sysex messages, 0xf2-workaround to PODxt --- usb/src/dev_handler.rs | 83 +++++++++++++++++++++++++++++++++++++----- usb/src/lib.rs | 10 ++++- 2 files changed, 82 insertions(+), 11 deletions(-) diff --git a/usb/src/dev_handler.rs b/usb/src/dev_handler.rs index 0c2a6f9..3fe7ddb 100644 --- a/usb/src/dev_handler.rs +++ b/usb/src/dev_handler.rs @@ -46,6 +46,7 @@ pub struct DevHandler { } const READ_DURATION: Duration = Duration::from_millis(500); +const WRITE_DURATION: Duration = Duration::from_millis(1000); impl Device { pub fn new(handle: DeviceHandle, usb_dev: &UsbDevice) -> Result { @@ -130,13 +131,31 @@ impl DeviceInner { debug!("USB read thread {:?} start", name); let mut buf = [0u8; 1024]; + let buf_ptr = buf.as_ptr(); + let mut read_ptr: &mut [u8] = &mut []; + let mut sysex = false; + + let mut reset_read_ptr = || unsafe { + std::slice::from_raw_parts_mut( + buf.as_mut_ptr(), + buf.len() + ) + }; + let mut advance_read_ptr = |len: usize| unsafe { + std::slice::from_raw_parts_mut( + read_ptr.as_mut_ptr().add(len), + read_ptr.len().checked_sub(len).unwrap_or(0) + ) + }; + read_ptr = reset_read_ptr(); + while !closed.load(Ordering::Relaxed) { let res = match read_ep.transfer_type { TransferType::Bulk => { - handle.read_bulk(read_ep.address, &mut buf, READ_DURATION) + handle.read_bulk(read_ep.address, &mut read_ptr, READ_DURATION) } TransferType::Interrupt => { - handle.read_interrupt(read_ep.address, &mut buf, READ_DURATION) + handle.read_interrupt(read_ep.address, &mut read_ptr, READ_DURATION) } tt => { error!("Transfer type {:?} not supported!", tt); @@ -145,8 +164,40 @@ impl DeviceInner { }; match res { Ok(len) => { - let b = buf.chunks(len).next().unwrap(); - trace!("<< {:02x?} len={}", &b, len); + if len == 0 { continue; } // does this ever happen? + let start_read = read_ptr.as_ptr() == buf_ptr; + if start_read { + // correct PODxt lower nibble 0010 in command byte, see + // https://github.com/torvalds/linux/blob/8508fa2e7472f673edbeedf1b1d2b7a6bb898ecc/sound/usb/line6/midibuf.c#L148 + if read_ptr[0] == 0xb2 || read_ptr[0] == 0xc2 || read_ptr[0] == 0xf2 { + read_ptr[0] = read_ptr[0] & 0xf0; + } + + sysex = read_ptr[0] == 0xf0; + } + let mut b = read_ptr.chunks(len).next().unwrap(); + let sysex_done = sysex && b[b.len() - 1] == 0xf7; + let mark = match (start_read, sysex, sysex_done) { + (true, true, false) => &"<<-", + (false, true, false) => &"<--", + (false, true, true) => &"-<<", + _ => "<<" + }; + trace!("{} {:02x?} len={}", mark, &b, len); + + if sysex { + if !sysex_done { + // advance read_ptr + read_ptr = advance_read_ptr(len); + continue; + } + if !start_read { + // return full buffer + let len = read_ptr.as_ptr() as u64 - buf_ptr as u64 + len as u64; + b = buf.chunks(len as usize).next().unwrap(); + } + } + match tx.send(b.to_vec()) { Ok(_) => {} Err(e) => { @@ -166,6 +217,11 @@ impl DeviceInner { } } + handle.release_interface(read_ep.iface).ok(); + if has_kernel_driver { + handle.attach_kernel_driver(read_ep.iface).ok(); + } + debug!("USB read thread {:?} finish", name); } }); @@ -179,15 +235,22 @@ impl DeviceInner { } fn send(&self, bytes: &[u8]) -> Result<()> { + if self.closed.load(Ordering::Relaxed) { + bail!("Device already closed"); + } + trace!(">> {:02x?} len={}", bytes, bytes.len()); + // TODO: this write will stall the executioner for the max + // WRITE_DURATION if something goes wrong. Instead, + // this should go through a channel to a `tokio::task::spawn_blocking` + // TX thread similar to how the RX thread does libusb polling... let res = match self.write_ep.transfer_type { TransferType::Bulk => { - self.handle.write_bulk(self.write_ep.address, bytes, Duration::MAX) + self.handle.write_bulk(self.write_ep.address, bytes, WRITE_DURATION) } - /* TransferType::Interrupt => { - self.handle.write_bulk(self.write_ep.address, buf, Duration::MAX) - }*/ + self.handle.write_interrupt(self.write_ep.address, bytes, WRITE_DURATION) + } tt => { bail!("Transfer type {:?} not supported!", tt); } @@ -219,7 +282,7 @@ impl MidiIn for DeviceInput { } fn close(&mut self) { - debug!("midi in close"); + debug!("midi in close - nop"); } } @@ -234,6 +297,6 @@ impl MidiOut for DeviceOutput { } fn close(&mut self) { - debug!("midi out close"); + debug!("midi out close - nop"); } } \ No newline at end of file diff --git a/usb/src/lib.rs b/usb/src/lib.rs index fd13007..a1e3603 100644 --- a/usb/src/lib.rs +++ b/usb/src/lib.rs @@ -147,7 +147,15 @@ pub fn usb_start() -> Result<()> { match msg { UsbEvent::DeviceAdded(DeviceAddedEvent{ vid, pid, bus, address }) => { let usb_dev = find_device(vid, pid).unwrap(); - //let Some(h) = rusb::open_device_with_vid_pid(vid, pid) else { continue }; + /* + let device_list = rusb::devices().unwrap(); + let dev = device_list.iter().find(|dev| { + let desc = dev.device_descriptor().unwrap(); + desc.vendor_id() == vid && desc.product_id() == pid + }).map(|dev| dev.open().unwrap()); + let Some(h) = dev; + */ + let Some(h) = rusb::open_device_with_vid_pid(vid, pid) else { continue }; let handler = match Device::new(h, usb_dev) { Ok(h) => { h }