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