Added Windows support (WIP)

Adapted from draft PR #20.

Co-authored-by: navrozashvili
This commit is contained in:
Lennard Kittner
2026-03-12 13:09:15 +01:00
parent 2a6472015c
commit b66718a081
7 changed files with 180 additions and 101 deletions

View File

@@ -1,6 +1,6 @@
use crate::{ use crate::{
debug_println, debug_println,
devices::{ChargingStatus, Color, Device, DeviceError, DeviceEvent, DeviceState}, devices::{ChargingStatus, Color, Device, DeviceEvent, DeviceState},
}; };
use std::time::Duration; use std::time::Duration;
@@ -48,11 +48,6 @@ impl CloudAlphaWireless {
pub fn new_from_state(state: DeviceState) -> Self { pub fn new_from_state(state: DeviceState) -> Self {
CloudAlphaWireless { state } CloudAlphaWireless { state }
} }
pub fn new() -> Result<Self, DeviceError> {
let state = DeviceState::new(&PRODUCT_IDS, &VENDOR_IDS)?;
Ok(CloudAlphaWireless { state })
}
} }
impl Device for CloudAlphaWireless { impl Device for CloudAlphaWireless {

View File

@@ -1,6 +1,6 @@
use crate::{ use crate::{
debug_println, debug_println,
devices::{ChargingStatus, Device, DeviceError, DeviceEvent, DeviceState}, devices::{ChargingStatus, Device, DeviceEvent, DeviceState},
}; };
use std::time::Duration; use std::time::Duration;
@@ -48,12 +48,6 @@ impl CloudIICoreWireless {
state.device_properties.connected = Some(true); state.device_properties.connected = Some(true);
CloudIICoreWireless { state } CloudIICoreWireless { state }
} }
pub fn new() -> Result<Self, DeviceError> {
let mut state = DeviceState::new(&PRODUCT_IDS, &VENDOR_IDS)?;
state.device_properties.connected = Some(true);
Ok(CloudIICoreWireless { state })
}
} }
impl Device for CloudIICoreWireless { impl Device for CloudIICoreWireless {

View File

@@ -7,7 +7,7 @@ use std::time::Duration;
const HYPERX: u16 = 0x0951; const HYPERX: u16 = 0x0951;
pub const VENDOR_IDS: [u16; 1] = [HYPERX]; pub const VENDOR_IDS: [u16; 1] = [HYPERX];
// Possible Cloud II Wireless product IDs (and Cloud Flight S) // 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] = { const BASE_PACKET: [u8; 62] = {
let mut tmp = [0u8; 62]; let mut tmp = [0u8; 62];
@@ -51,12 +51,6 @@ impl CloudIIWireless {
tmp_state.device_properties.connected = Some(true); tmp_state.device_properties.connected = Some(true);
CloudIIWireless { state: tmp_state } CloudIIWireless { state: tmp_state }
} }
pub fn new() -> Result<Self, DeviceError> {
let mut state = DeviceState::new(&PRODUCT_IDS, &VENDOR_IDS)?;
state.device_properties.connected = Some(true);
Ok(CloudIIWireless { state })
}
} }
impl Device for CloudIIWireless { impl Device for CloudIIWireless {

View File

@@ -1,6 +1,6 @@
use crate::{ use crate::{
debug_println, debug_println,
devices::{ChargingStatus, Color, Device, DeviceError, DeviceEvent, DeviceState}, devices::{ChargingStatus, Color, Device, DeviceEvent, DeviceState},
}; };
use std::time::Duration; use std::time::Duration;
@@ -52,12 +52,6 @@ impl CloudIIWirelessDTS {
state.device_properties.connected = Some(true); state.device_properties.connected = Some(true);
CloudIIWirelessDTS { state } CloudIIWirelessDTS { state }
} }
pub fn new() -> Result<Self, DeviceError> {
let mut state = DeviceState::new(&PRODUCT_IDS, &VENDOR_IDS)?;
state.device_properties.connected = Some(true);
Ok(CloudIIWirelessDTS { state })
}
} }
impl Device for CloudIIWirelessDTS { impl Device for CloudIIWirelessDTS {

View File

@@ -1,6 +1,6 @@
use crate::{ use crate::{
debug_println, debug_println,
devices::{ChargingStatus, Color, Device, DeviceError, DeviceEvent, DeviceState}, devices::{ChargingStatus, Color, Device, DeviceEvent, DeviceState},
}; };
use std::time::Duration; use std::time::Duration;
@@ -144,11 +144,6 @@ impl CloudIIISWireless {
pub fn new_from_state(state: DeviceState) -> Self { pub fn new_from_state(state: DeviceState) -> Self {
CloudIIISWireless { state } CloudIIISWireless { state }
} }
pub fn new() -> Result<Self, DeviceError> {
let state = DeviceState::new(&PRODUCT_IDS, &VENDOR_IDS)?;
Ok(CloudIIISWireless { state })
}
} }
impl Device for CloudIIISWireless { impl Device for CloudIIISWireless {

View File

@@ -1,6 +1,6 @@
use crate::{ use crate::{
debug_println, debug_println,
devices::{ChargingStatus, Color, Device, DeviceError, DeviceEvent, DeviceState}, devices::{ChargingStatus, Color, Device, DeviceEvent, DeviceState},
}; };
use std::{time::Duration, vec}; use std::{time::Duration, vec};
@@ -46,11 +46,6 @@ impl CloudIIIWireless {
pub fn new_from_state(state: DeviceState) -> Self { pub fn new_from_state(state: DeviceState) -> Self {
CloudIIIWireless { state } CloudIIIWireless { state }
} }
pub fn new() -> Result<Self, DeviceError> {
let state = DeviceState::new(&PRODUCT_IDS, &VENDOR_IDS)?;
Ok(CloudIIIWireless { state })
}
} }
impl Device for CloudIIIWireless { impl Device for CloudIIIWireless {

View File

@@ -76,25 +76,80 @@ pub fn connect_compatible_device() -> Result<Box<dyn Device>, DeviceError> {
.iter() .iter()
.flat_map(|e| e.vendor_ids.iter().copied()) .flat_map(|e| e.vendor_ids.iter().copied())
.collect(); .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"); 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); // On Linux and MacOS we can just take the first
device.init_capabilities(); #[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)] #[derive(Debug)]
@@ -141,9 +196,10 @@ impl Display for DeviceProperties {
} }
impl DeviceState { impl DeviceState {
pub fn new(product_ids: &[u16], vendor_ids: &[u16]) -> Result<Self, DeviceError> { pub fn new(product_ids: &[u16], vendor_ids: &[u16]) -> Result<Vec<Self>, DeviceError> {
let hid_api = HidApi::new()?; let hid_api = HidApi::new()?;
let mut potential_devices = HashSet::new(); let mut potential_devices = HashSet::new();
let mut error = Ok(());
debug_println!( debug_println!(
"Devices: {:?}", "Devices: {:?}",
hid_api hid_api
@@ -152,9 +208,9 @@ impl DeviceState {
.map(|d| { (d.vendor_id(), d.product_id(), d.product_string()) }) .map(|d| { (d.vendor_id(), d.product_id(), d.product_string()) })
.collect::<Vec<(u16, u16, Option<&str>)>>() .collect::<Vec<(u16, u16, Option<&str>)>>()
); );
let device_lookup_result = hid_api let device_candidates: Vec<(HidDevice, u16, u16)> = hid_api
.device_list() .device_list()
.find_map(|info| { .filter_map(|info| {
if product_ids.contains(&info.product_id()) if product_ids.contains(&info.product_id())
&& vendor_ids.contains(&info.vendor_id()) && vendor_ids.contains(&info.vendor_id())
{ {
@@ -164,11 +220,20 @@ impl DeviceState {
info.product_id(), info.product_id(),
info.product_string() info.product_string()
); );
Some(( match info.open_device(&hid_api) {
hid_api.open(info.vendor_id(), info.product_id()), Ok(device) => Some((device, info.product_id(), info.vendor_id())),
info.product_id(), Err(e) => {
info.vendor_id(), debug_println!(
)) "Failed to open: {:x}:{:x} {:?}: {:?}",
info.vendor_id(),
info.product_id(),
info.product_string(),
e
);
error = Err(e);
None
}
}
} else { } else {
if let Some(name) = info.product_string() { if let Some(name) = info.product_string() {
if name.contains("HyperX") { if name.contains("HyperX") {
@@ -182,38 +247,79 @@ impl DeviceState {
None None
} }
}) })
.ok_or(DeviceError::NoDeviceFound()); .collect();
let (hid_device, product_id, vendor_id) = match device_lookup_result {
Ok(value) => value, if device_candidates.is_empty() {
Err(DeviceError::NoDeviceFound()) => { if !potential_devices.is_empty() {
if !potential_devices.is_empty() { let names = potential_devices
let names = potential_devices .iter()
.iter() .map(|e| {
.map(|e| { format!(
format!( " vendorID: 0x{:04X} productID: 0x{:04X} name: {}",
" vendorID: 0x{:04X} productID: 0x{:04X} name: {}", e.0,
e.0, e.1,
e.1, e.2.unwrap_or("Unknown")
e.2.unwrap_or("Unknown") )
) })
}) .collect::<Vec<String>>()
.collect::<Vec<String>>() .join(",\n");
.join(",\n"); //TODO: show as message in tray app
println!( println!(
"Found the following HyperX device{}: [\n{}\n]\nHowever, either {} not supported or the product ID is not yet known.", "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" } if potential_devices.len() > 1 { "s" } else { "" }, names, if potential_devices.len() > 1 { "they are" } else { "it is" }
) );
}
return Err(DeviceError::NoDeviceFound());
} }
Err(e) => return Err(e), error?;
}; return Err(DeviceError::NoDeviceFound());
let hid_device = hid_device?; }
let device_name = hid_device.get_product_string()?;
Ok(DeviceState { Ok(device_candidates
hid_device, .into_iter()
device_properties: DeviceProperties::new(product_id, vendor_id, device_name), .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) { fn update_self_with_event(&mut self, event: &DeviceEvent) {
@@ -756,9 +862,8 @@ pub trait Device {
self.get_event_from_device_response(&buf) self.get_event_from_device_response(&buf)
} }
/// Refreshes the state by querying all available information fn get_query_packets(&self) -> Vec<Vec<u8>> {
fn active_refresh_state(&mut self) -> Result<(), DeviceError> { vec![
let packets = vec![
self.get_wireless_connected_status_packet(), self.get_wireless_connected_status_packet(),
self.get_charging_packet(), self.get_charging_packet(),
self.get_battery_packet(), self.get_battery_packet(),
@@ -774,15 +879,22 @@ pub trait Device {
self.get_sirk_packet(), self.get_sirk_packet(),
self.get_silent_mode_packet(), self.get_silent_mode_packet(),
self.get_noise_gate_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()?; self.execute_headset_specific_functionality()?;
let mut responded = false; let mut responded = false;
for packet in packets.into_iter().flatten() { for packet in packets.into_iter() {
self.prepare_write(); self.prepare_write();
debug_println!("Write packet: {packet:?}"); 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); std::thread::sleep(RESPONSE_DELAY);
if let Some(events) = self.wait_for_updates(Duration::from_secs(1)) { if let Some(events) = self.wait_for_updates(Duration::from_secs(1)) {
for event in events { for event in events {
@@ -817,7 +929,7 @@ pub trait Device {
} }
if let Some(batter_packet) = self.get_battery_packet() { if let Some(batter_packet) = self.get_battery_packet() {
self.prepare_write(); 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); std::thread::sleep(RESPONSE_DELAY);
if let Some(events) = self.wait_for_updates(Duration::from_secs(1)) { if let Some(events) = self.wait_for_updates(Duration::from_secs(1)) {
for event in events { for event in events {