#![cfg_attr(feature = "strict", deny(warnings))]
mod common;
pub use common::UsbDevice;
use crossbeam::channel::{bounded, unbounded, Receiver, Sender};
use std::{collections::HashSet, thread, time::Duration};
#[cfg(target_os = "windows")]
mod windows;
#[cfg(target_os = "windows")]
use crate::windows::*;
#[cfg(target_os = "macos")]
mod macos;
#[cfg(target_os = "macos")]
use crate::macos::*;
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "linux")]
use crate::linux::*;
pub fn enumerate(vendor_id: Option<u16>, product_id: Option<u16>) -> Vec<UsbDevice> {
enumerate_platform(vendor_id, product_id)
}
#[derive(Debug, Clone)]
pub enum Event {
Initial(Vec<UsbDevice>),
Connect(UsbDevice),
Disconnect(UsbDevice),
}
#[derive(Clone)]
pub struct Subscription {
pub rx_event: Receiver<Event>,
_tx_close: Sender<()>,
}
#[derive(Debug, Clone)]
pub struct Observer {
poll_interval: u32,
vendor_id: Option<u16>,
product_id: Option<u16>,
}
impl Default for Observer {
fn default() -> Self {
Observer::new()
}
}
impl Observer {
pub fn new() -> Self {
Observer {
poll_interval: 1,
vendor_id: None,
product_id: None,
}
}
pub fn with_poll_interval(mut self, seconds: u32) -> Self {
self.poll_interval = seconds;
self
}
pub fn with_vendor_id(mut self, vendor_id: u16) -> Self {
self.vendor_id = Some(vendor_id);
self
}
pub fn with_product_id(mut self, product_id: u16) -> Self {
self.product_id = Some(product_id);
self
}
pub fn subscribe(&self) -> Subscription {
let (tx_event, rx_event) = unbounded();
let (tx_close, rx_close) = bounded::<()>(0);
thread::Builder::new()
.name("USB Enumeration Thread".to_string())
.spawn({
let this = self.clone();
move || {
let device_list = enumerate(this.vendor_id, this.product_id);
if tx_event.send(Event::Initial(device_list.clone())).is_err() {
return;
}
let mut device_list: HashSet<UsbDevice> = device_list.into_iter().collect();
let mut wait_seconds = this.poll_interval as f32;
loop {
while wait_seconds > 0.0 {
if let Err(crossbeam::channel::RecvTimeoutError::Disconnected) =
rx_close.recv_timeout(Duration::from_millis(250))
{
return;
}
wait_seconds -= 0.25;
}
wait_seconds = this.poll_interval as f32;
let next_devices: HashSet<UsbDevice> =
enumerate(this.vendor_id, this.product_id)
.into_iter()
.collect();
for device in &device_list {
if !next_devices.contains(device)
&& tx_event.send(Event::Disconnect(device.clone())).is_err()
{
return;
}
}
for device in &next_devices {
if !device_list.contains(device)
&& tx_event.send(Event::Connect(device.clone())).is_err()
{
return;
}
}
device_list = next_devices;
}
}
})
.expect("Could not spawn background thread");
Subscription {
rx_event,
_tx_close: tx_close,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_enumerate() {
let devices = enumerate(None, None);
println!("Enumerated devices: {:#?}", devices);
assert!(!devices.is_empty());
}
#[test]
fn test_subscribe() {
let subscription = Observer::new().subscribe();
let mut iter = subscription.rx_event.iter();
let initial = iter.next().expect("Should get an Event");
assert!(matches!(initial, Event::Initial(_)));
println!("Connect a USB device");
let connect_event = iter.next().expect("Should get an Event");
let connect_device = if let Event::Connect(device) = connect_event {
device
} else {
panic!("Expected Event::Connect. Actual: {:?}", connect_event);
};
println!("Disconnect that same device");
let disconnect_event = iter.next().expect("Should get an Event");
let disconnect_device = if let Event::Disconnect(device) = disconnect_event {
device
} else {
panic!("Expected Event::Disconnect. Actual: {:?}", disconnect_event);
};
assert_eq!(connect_device, disconnect_device);
}
}