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::{
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<Self, DeviceError> {
let state = DeviceState::new(&PRODUCT_IDS, &VENDOR_IDS)?;
Ok(CloudAlphaWireless { state })
}
}
impl Device for CloudAlphaWireless {

View File

@@ -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<Self, DeviceError> {
let mut state = DeviceState::new(&PRODUCT_IDS, &VENDOR_IDS)?;
state.device_properties.connected = Some(true);
Ok(CloudIICoreWireless { state })
}
}
impl Device for CloudIICoreWireless {

View File

@@ -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<Self, DeviceError> {
let mut state = DeviceState::new(&PRODUCT_IDS, &VENDOR_IDS)?;
state.device_properties.connected = Some(true);
Ok(CloudIIWireless { state })
}
}
impl Device for CloudIIWireless {

View File

@@ -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<Self, DeviceError> {
let mut state = DeviceState::new(&PRODUCT_IDS, &VENDOR_IDS)?;
state.device_properties.connected = Some(true);
Ok(CloudIIWirelessDTS { state })
}
}
impl Device for CloudIIWirelessDTS {

View File

@@ -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<Self, DeviceError> {
let state = DeviceState::new(&PRODUCT_IDS, &VENDOR_IDS)?;
Ok(CloudIIISWireless { state })
}
}
impl Device for CloudIIISWireless {

View File

@@ -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<Self, DeviceError> {
let state = DeviceState::new(&PRODUCT_IDS, &VENDOR_IDS)?;
Ok(CloudIIIWireless { state })
}
}
impl Device for CloudIIIWireless {

View File

@@ -76,13 +76,24 @@ pub fn connect_compatible_device() -> Result<Box<dyn Device>, 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()?
// 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 {}", name);
println!(
"Connecting to {}",
state
.device_properties
.device_name
.clone()
.unwrap_or("???".to_string())
);
let entry = DEVICE_REGISTER
.iter()
.find(|e| {
@@ -93,9 +104,53 @@ pub fn connect_compatible_device() -> Result<Box<dyn Device>, DeviceError> {
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)]
pub struct DeviceState {
@@ -141,9 +196,10 @@ impl Display for DeviceProperties {
}
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 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::<Vec<(u16, u16, Option<&str>)>>()
);
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(),
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,10 +247,9 @@ impl DeviceState {
None
}
})
.ok_or(DeviceError::NoDeviceFound());
let (hid_device, product_id, vendor_id) = match device_lookup_result {
Ok(value) => value,
Err(DeviceError::NoDeviceFound()) => {
.collect();
if device_candidates.is_empty() {
if !potential_devices.is_empty() {
let names = potential_devices
.iter()
@@ -199,21 +263,63 @@ impl DeviceState {
})
.collect::<Vec<String>>()
.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" }
)
);
}
error?;
return Err(DeviceError::NoDeviceFound());
}
Err(e) => return Err(e),
};
let hid_device = hid_device?;
let device_name = hid_device.get_product_string()?;
Ok(DeviceState {
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<u8>> {
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 {