166 lines
5.3 KiB
Rust
166 lines
5.3 KiB
Rust
fn try_connect_summary(
|
|
summary: DeviceSummary,
|
|
logs: std::sync::Arc<Mutex<Vec<DebugLogEntry>>>,
|
|
) -> Result<(ConnectedDevice, DeviceState), String> {
|
|
let file = OpenOptions::new()
|
|
.read(true)
|
|
.write(true)
|
|
.open(&summary.path)
|
|
.map_err(|err| format!("Could not open. Check hidraw permissions or udev rules: {err}"))?;
|
|
|
|
let mut device = ConnectedDevice { file, summary, logs };
|
|
let snapshot = device.snapshot("direct")?;
|
|
Ok((device, snapshot))
|
|
}
|
|
|
|
fn connection_candidates(
|
|
discovered: &[DeviceSummary],
|
|
selected: &DeviceSummary,
|
|
) -> Vec<DeviceSummary> {
|
|
let mut candidates: Vec<DeviceSummary> = discovered
|
|
.iter()
|
|
.filter(|device| {
|
|
device.vendor_id == selected.vendor_id
|
|
&& device.product_id == selected.product_id
|
|
&& device.probe_group == selected.probe_group
|
|
})
|
|
.cloned()
|
|
.collect();
|
|
|
|
candidates.sort_by_key(|device| {
|
|
(
|
|
device.path != selected.path,
|
|
interface_probe_priority(device.interface_number),
|
|
device.path.clone(),
|
|
)
|
|
});
|
|
candidates
|
|
}
|
|
|
|
fn interface_probe_priority(interface_number: Option<u8>) -> u8 {
|
|
match interface_number {
|
|
Some(3) => 0,
|
|
Some(2) => 1,
|
|
Some(1) => 2,
|
|
Some(0) => 3,
|
|
Some(other) => 4 + other,
|
|
None => u8::MAX,
|
|
}
|
|
}
|
|
|
|
fn discover_devices() -> io::Result<Vec<DeviceSummary>> {
|
|
let mut devices = Vec::new();
|
|
let hidraw_root = Path::new("/sys/class/hidraw");
|
|
if !hidraw_root.exists() {
|
|
return Ok(devices);
|
|
}
|
|
|
|
for entry in fs::read_dir(hidraw_root)? {
|
|
let entry = entry?;
|
|
let name = entry.file_name().to_string_lossy().to_string();
|
|
let Some((vendor_id, product_id, product_name, serial)) =
|
|
read_hidraw_identity(&entry.path())?
|
|
else {
|
|
continue;
|
|
};
|
|
let Some(supported) = SUPPORTED_DEVICES
|
|
.iter()
|
|
.find(|device| device.product_id == product_id)
|
|
else {
|
|
continue;
|
|
};
|
|
let interface_number = read_interface_number(&entry.path())?;
|
|
let probe_group = read_probe_group(&entry.path())?;
|
|
let manufacturer = read_ancestor_file(&entry.path(), "manufacturer")?;
|
|
let product_usb_string = read_ancestor_file(&entry.path(), "product")?;
|
|
|
|
devices.push(DeviceSummary {
|
|
path: format!("/dev/{name}"),
|
|
vendor_id,
|
|
product_id,
|
|
product_name: product_usb_string.unwrap_or(product_name),
|
|
manufacturer,
|
|
serial,
|
|
interface_number,
|
|
supported_name: supported.name.to_string(),
|
|
probe_group,
|
|
});
|
|
}
|
|
|
|
devices.sort_by_key(|device| {
|
|
(
|
|
device.probe_group.clone(),
|
|
interface_probe_priority(device.interface_number),
|
|
device.path.clone(),
|
|
)
|
|
});
|
|
Ok(devices)
|
|
}
|
|
|
|
fn read_hidraw_identity(
|
|
hidraw_path: &Path,
|
|
) -> io::Result<Option<(u16, u16, String, Option<String>)>> {
|
|
let uevent = fs::read_to_string(hidraw_path.join("device/uevent"))?;
|
|
let mut vendor_id = None;
|
|
let mut product_id = None;
|
|
let mut product_name = None;
|
|
let mut serial = None;
|
|
|
|
for line in uevent.lines() {
|
|
if let Some(value) = line.strip_prefix("HID_ID=") {
|
|
let parts: Vec<&str> = value.split(':').collect();
|
|
if parts.len() == 3 {
|
|
vendor_id = u16::from_str_radix(parts[1].trim_start_matches("0000"), 16).ok();
|
|
product_id = u16::from_str_radix(parts[2].trim_start_matches("0000"), 16).ok();
|
|
}
|
|
} else if let Some(value) = line.strip_prefix("HID_NAME=") {
|
|
product_name = Some(value.to_string());
|
|
} else if let Some(value) = line.strip_prefix("HID_UNIQ=") {
|
|
if !value.is_empty() {
|
|
serial = Some(value.to_string());
|
|
}
|
|
}
|
|
}
|
|
|
|
match (vendor_id, product_id) {
|
|
(Some(RAZER_VENDOR_ID), Some(product_id)) => Ok(Some((
|
|
RAZER_VENDOR_ID,
|
|
product_id,
|
|
product_name.unwrap_or_else(|| "Razer HID device".to_string()),
|
|
serial,
|
|
))),
|
|
_ => Ok(None),
|
|
}
|
|
}
|
|
|
|
fn read_interface_number(hidraw_path: &Path) -> io::Result<Option<u8>> {
|
|
let Some(value) = read_ancestor_file(hidraw_path, "bInterfaceNumber")? else {
|
|
return Ok(None);
|
|
};
|
|
Ok(u8::from_str_radix(value.trim(), 16).ok())
|
|
}
|
|
|
|
fn read_probe_group(hidraw_path: &Path) -> io::Result<String> {
|
|
let canonical = fs::canonicalize(hidraw_path)?;
|
|
let interface_path = canonical
|
|
.parent()
|
|
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "hidraw device has no parent"))?;
|
|
let usb_device = interface_path.parent().ok_or_else(|| {
|
|
io::Error::new(io::ErrorKind::NotFound, "HID interface has no USB parent")
|
|
})?;
|
|
Ok(usb_device.to_string_lossy().to_string())
|
|
}
|
|
|
|
fn read_ancestor_file(path: &Path, file_name: &str) -> io::Result<Option<String>> {
|
|
let mut current = fs::canonicalize(path)?;
|
|
for _ in 0..8 {
|
|
let candidate = current.join(file_name);
|
|
if candidate.exists() {
|
|
return Ok(Some(fs::read_to_string(candidate)?.trim().to_string()));
|
|
}
|
|
if !current.pop() {
|
|
break;
|
|
}
|
|
}
|
|
Ok(None)
|
|
}
|