From b66718a081b403a7409c30e1c04d76ab245d9753 Mon Sep 17 00:00:00 2001 From: Lennard Kittner Date: Thu, 12 Mar 2026 13:09:15 +0100 Subject: [PATCH] Added Windows support (WIP) Adapted from draft PR #20. Co-authored-by: navrozashvili --- src/devices/cloud_alpha_wireless.rs | 7 +- src/devices/cloud_ii_core_wireless.rs | 8 +- src/devices/cloud_ii_wireless.rs | 8 +- src/devices/cloud_ii_wireless_dts.rs | 8 +- src/devices/cloud_iii_s_wireless.rs | 7 +- src/devices/cloud_iii_wireless.rs | 7 +- src/devices/mod.rs | 236 +++++++++++++++++++------- 7 files changed, 180 insertions(+), 101 deletions(-) diff --git a/src/devices/cloud_alpha_wireless.rs b/src/devices/cloud_alpha_wireless.rs index 298b27a..bbac627 100644 --- a/src/devices/cloud_alpha_wireless.rs +++ b/src/devices/cloud_alpha_wireless.rs @@ -1,6 +1,6 @@ use crate::{ debug_println, - devices::{ChargingStatus, Color, Device, DeviceError, DeviceEvent, DeviceState}, + devices::{ChargingStatus, Color, Device, DeviceEvent, DeviceState}, }; use std::time::Duration; @@ -48,11 +48,6 @@ impl CloudAlphaWireless { pub fn new_from_state(state: DeviceState) -> Self { CloudAlphaWireless { state } } - - pub fn new() -> Result { - let state = DeviceState::new(&PRODUCT_IDS, &VENDOR_IDS)?; - Ok(CloudAlphaWireless { state }) - } } impl Device for CloudAlphaWireless { diff --git a/src/devices/cloud_ii_core_wireless.rs b/src/devices/cloud_ii_core_wireless.rs index 1c1bfa7..ba8761a 100644 --- a/src/devices/cloud_ii_core_wireless.rs +++ b/src/devices/cloud_ii_core_wireless.rs @@ -1,6 +1,6 @@ use crate::{ debug_println, - devices::{ChargingStatus, Device, DeviceError, DeviceEvent, DeviceState}, + devices::{ChargingStatus, Device, DeviceEvent, DeviceState}, }; use std::time::Duration; @@ -48,12 +48,6 @@ impl CloudIICoreWireless { state.device_properties.connected = Some(true); CloudIICoreWireless { state } } - - pub fn new() -> Result { - let mut state = DeviceState::new(&PRODUCT_IDS, &VENDOR_IDS)?; - state.device_properties.connected = Some(true); - Ok(CloudIICoreWireless { state }) - } } impl Device for CloudIICoreWireless { diff --git a/src/devices/cloud_ii_wireless.rs b/src/devices/cloud_ii_wireless.rs index 3e7b9ac..e20ec6e 100644 --- a/src/devices/cloud_ii_wireless.rs +++ b/src/devices/cloud_ii_wireless.rs @@ -7,7 +7,7 @@ use std::time::Duration; const HYPERX: u16 = 0x0951; pub const VENDOR_IDS: [u16; 1] = [HYPERX]; // Possible Cloud II Wireless product IDs (and Cloud Flight S) -pub const PRODUCT_IDS: [u16; 5] = [0x1718, 0x018B, 0x0b92, 0x16EA, 0x16EB]; +pub const PRODUCT_IDS: [u16; 4] = [0x1718, 0x0b92, 0x16EA, 0x16EB]; const BASE_PACKET: [u8; 62] = { let mut tmp = [0u8; 62]; @@ -51,12 +51,6 @@ impl CloudIIWireless { tmp_state.device_properties.connected = Some(true); CloudIIWireless { state: tmp_state } } - - pub fn new() -> Result { - let mut state = DeviceState::new(&PRODUCT_IDS, &VENDOR_IDS)?; - state.device_properties.connected = Some(true); - Ok(CloudIIWireless { state }) - } } impl Device for CloudIIWireless { diff --git a/src/devices/cloud_ii_wireless_dts.rs b/src/devices/cloud_ii_wireless_dts.rs index 617aef8..8e5e9d2 100644 --- a/src/devices/cloud_ii_wireless_dts.rs +++ b/src/devices/cloud_ii_wireless_dts.rs @@ -1,6 +1,6 @@ use crate::{ debug_println, - devices::{ChargingStatus, Color, Device, DeviceError, DeviceEvent, DeviceState}, + devices::{ChargingStatus, Color, Device, DeviceEvent, DeviceState}, }; use std::time::Duration; @@ -52,12 +52,6 @@ impl CloudIIWirelessDTS { state.device_properties.connected = Some(true); CloudIIWirelessDTS { state } } - - pub fn new() -> Result { - let mut state = DeviceState::new(&PRODUCT_IDS, &VENDOR_IDS)?; - state.device_properties.connected = Some(true); - Ok(CloudIIWirelessDTS { state }) - } } impl Device for CloudIIWirelessDTS { diff --git a/src/devices/cloud_iii_s_wireless.rs b/src/devices/cloud_iii_s_wireless.rs index de97acc..71f1bde 100644 --- a/src/devices/cloud_iii_s_wireless.rs +++ b/src/devices/cloud_iii_s_wireless.rs @@ -1,6 +1,6 @@ use crate::{ debug_println, - devices::{ChargingStatus, Color, Device, DeviceError, DeviceEvent, DeviceState}, + devices::{ChargingStatus, Color, Device, DeviceEvent, DeviceState}, }; use std::time::Duration; @@ -144,11 +144,6 @@ impl CloudIIISWireless { pub fn new_from_state(state: DeviceState) -> Self { CloudIIISWireless { state } } - - pub fn new() -> Result { - let state = DeviceState::new(&PRODUCT_IDS, &VENDOR_IDS)?; - Ok(CloudIIISWireless { state }) - } } impl Device for CloudIIISWireless { diff --git a/src/devices/cloud_iii_wireless.rs b/src/devices/cloud_iii_wireless.rs index 07629da..1cec27f 100644 --- a/src/devices/cloud_iii_wireless.rs +++ b/src/devices/cloud_iii_wireless.rs @@ -1,6 +1,6 @@ use crate::{ debug_println, - devices::{ChargingStatus, Color, Device, DeviceError, DeviceEvent, DeviceState}, + devices::{ChargingStatus, Color, Device, DeviceEvent, DeviceState}, }; use std::{time::Duration, vec}; @@ -46,11 +46,6 @@ impl CloudIIIWireless { pub fn new_from_state(state: DeviceState) -> Self { CloudIIIWireless { state } } - - pub fn new() -> Result { - let state = DeviceState::new(&PRODUCT_IDS, &VENDOR_IDS)?; - Ok(CloudIIIWireless { state }) - } } impl Device for CloudIIIWireless { diff --git a/src/devices/mod.rs b/src/devices/mod.rs index 3744dc4..e221587 100644 --- a/src/devices/mod.rs +++ b/src/devices/mod.rs @@ -76,25 +76,80 @@ pub fn connect_compatible_device() -> Result, DeviceError> { .iter() .flat_map(|e| e.vendor_ids.iter().copied()) .collect(); - let state = DeviceState::new(&all_product_ids, &all_vendor_ids)?; + let states = DeviceState::new(&all_product_ids, &all_vendor_ids)?; debug_println!("Found device selecting handler"); - let name = state - .hid_device - .get_product_string()? - .ok_or(DeviceError::NoDeviceFound())?; - println!("Connecting to {}", name); - let entry = DEVICE_REGISTER - .iter() - .find(|e| { - e.vendor_ids.contains(&state.device_properties.vendor_id) - && e.product_ids.contains(&state.device_properties.product_id) - }) - .ok_or(DeviceError::NoDeviceFound())?; - let mut device = (entry.factory)(state); - device.init_capabilities(); + // On Linux and MacOS we can just take the first + #[cfg(not(target_os = "windows"))] + { + let state = states + .into_iter() + .next() + .ok_or(DeviceError::NoDeviceFound())?; + println!( + "Connecting to {}", + state + .device_properties + .device_name + .clone() + .unwrap_or("???".to_string()) + ); + let entry = DEVICE_REGISTER + .iter() + .find(|e| { + e.vendor_ids.contains(&state.device_properties.vendor_id) + && e.product_ids.contains(&state.device_properties.product_id) + }) + .ok_or(DeviceError::NoDeviceFound())?; - Ok(device) + let mut device = (entry.factory)(state); + device.init_capabilities(); + Ok(device) + } + // On Windows we have to check which interface can be used + #[cfg(target_os = "windows")] + { + let mut device = None; + for state in states { + println!( + "Try to connecting to {}", + state + .device_properties + .device_name + .clone() + .unwrap_or("???".to_string()) + ); + let entry = DEVICE_REGISTER + .iter() + .find(|e| { + e.vendor_ids.contains(&state.device_properties.vendor_id) + && e.product_ids.contains(&state.device_properties.product_id) + }) + .ok_or(DeviceError::NoDeviceFound())?; + + let mut test_device = (entry.factory)(state); + test_device.init_capabilities(); + + let probe_packet = test_device + .get_query_packets() + .into_iter() + .next() + .expect("Why is there a device without packets ???"); + + test_device.prepare_write(); + if let Err(e) = test_device + .get_device_state() + .write_hid_report(&probe_packet) + { + debug_println!("Failed to open: {e:?}"); + continue; + } else { + device = Some(test_device); + break; + } + } + device.ok_or(DeviceError::NoDeviceFound()) + } } #[derive(Debug)] @@ -141,9 +196,10 @@ impl Display for DeviceProperties { } impl DeviceState { - pub fn new(product_ids: &[u16], vendor_ids: &[u16]) -> Result { + pub fn new(product_ids: &[u16], vendor_ids: &[u16]) -> Result, DeviceError> { let hid_api = HidApi::new()?; let mut potential_devices = HashSet::new(); + let mut error = Ok(()); debug_println!( "Devices: {:?}", hid_api @@ -152,9 +208,9 @@ impl DeviceState { .map(|d| { (d.vendor_id(), d.product_id(), d.product_string()) }) .collect::)>>() ); - let device_lookup_result = hid_api + let device_candidates: Vec<(HidDevice, u16, u16)> = hid_api .device_list() - .find_map(|info| { + .filter_map(|info| { if product_ids.contains(&info.product_id()) && vendor_ids.contains(&info.vendor_id()) { @@ -164,11 +220,20 @@ impl DeviceState { info.product_id(), info.product_string() ); - Some(( - hid_api.open(info.vendor_id(), info.product_id()), - info.product_id(), - info.vendor_id(), - )) + match info.open_device(&hid_api) { + Ok(device) => Some((device, info.product_id(), info.vendor_id())), + Err(e) => { + debug_println!( + "Failed to open: {:x}:{:x} {:?}: {:?}", + info.vendor_id(), + info.product_id(), + info.product_string(), + e + ); + error = Err(e); + None + } + } } else { if let Some(name) = info.product_string() { if name.contains("HyperX") { @@ -182,38 +247,79 @@ impl DeviceState { None } }) - .ok_or(DeviceError::NoDeviceFound()); - let (hid_device, product_id, vendor_id) = match device_lookup_result { - Ok(value) => value, - Err(DeviceError::NoDeviceFound()) => { - if !potential_devices.is_empty() { - let names = potential_devices - .iter() - .map(|e| { - format!( - " vendorID: 0x{:04X} productID: 0x{:04X} name: {}", - e.0, - e.1, - e.2.unwrap_or("Unknown") - ) - }) - .collect::>() - .join(",\n"); - println!( - "Found the following HyperX device{}: [\n{}\n]\nHowever, either {} not supported or the product ID is not yet known.", - if potential_devices.len() > 1 { "s" } else { "" }, names, if potential_devices.len() > 1 { "they are" } else { "it is" } - ) - } - return Err(DeviceError::NoDeviceFound()); + .collect(); + + if device_candidates.is_empty() { + if !potential_devices.is_empty() { + let names = potential_devices + .iter() + .map(|e| { + format!( + " vendorID: 0x{:04X} productID: 0x{:04X} name: {}", + e.0, + e.1, + e.2.unwrap_or("Unknown") + ) + }) + .collect::>() + .join(",\n"); + //TODO: show as message in tray app + println!( + "Found the following HyperX device{}: [\n{}\n]\nHowever, either {} not supported or the product ID is not yet known.", + if potential_devices.len() > 1 { "s" } else { "" }, names, if potential_devices.len() > 1 { "they are" } else { "it is" } + ); } - Err(e) => return Err(e), - }; - let hid_device = hid_device?; - let device_name = hid_device.get_product_string()?; - Ok(DeviceState { - hid_device, - device_properties: DeviceProperties::new(product_id, vendor_id, device_name), - }) + error?; + return Err(DeviceError::NoDeviceFound()); + } + + Ok(device_candidates + .into_iter() + .map(|(hid_device, product_id, vendor_id)| { + let device_name = hid_device.get_product_string().ok().flatten(); + DeviceState { + hid_device, + device_properties: DeviceProperties::new(product_id, vendor_id, device_name), + } + }) + .collect()) + } + + /// Write a HID report to the device. + /// + /// On Windows, some HyperX dongles expose commands as **Feature reports** only. + /// In that case, `hidapi::HidDevice::write()` fails with: + /// `WriteFile: (0x00000001) Incorrect function.` + /// + /// Linux/macOS hidraw paths often accept the same bytes via output reports, so this can look + /// "Windows-exclusive". We transparently fall back to `send_feature_report` when we detect + /// this specific failure. + /// Adapted from PR #20 by @navrozashvili + /// Source: https://github.com/LennardKittner/HyperHeadset/pull/20 + pub fn write_hid_report(&self, packet: &[u8]) -> Result<(), HidError> { + match self.hid_device.write(packet) { + Ok(_) => Ok(()), + Err(write_err) => { + #[cfg(target_os = "windows")] + { + if let HidError::HidApiError { message } = &write_err { + // Windows HID stack returns ERROR_INVALID_FUNCTION (0x1) when the device + // doesn't support output reports / interrupt OUT. + if message.contains("Incorrect function") + || message.contains("(0x00000001)") + { + // If the feature report also fails, prefer returning the original + // write() error since that's what callers attempted. + if let Err(_feature_err) = self.hid_device.send_feature_report(packet) { + return Err(write_err); + } + return Ok(()); + } + } + } + Err(write_err) + } + } } fn update_self_with_event(&mut self, event: &DeviceEvent) { @@ -756,9 +862,8 @@ pub trait Device { self.get_event_from_device_response(&buf) } - /// Refreshes the state by querying all available information - fn active_refresh_state(&mut self) -> Result<(), DeviceError> { - let packets = vec![ + fn get_query_packets(&self) -> Vec> { + vec![ self.get_wireless_connected_status_packet(), self.get_charging_packet(), self.get_battery_packet(), @@ -774,15 +879,22 @@ pub trait Device { self.get_sirk_packet(), self.get_silent_mode_packet(), self.get_noise_gate_packet(), - ]; + ] + .into_iter() + .flatten() + .collect() + } + /// Refreshes the state by querying all available information + fn active_refresh_state(&mut self) -> Result<(), DeviceError> { + let packets = self.get_query_packets(); self.execute_headset_specific_functionality()?; let mut responded = false; - for packet in packets.into_iter().flatten() { + for packet in packets.into_iter() { self.prepare_write(); debug_println!("Write packet: {packet:?}"); - self.get_device_state().hid_device.write(&packet)?; + self.get_device_state().write_hid_report(&packet)?; std::thread::sleep(RESPONSE_DELAY); if let Some(events) = self.wait_for_updates(Duration::from_secs(1)) { for event in events { @@ -817,7 +929,7 @@ pub trait Device { } if let Some(batter_packet) = self.get_battery_packet() { self.prepare_write(); - self.get_device_state().hid_device.write(&batter_packet)?; + self.get_device_state().write_hid_report(&batter_packet)?; std::thread::sleep(RESPONSE_DELAY); if let Some(events) = self.wait_for_updates(Duration::from_secs(1)) { for event in events {