Added Windows support (WIP)
Adapted from draft PR #20. Co-authored-by: navrozashvili
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user