use crate::constants::find_known_device;
use crate::error::RtlSdrError;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct DeviceInfo {
pub index: u32,
pub name: String,
pub manufacturer: String,
pub product: String,
pub serial: String,
}
#[must_use]
pub fn list_devices() -> Vec<DeviceInfo> {
let count = get_device_count();
(0..count)
.map(|index| {
let name = get_device_name(index);
let (manufacturer, product, serial) = get_device_usb_strings(index)
.unwrap_or_else(|_| (String::new(), String::new(), String::new()));
DeviceInfo {
index,
name,
manufacturer,
product,
serial,
}
})
.collect()
}
pub fn get_device_count() -> u32 {
let mut count = 0u32;
match rusb::devices() {
Ok(devices) => {
for device in devices.iter() {
if let Ok(dd) = device.device_descriptor() {
if find_known_device(dd.vendor_id(), dd.product_id()).is_some() {
count += 1;
}
}
}
}
Err(e) => {
tracing::warn!("get_device_count: rusb::devices() failed ({e}); reporting 0 devices");
}
}
count
}
pub fn get_device_name(index: u32) -> String {
let mut count = 0u32;
match rusb::devices() {
Ok(devices) => {
for device in devices.iter() {
if let Ok(dd) = device.device_descriptor() {
if let Some(known) = find_known_device(dd.vendor_id(), dd.product_id()) {
if count == index {
return known.name.to_string();
}
count += 1;
}
}
}
}
Err(e) => {
tracing::warn!(
"get_device_name({index}): rusb::devices() failed ({e}); returning empty name"
);
}
}
String::new()
}
pub fn get_device_usb_strings(index: u32) -> Result<(String, String, String), RtlSdrError> {
let (device, dd) = find_device_by_index(index)?;
let handle = device.open()?;
let manufact = handle
.read_manufacturer_string_ascii(&dd)
.unwrap_or_default();
let product = handle.read_product_string_ascii(&dd).unwrap_or_default();
let serial = handle
.read_serial_number_string_ascii(&dd)
.unwrap_or_default();
Ok((manufact, product, serial))
}
pub fn get_index_by_serial(serial: &str) -> Result<u32, RtlSdrError> {
lookup_serial(serial, get_device_count(), get_device_usb_strings)
}
pub(crate) fn lookup_serial<F>(serial: &str, count: u32, lookup: F) -> Result<u32, RtlSdrError>
where
F: Fn(u32) -> Result<(String, String, String), RtlSdrError>,
{
for i in 0..count {
if let Ok((_, _, dev_serial)) = lookup(i) {
if dev_serial == serial {
return Ok(i);
}
}
}
Err(RtlSdrError::InvalidParameter(format!(
"no device with serial '{serial}'"
)))
}
pub(crate) fn find_device_by_index(
index: u32,
) -> Result<(rusb::Device<rusb::GlobalContext>, rusb::DeviceDescriptor), RtlSdrError> {
let devices = rusb::devices()?;
let mut count = 0u32;
for device in devices.iter() {
if let Ok(dd) = device.device_descriptor() {
if find_known_device(dd.vendor_id(), dd.product_id()).is_some() {
if count == index {
return Ok((device, dd));
}
count += 1;
}
}
}
Err(RtlSdrError::DeviceNotFound { index })
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::Cell;
#[test]
fn lookup_serial_count_zero_returns_invalid_parameter() {
let bogus_serial = "nonexistent_serial_for_test_6";
let lookup_called = Cell::new(false);
let result = lookup_serial(bogus_serial, 0, |_| {
lookup_called.set(true);
Ok((String::new(), String::new(), String::new()))
});
assert!(
!lookup_called.get(),
"lookup must not be called when count == 0",
);
assert!(
matches!(&result, Err(RtlSdrError::InvalidParameter(msg)) if msg.contains(bogus_serial)),
"expected InvalidParameter containing serial, got {result:?}",
);
}
#[test]
fn lookup_serial_finds_match_at_index() {
let result = lookup_serial("wanted", 3, |i| {
Ok((
String::new(),
String::new(),
match i {
0 => "other_a".to_string(),
1 => "wanted".to_string(),
_ => "other_b".to_string(),
},
))
});
assert_eq!(result.ok(), Some(1));
}
#[test]
fn lookup_serial_no_match_returns_invalid_parameter() {
let bogus_serial = "wanted";
let result = lookup_serial(bogus_serial, 2, |_| {
Ok((String::new(), String::new(), "nope".to_string()))
});
assert!(
matches!(&result, Err(RtlSdrError::InvalidParameter(msg)) if msg.contains(bogus_serial)),
"expected InvalidParameter containing serial, got {result:?}",
);
}
#[test]
fn lookup_serial_skips_lookup_errors_and_continues() {
let result = lookup_serial("wanted", 3, |i| {
if i == 1 {
Err(RtlSdrError::Usb(rusb::Error::Access))
} else {
Ok((
String::new(),
String::new(),
if i == 2 {
"wanted".to_string()
} else {
"other".to_string()
},
))
}
});
assert_eq!(result.ok(), Some(2));
}
}