From f28865a35be7067820493c0a32ff9e4bd052815e Mon Sep 17 00:00:00 2001 From: Martin Ling Date: Wed, 15 May 2024 03:10:07 +0100 Subject: [PATCH 1/6] Remove superfluous closure. --- src/ui.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui.rs b/src/ui.rs index adb033dd..09dbc0f4 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -146,7 +146,7 @@ impl DeviceSelector { 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() + device.speeds.iter().map(Speed::description).collect() ) } let no_speeds = vec![]; From 3a7ae728e1555e2db39f32e9335303b83a823df0 Mon Sep 17 00:00:00 2001 From: Martin Ling Date: Wed, 15 May 2024 03:11:53 +0100 Subject: [PATCH 2/6] DeviceSelector::scan() no longer needs to return a boolean. --- src/ui.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/ui.rs b/src/ui.rs index 09dbc0f4..baa3e632 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -139,7 +139,7 @@ impl DeviceSelector { self.speed_dropdown.set_sensitive(sensitive); } - fn scan(&mut self) -> Result { + fn scan(&mut self) -> Result<(), Error> { self.devices = CynthionDevice::scan()?; self.dev_strings = Vec::with_capacity(self.devices.len()); self.dev_speeds = Vec::with_capacity(self.devices.len()); @@ -153,9 +153,8 @@ impl DeviceSelector { 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.set_sensitive(self.device_available()); + Ok(()) } fn open(&self) -> Result<(CynthionHandle, Speed), Error> { From c25e48d4f49680a5be5278a99604847658c37e68 Mon Sep 17 00:00:00 2001 From: Martin Ling Date: Wed, 15 May 2024 03:15:04 +0100 Subject: [PATCH 3/6] Update available speeds when device selection changes. --- src/ui.rs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/ui.rs b/src/ui.rs index baa3e632..b401a526 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, @@ -98,6 +98,7 @@ struct DeviceSelector { dev_speeds: Vec>, dev_dropdown: DropDown, speed_dropdown: DropDown, + change_handler: Option, container: gtk::Box, } @@ -109,6 +110,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() @@ -140,6 +142,9 @@ impl DeviceSelector { } 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()); @@ -154,9 +159,19 @@ impl DeviceSelector { self.replace_dropdown(&self.dev_dropdown, &self.dev_strings); self.replace_dropdown(&self.speed_dropdown, speed_strings); self.set_sensitive(self.device_available()); + 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]; @@ -778,6 +793,13 @@ fn detect_hardware() -> Result<(), Error> { }) } +fn device_selection_changed() -> Result<(), Error> { + with_ui(|ui| { + ui.selector.update_speeds(); + Ok(()) + }) +} + pub fn start_cynthion() -> Result<(), Error> { let writer = reset_capture()?; with_ui(|ui| { From d0a9b22a360577b0cf58f54929a59a4cbdb52443 Mon Sep 17 00:00:00 2001 From: Martin Ling Date: Wed, 15 May 2024 03:23:33 +0100 Subject: [PATCH 4/6] List devices as "Cynthion", disambiguating if necessary. --- src/backend/cynthion.rs | 11 +---------- src/ui.rs | 20 +++++++++++++++++--- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/backend/cynthion.rs b/src/backend/cynthion.rs index 76801c63..699ea6f0 100644 --- a/src/backend/cynthion.rs +++ b/src/backend/cynthion.rs @@ -82,8 +82,7 @@ impl State { /// A Cynthion device attached to the system. pub struct CynthionDevice { - device_info: DeviceInfo, - pub description: String, + pub device_info: DeviceInfo, pub speeds: Vec, } @@ -113,18 +112,10 @@ impl CynthionDevice { 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, }) } diff --git a/src/ui.rs b/src/ui.rs index b401a526..96be85c2 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -146,10 +146,24 @@ impl DeviceSelector { 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_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()) + } + } + ); self.dev_speeds.push( device.speeds.iter().map(Speed::description).collect() ) From 2173663fd34f4372aa400b58a5488f2de6fd316e Mon Sep 17 00:00:00 2001 From: Martin Ling Date: Wed, 15 May 2024 03:37:06 +0100 Subject: [PATCH 5/6] Show devices that are not usable in the list, and keep track of why. --- src/backend/cynthion.rs | 87 +++++++++++++++++++++++++++++------------ src/ui.rs | 72 +++++++++++++++++++++++++--------- 2 files changed, 117 insertions(+), 42 deletions(-) diff --git a/src/backend/cynthion.rs b/src/backend/cynthion.rs index 699ea6f0..1e14a004 100644 --- a/src/backend/cynthion.rs +++ b/src/backend/cynthion.rs @@ -80,10 +80,20 @@ impl State { } } +/// Whether a Cynthion device is ready for use as an analyzer. +pub enum CynthionUsability { + /// Device is usable at supported speeds. + Usable(Vec), + /// Device not usable, with a string explaining why. + Unusable(String), +} + +use CynthionUsability::*; + /// A Cynthion device attached to the system. pub struct CynthionDevice { pub device_info: DeviceInfo, - pub speeds: Vec, + pub usability: CynthionUsability, } /// A handle to an open Cynthion device. @@ -101,39 +111,68 @@ pub struct CynthionStop { worker: JoinHandle::<()>, } +/// Check whether a Cynthion device is usable as an analyzer. +fn check_device(device_info: &DeviceInfo) -> Result, Error> +{ + // Check version is correct. + let version = device_info.device_version(); + if !(MIN_SUPPORTED..=NOT_SUPPORTED).contains(&version) { + bail!("Device version not supported"); + } + + // Check we can open the device. + let device = device_info + .open() + .context("Failed to open device")?; + + // Try to claim the interface. + let interface = device + .claim_interface(0) + .context("Failed to claim interface")?; + + // 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(speeds); +} + 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 - { - let version = device_info.device_version(); - if !(MIN_SUPPORTED..=NOT_SUPPORTED).contains(&version) { - continue; + 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(speeds) => CynthionDevice { + device_info, + usability: Usable(speeds) + }, + Err(err) => CynthionDevice { + device_info, + usability: Unusable(format!("{}", err)) + } } - let handle = CynthionHandle::new(&device_info)?; - let speeds = handle.speeds()?; - result.push(CynthionDevice{ - device_info, - speeds, - }) - } - } - Ok(result) + ) + .collect()) } pub fn open(&self) -> Result { - CynthionHandle::new(&self.device_info) + match &self.usability { + Usable(..) => { + let device = self.device_info.open()?; + let interface = device.claim_interface(0)?; + 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::*; diff --git a/src/ui.rs b/src/ui.rs index 96be85c2..00ff3993 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -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, @@ -132,13 +138,32 @@ 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<(), Error> { @@ -164,15 +189,20 @@ impl DeviceSelector { } } ); - self.dev_speeds.push( - device.speeds.iter().map(Speed::description).collect() - ) + 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); - self.set_sensitive(self.device_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()))); @@ -189,10 +219,17 @@ impl DeviceSelector { 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>( @@ -775,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(()) }) ); @@ -809,6 +845,7 @@ 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(()) }) @@ -844,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(()) }) ); From 11bea09bc4f91a0360ca1649c5f74788201c69b9 Mon Sep 17 00:00:00 2001 From: Martin Ling Date: Wed, 15 May 2024 03:41:08 +0100 Subject: [PATCH 6/6] Update to new analyzer USB API. --- src/backend/cynthion.rs | 110 +++++++++++++++++++++++++++++----------- src/ui.rs | 4 +- 2 files changed, 81 insertions(+), 33 deletions(-) diff --git a/src/backend/cynthion.rs b/src/backend/cynthion.rs index 1e14a004..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,10 +81,15 @@ 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 at supported speeds. - Usable(Vec), + /// Device is usable via the given interface, at supported speeds. + Usable(InterfaceSelection, Vec), /// Device not usable, with a string explaining why. Unusable(String), } @@ -111,33 +117,72 @@ pub struct CynthionStop { worker: JoinHandle::<()>, } -/// Check whether a Cynthion device is usable as an analyzer. -fn check_device(device_info: &DeviceInfo) -> Result, Error> +/// Check whether a Cynthion device has an accessible analyzer interface. +fn check_device(device_info: &DeviceInfo) + -> Result<(InterfaceSelection, Vec), Error> { - // Check version is correct. - let version = device_info.device_version(); - if !(MIN_SUPPORTED..=NOT_SUPPORTED).contains(&version) { - bail!("Device version not supported"); - } - // Check we can open the device. let device = device_info .open() .context("Failed to open device")?; - // Try to claim the interface. - let interface = device - .claim_interface(0) - .context("Failed to claim interface")?; + // 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 + { + 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")?; + // 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 + )) + } + } - // Now we have a usable device. - return Ok(speeds); + bail!("No supported analyzer interface found"); } impl CynthionDevice { @@ -147,9 +192,9 @@ impl CynthionDevice { .filter(|info| info.product_id() == PID) .map(|device_info| match check_device(&device_info) { - Ok(speeds) => CynthionDevice { + Ok((iface, speeds)) => CynthionDevice { device_info, - usability: Usable(speeds) + usability: Usable(iface, speeds) }, Err(err) => CynthionDevice { device_info, @@ -162,9 +207,12 @@ impl CynthionDevice { pub fn open(&self) -> Result { match &self.usability { - Usable(..) => { + Usable(iface, _) => { let device = self.device_info.open()?; - let interface = device.claim_interface(0)?; + 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), @@ -178,10 +226,10 @@ impl CynthionHandle { 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); @@ -285,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 00ff3993..a028cd0d 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -189,7 +189,7 @@ impl DeviceSelector { } } ); - if let Usable(speeds) = &device.usability { + if let Usable(_, speeds) = &device.usability { self.dev_speeds.push( speeds.iter().map(Speed::description).collect() ) @@ -220,7 +220,7 @@ impl DeviceSelector { let device_id = self.dev_dropdown.selected(); let device = &self.devices[device_id as usize]; match &device.usability { - Usable(speeds) => { + Usable(_, speeds) => { let speed_id = self.speed_dropdown.selected() as usize; let speed = speeds[speed_id]; let cynthion = device.open()?;