fn try_connect_summary( summary: DeviceSummary, logs: std::sync::Arc>>, ) -> 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 { let mut candidates: Vec = 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 { 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> { 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)>> { 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> { 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 { 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> { 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) }