diff --git a/src/backend/cynthion.rs b/src/backend/cynthion.rs index 76801c63..92ece8ef 100644 --- a/src/backend/cynthion.rs +++ b/src/backend/cynthion.rs @@ -24,8 +24,9 @@ use nusb::{ const VID: u16 = 0x1d50; const PID: u16 = 0x615b; -const MIN_SUPPORTED: u16 = 0x0002; -const NOT_SUPPORTED: u16 = 0x0003; +const CLASS: u8 = 0xff; +const SUBCLASS: u8 = 0x10; +const PROTOCOL: u8 = 0x00; const ENDPOINT: u8 = 0x81; @@ -80,11 +81,25 @@ impl State { } } +pub struct InterfaceSelection { + interface_number: u8, + alt_setting_number: u8, +} + +/// Whether a Cynthion device is ready for use as an analyzer. +pub enum CynthionUsability { + /// Device is usable via the given interface, at supported speeds. + Usable(InterfaceSelection, Vec), + /// Device not usable, with a string explaining why. + Unusable(String), +} + +use CynthionUsability::*; + /// A Cynthion device attached to the system. pub struct CynthionDevice { - device_info: DeviceInfo, - pub description: String, - pub speeds: Vec, + pub device_info: DeviceInfo, + pub usability: CynthionUsability, } /// A handle to an open Cynthion device. @@ -102,56 +117,119 @@ pub struct CynthionStop { worker: JoinHandle::<()>, } -impl CynthionDevice { - pub fn scan() -> Result, Error> { - let mut result = Vec::new(); - for device_info in nusb::list_devices()? { - if device_info.vendor_id() == VID && - device_info.product_id() == PID +/// Check whether a Cynthion device has an accessible analyzer interface. +fn check_device(device_info: &DeviceInfo) + -> Result<(InterfaceSelection, Vec), Error> +{ + // Check we can open the device. + let device = device_info + .open() + .context("Failed to open device")?; + + // Read the active configuration. + let config = device + .active_configuration() + .context("Failed to retrieve active configuration")?; + + // Iterate over the interfaces... + for interface in config.interfaces() { + let interface_number = interface.interface_number(); + + // ...and alternate settings... + for alt_setting in interface.alt_settings() { + let alt_setting_number = alt_setting.alternate_setting(); + + // Ignore if this is not our supported target. + if alt_setting.class() != CLASS || + alt_setting.subclass() != SUBCLASS { - let version = device_info.device_version(); - if !(MIN_SUPPORTED..=NOT_SUPPORTED).contains(&version) { - continue; - } - let manufacturer = device_info - .manufacturer_string() - .unwrap_or("Unknown"); - let product = device_info - .product_string() - .unwrap_or("Device"); - let description = format!("{} {}", manufacturer, product); - let handle = CynthionHandle::new(&device_info)?; - let speeds = handle.speeds()?; - result.push(CynthionDevice{ - device_info, - description, - speeds, - }) + continue; + } + + // Check protocol version. + let protocol = alt_setting.protocol(); + if protocol != PROTOCOL { + bail!("Wrong protocol version: {} supported, {} found", + PROTOCOL, protocol); + } + + // Try to claim the interface. + let interface = device + .claim_interface(interface_number) + .context("Failed to claim interface")?; + + // Select the required alternate, if not the default. + if alt_setting_number != 0 { + interface + .set_alt_setting(alt_setting_number) + .context("Failed to select alternate setting")?; } + + // Fetch the available speeds. + let handle = CynthionHandle { interface }; + let speeds = handle + .speeds() + .context("Failed to fetch available speeds")?; + + // Now we have a usable device. + return Ok(( + InterfaceSelection { + interface_number, + alt_setting_number, + }, + speeds + )) } - Ok(result) + } + + bail!("No supported analyzer interface found"); +} + +impl CynthionDevice { + pub fn scan() -> Result, Error> { + Ok(nusb::list_devices()? + .filter(|info| info.vendor_id() == VID) + .filter(|info| info.product_id() == PID) + .map(|device_info| + match check_device(&device_info) { + Ok((iface, speeds)) => CynthionDevice { + device_info, + usability: Usable(iface, speeds) + }, + Err(err) => CynthionDevice { + device_info, + usability: Unusable(format!("{}", err)) + } + } + ) + .collect()) } pub fn open(&self) -> Result { - CynthionHandle::new(&self.device_info) + match &self.usability { + Usable(iface, _) => { + let device = self.device_info.open()?; + let interface = device.claim_interface(iface.interface_number)?; + if iface.alt_setting_number != 0 { + interface.set_alt_setting(iface.alt_setting_number)?; + } + Ok(CynthionHandle { interface }) + }, + Unusable(reason) => bail!("Device not usable: {}", reason), + } } } impl CynthionHandle { - fn new(device_info: &DeviceInfo) -> Result { - let device = device_info.open()?; - let interface = device.claim_interface(0)?; - Ok(CynthionHandle { interface }) - } pub fn speeds(&self) -> Result, Error> { use Speed::*; let control = Control { control_type: ControlType::Vendor, - recipient: Recipient::Device, + recipient: Recipient::Interface, request: 2, value: 0, - index: 0, + index: self.interface.interface_number() as u16, }; let mut buf = [0; 64]; let timeout = Duration::from_secs(1); @@ -255,10 +333,10 @@ impl CynthionHandle { fn write_state(&mut self, state: State) -> Result<(), Error> { let control = Control { control_type: ControlType::Vendor, - recipient: Recipient::Device, + recipient: Recipient::Interface, request: 1, value: u16::from(state.0), - index: 0, + index: self.interface.interface_number() as u16, }; let data = &[]; let timeout = Duration::from_secs(1); diff --git a/src/ui.rs b/src/ui.rs index adb033dd..a028cd0d 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -13,7 +13,7 @@ use std::{io::Read, net::TcpListener}; use anyhow::{Context as ErrorContext, Error, bail}; use gtk::gio::ListModel; -use gtk::glib::Object; +use gtk::glib::{Object, SignalHandlerId}; use gtk::{ prelude::*, Align, @@ -47,7 +47,13 @@ use pcap_file::{ pcap::{PcapReader, PcapWriter, PcapHeader, RawPcapPacket}, }; -use crate::backend::cynthion::{CynthionDevice, CynthionHandle, CynthionStop, Speed}; +use crate::backend::cynthion::{ + CynthionDevice, + CynthionHandle, + CynthionStop, + CynthionUsability::*, + Speed}; + use crate::capture::{ create_capture, CaptureReader, @@ -98,6 +104,7 @@ struct DeviceSelector { dev_speeds: Vec>, dev_dropdown: DropDown, speed_dropdown: DropDown, + change_handler: Option, container: gtk::Box, } @@ -109,6 +116,7 @@ impl DeviceSelector { dev_speeds: vec![], dev_dropdown: DropDown::from_strings(&[]), speed_dropdown: DropDown::from_strings(&[]), + change_handler: None, container: gtk::Box::builder() .orientation(Orientation::Horizontal) .build() @@ -130,41 +138,98 @@ impl DeviceSelector { Ok(selector) } + fn current_device(&self) -> Option<&CynthionDevice> { + if self.devices.is_empty() { + None + } else { + Some(&self.devices[self.dev_dropdown.selected() as usize]) + } + } + fn device_available(&self) -> bool { - !self.devices.is_empty() + match self.current_device() { + None => false, + Some(device) => match device.usability { + Usable(..) => true, + Unusable(..) => false, + } + } } fn set_sensitive(&mut self, sensitive: bool) { - self.dev_dropdown.set_sensitive(sensitive); - self.speed_dropdown.set_sensitive(sensitive); + if sensitive { + self.dev_dropdown.set_sensitive(!self.devices.is_empty()); + self.speed_dropdown.set_sensitive(self.device_available()); + } else { + self.dev_dropdown.set_sensitive(false); + self.speed_dropdown.set_sensitive(false); + } } - fn scan(&mut self) -> Result { + fn scan(&mut self) -> Result<(), Error> { + if let Some(handler) = self.change_handler.take() { + self.dev_dropdown.disconnect(handler); + } self.devices = CynthionDevice::scan()?; - self.dev_strings = Vec::with_capacity(self.devices.len()); - self.dev_speeds = Vec::with_capacity(self.devices.len()); + let count = self.devices.len(); + self.dev_strings = Vec::with_capacity(count); + self.dev_speeds = Vec::with_capacity(count); for device in self.devices.iter() { - self.dev_strings.push(device.description.clone()); - self.dev_speeds.push( - device.speeds.iter().map(|x| x.description()).collect() - ) + self.dev_strings.push( + if count <= 1 { + String::from("Cynthion") + } else { + let info = &device.device_info; + if let Some(serial) = info.serial_number() { + format!("Cynthion #{}", serial) + } else { + format!("Cynthion (bus {}, device {})", + info.bus_number(), + info.device_address()) + } + } + ); + if let Usable(_, speeds) = &device.usability { + self.dev_speeds.push( + speeds.iter().map(Speed::description).collect() + ) + } else { + self.dev_speeds.push(vec![]); + } } let no_speeds = vec![]; let speed_strings = self.dev_speeds.first().unwrap_or(&no_speeds); self.replace_dropdown(&self.dev_dropdown, &self.dev_strings); self.replace_dropdown(&self.speed_dropdown, speed_strings); - let available = self.device_available(); - self.set_sensitive(available); - Ok(available) + self.dev_dropdown.set_sensitive(!self.devices.is_empty()); + self.speed_dropdown.set_sensitive(!speed_strings.is_empty()); + self.change_handler = Some( + self.dev_dropdown.connect_selected_notify( + |_| display_error(device_selection_changed()))); + Ok(()) + } + + fn update_speeds(&self) { + let index = self.dev_dropdown.selected() as usize; + let speed_strings = &self.dev_speeds[index]; + self.replace_dropdown(&self.speed_dropdown, speed_strings); + self.speed_dropdown.set_sensitive(!speed_strings.is_empty()); } fn open(&self) -> Result<(CynthionHandle, Speed), Error> { let device_id = self.dev_dropdown.selected(); let device = &self.devices[device_id as usize]; - let speed_id = self.speed_dropdown.selected() as usize; - let speed = device.speeds[speed_id]; - let cynthion = device.open()?; - Ok((cynthion, speed)) + match &device.usability { + Usable(_, speeds) => { + let speed_id = self.speed_dropdown.selected() as usize; + let speed = speeds[speed_id]; + let cynthion = device.open()?; + Ok((cynthion, speed)) + }, + Unusable(reason) => { + bail!("Device not usable: {}", reason) + } + } } fn replace_dropdown>( @@ -747,9 +812,8 @@ fn start_pcap(action: FileAction, path: PathBuf) -> Result<(), Error> { ui.open_button.set_sensitive(true); ui.save_button.set_sensitive(true); ui.scan_button.set_sensitive(true); - let available = ui.selector.device_available(); - ui.selector.set_sensitive(available); - ui.capture_button.set_sensitive(available); + ui.selector.set_sensitive(true); + ui.capture_button.set_sensitive(ui.selector.device_available()); Ok(()) }) ); @@ -779,6 +843,14 @@ fn detect_hardware() -> Result<(), Error> { }) } +fn device_selection_changed() -> Result<(), Error> { + with_ui(|ui| { + ui.capture_button.set_sensitive(ui.selector.device_available()); + ui.selector.update_speeds(); + Ok(()) + }) +} + pub fn start_cynthion() -> Result<(), Error> { let writer = reset_capture()?; with_ui(|ui| { @@ -809,9 +881,8 @@ pub fn start_cynthion() -> Result<(), Error> { ui.stop_button.disconnect(signal_id); ui.stop_button.set_sensitive(false); ui.open_button.set_sensitive(true); - let available = ui.selector.device_available(); - ui.selector.set_sensitive(available); - ui.capture_button.set_sensitive(available); + ui.selector.set_sensitive(true); + ui.capture_button.set_sensitive(ui.selector.device_available()); Ok(()) }) );