diff --git a/src/hid.rs b/src/hid.rs index d3697d1..83a2f3c 100644 --- a/src/hid.rs +++ b/src/hid.rs @@ -600,9 +600,20 @@ impl HidSession { self.device_keys.insert(device_id, key.clone()); key }; - if !self.candidates.contains_key(&key) { - let candidate = inspect_candidate(&self.api, key.clone(), &infos)?; - self.candidates.insert(key.clone(), candidate); + let paths = infos.iter().map(|info| info.path()).collect::>(); + if self.candidates.get(&key).is_none_or(|candidate| { + candidate.paths.len() != paths.len() + || candidate + .paths + .iter() + .any(|path| !paths.contains(path.path.as_c_str())) + }) { + if !self.open_devices.contains_key(&key) { + let candidate = inspect_candidate(&self.api, key.clone(), &infos)?; + self.candidates.insert(key.clone(), candidate); + } else { + self.recover(&key)?; + } } if let Some(candidate) = self.candidates.get(&key) { summaries.push(candidate.summary.clone()); @@ -922,8 +933,8 @@ impl HidSession { .open_devices .get(key) .ok_or_else(|| "the HID interface is not open".to_owned())?; - // An open device has several paths only when `inspect_candidate` verified - // they share one serial. A report still goes to exactly one of them. + // An open device has several paths only when `grouped_device_key` matched + // them by serial or Windows HID interface. A report still goes to exactly one. if open.paths.is_empty() { return Err("the HID interface has no open path".into()); } @@ -1082,13 +1093,19 @@ fn grouped_device_key( serial: Option<&str>, ) -> Vec { // A serial is a verified physical identity, so every collection of that - // device is one candidate. Without one, collections stay apart by path. + // device is one candidate. Without one, Windows collections group by their + // interface's OS-assigned parent instance; anything else stays apart by path. let serial = serial.filter(|serial| !serial.is_empty()); - let identity = serial.map_or(path, str::as_bytes); + let interface = collection_interface(path); + let (kind, identity) = match (serial, &interface) { + (Some(serial), _) => (1, serial.as_bytes()), + (None, Some(interface)) => (2, interface.as_bytes()), + (None, None) => (0, path), + }; let mut key = Vec::with_capacity(13 + identity.len()); key.extend_from_slice(&vendor_id.to_le_bytes()); key.extend_from_slice(&product_id.to_le_bytes()); - key.push(u8::from(serial.is_some())); + key.push(kind); key.extend_from_slice(&(identity.len() as u64).to_le_bytes()); key.extend_from_slice(identity); key @@ -1124,12 +1141,13 @@ fn report_paths(layouts: &[&ReportLayout], report_id: u8, feature: bool) -> Vec< } } -/// True when every interface reports the same non-empty serial. -fn shares_verified_serial<'a>(mut serials: impl Iterator>) -> bool { - match serials.next().flatten().filter(|serial| !serial.is_empty()) { - Some(first) => serials.all(|serial| serial == Some(first)), - None => false, - } +fn collection_interface(path: &[u8]) -> Option { + let path = std::str::from_utf8(path).ok()?.to_ascii_lowercase(); + let (hardware, rest) = path.strip_prefix(r"\\?\hid#")?.split_once('#')?; + let (instance, _) = rest.split_once('#')?; + let (hardware, _) = hardware.rsplit_once("&col")?; + let (parent, _) = instance.rsplit_once('&')?; + Some(format!("{hardware}#{parent}")) } fn inspect_candidate( @@ -1148,14 +1166,6 @@ fn inspect_candidate( } } - // Several paths are the collections of one device only when they share a - // verified serial; that is the one case `grouped_device_key` merges them. - if unique_paths.len() > 1 - && !shares_verified_serial(infos.iter().map(hidapi::DeviceInfo::serial_number)) - { - return Err("HID physical identity is ambiguous; refusing multi-path access".into()); - } - let inspect_descriptors = primary.vendor_id() != RAZER_VENDOR_ID; let mut collections = Vec::new(); let mut paths = Vec::with_capacity(unique_paths.len()); @@ -1173,20 +1183,20 @@ fn inspect_candidate( collections.append(&mut path_collections); path_layout } else { + collections.extend( + infos + .iter() + .filter(|info| info.path() == path.as_c_str()) + .map(|info| CollectionInfo { + usage_page: info.usage_page(), + usage: info.usage(), + ..CollectionInfo::default() + }), + ); ReportLayout::default() }; paths.push(CandidatePath { path, layout }); } - if collections.is_empty() { - collections = infos - .iter() - .map(|info| CollectionInfo { - usage_page: info.usage_page(), - usage: info.usage(), - ..CollectionInfo::default() - }) - .collect(); - } Ok(Candidate { paths, @@ -1687,22 +1697,6 @@ mod tests { ); } - #[test] - fn several_paths_open_only_under_one_verified_serial() { - assert!(shares_verified_serial( - [Some("receiver-123"), Some("receiver-123")].into_iter() - )); - assert!(!shares_verified_serial( - [Some("receiver-123"), Some("receiver-456")].into_iter() - )); - assert!(!shares_verified_serial( - [Some("receiver-123"), None].into_iter() - )); - assert!(!shares_verified_serial([None, None].into_iter())); - assert!(!shares_verified_serial([Some(""), Some("")].into_iter())); - assert!(!shares_verified_serial(std::iter::empty())); - } - #[test] fn a_report_goes_to_the_paths_that_declare_it_first() { let mut vendor = ReportLayout::default();