diff --git a/README.md b/README.md index 6af7510..604c480 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,8 @@ Although it was only tested on Manjaro and Kubuntu with KDE, it should also work - HyperX Cloud II Wireless HP vendorID - HyperX Cloud II Wireless HyperX vendorID - HyperX Cloud III Wireless -- HyperX Cloud Stinger 2 Wireless +- HyperX Cloud III S Wireless +- HyperX Cloud Stinger 2 Wireless It should be possible to add support for other HyperX headsets. @@ -62,12 +63,14 @@ SUBSYSTEMS=="usb", ATTRS{idProduct}=="0696", ATTRS{idVendor}=="03f0", MODE="0666 SUBSYSTEMS=="usb", ATTRS{idProduct}=="1718", ATTRS{idVendor}=="0951", MODE="0666" SUBSYSTEMS=="usb", ATTRS{idProduct}=="0d93", ATTRS{idVendor}=="03f0", MODE="0666" SUBSYSTEMS=="usb", ATTRS{idProduct}=="05b7", ATTRS{idVendor}=="03f0", MODE="0666" +SUBSYSTEMS=="usb", ATTRS{idProduct}=="06be", ATTRS{idVendor}=="03f0", MODE="0666" KERNEL=="hidraw*", ATTRS{idProduct}=="0d93", ATTRS{idVendor}=="03f0", MODE="0666" KERNEL=="hidraw*", ATTRS{idProduct}=="018b", ATTRS{idVendor}=="03f0", MODE="0666" KERNEL=="hidraw*", ATTRS{idProduct}=="0696", ATTRS{idVendor}=="03f0", MODE="0666" KERNEL=="hidraw*", ATTRS{idProduct}=="1718", ATTRS{idVendor}=="0951", MODE="0666" KERNEL=="hidraw*", ATTRS{idProduct}=="05b7", ATTRS{idVendor}=="03f0", MODE="0666" +KERNEL=="hidraw*", ATTRS{idProduct}=="06be", ATTRS{idVendor}=="03f0", MODE="0666" ``` Once created, replug the wireless dongle. diff --git a/src/devices/cloud_iii_s_wireless.rs b/src/devices/cloud_iii_s_wireless.rs new file mode 100644 index 0000000..586a89c --- /dev/null +++ b/src/devices/cloud_iii_s_wireless.rs @@ -0,0 +1,201 @@ +use crate::{ + debug_println, + devices::{Device, DeviceError, DeviceEvent, DeviceState}, +}; +use std::time::Duration; + +const HP: u16 = 0x03F0; +pub const VENDOR_IDS: [u16; 1] = [HP]; +pub const PRODUCT_IDS: [u16; 1] = [0x06BE]; + +// Cloud III S uses a different protocol than Cloud III +// Header 0x05 for mic control, 20-byte packets +const PACKET_SIZE: usize = 20; +const MIC_HEADER: u8 = 0x05; + +// Mic control commands (byte 1 after header) +// Pattern: (cmd & 0x02) == 0 means ON +const MIC_ON_CMD: u8 = 0x00; +const MIC_OFF_CMD: u8 = 0x02; + +// Button report header (incoming from headset) +const CONSUMER_CONTROL_HEADER: u8 = 0x0f; +// Consumer control button values +const _VOL_UP: u8 = 0x01; +const _VOL_DOWN: u8 = 0x02; +const _PLAY_PAUSE: u8 = 0x08; + +fn make_mic_packet(mute: bool) -> Vec { + let mut packet = vec![0u8; PACKET_SIZE]; + packet[0] = MIC_HEADER; + packet[1] = if mute { MIC_OFF_CMD } else { MIC_ON_CMD }; + packet +} + +pub struct CloudIIISWireless { + state: DeviceState, +} + +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 { + // Cloud III S: Battery query not discovered yet + fn get_charging_packet(&self) -> Option> { + None + } + + // Cloud III S: Battery query not discovered yet + fn get_battery_packet(&self) -> Option> { + None + } + + // Cloud III S: Auto shutdown not discovered yet + fn set_automatic_shut_down_packet(&self, _shutdown_after: Duration) -> Option> { + None + } + + fn get_automatic_shut_down_packet(&self) -> Option> { + None + } + + // Cloud III S: Cannot query mic state (no response received) + // Physical mic button doesn't emit state packets over HID + fn get_mute_packet(&self) -> Option> { + None + } + + // Cloud III S: Mic control - CONFIRMED WORKING + fn set_mute_packet(&self, mute: bool) -> Option> { + Some(make_mic_packet(mute)) + } + + fn get_surround_sound_packet(&self) -> Option> { + None + } + + fn set_surround_sound_packet(&self, _surround_sound: bool) -> Option> { + None + } + + fn get_mic_connected_packet(&self) -> Option> { + None + } + + fn get_pairing_info_packet(&self) -> Option> { + None + } + + fn get_product_color_packet(&self) -> Option> { + None + } + + // Cloud III S: Sidetone not discovered yet + fn get_side_tone_packet(&self) -> Option> { + None + } + + fn set_side_tone_packet(&self, _side_tone_on: bool) -> Option> { + None + } + + fn get_side_tone_volume_packet(&self) -> Option> { + None + } + + fn set_side_tone_volume_packet(&self, _volume: u8) -> Option> { + None + } + + fn get_voice_prompt_packet(&self) -> Option> { + None + } + + fn set_voice_prompt_packet(&self, _enable: bool) -> Option> { + None + } + + fn get_wireless_connected_status_packet(&self) -> Option> { + None + } + + fn get_sirk_packet(&self) -> Option> { + None + } + + fn reset_sirk_packet(&self) -> Option> { + None + } + + fn get_silent_mode_packet(&self) -> Option> { + None + } + + fn set_silent_mode_packet(&self, _silence: bool) -> Option> { + None + } + + fn get_event_from_device_response(&self, response: &[u8]) -> Option> { + debug_println!("Read packet: {response:?}"); + + match response[0] { + MIC_HEADER => { + // Mic state response + // Pattern: (byte[1] & 0x02) == 0 means mic is ON (not muted) + let muted = (response[1] & 0x02) != 0; + Some(vec![DeviceEvent::Muted(muted)]) + } + CONSUMER_CONTROL_HEADER => { + // Button press events - we log but don't need to store state + debug_println!( + "Consumer control event: 0x{:02x}", + response.get(1).unwrap_or(&0) + ); + None + } + _ => { + debug_println!("Unknown device event: {:?}", response); + None + } + } + } + + fn allow_passive_refresh(&mut self) -> bool { + true + } + + fn get_device_state(&self) -> &DeviceState { + &self.state + } + + fn get_device_state_mut(&mut self) -> &mut DeviceState { + &mut self.state + } + + /// Cloud III S has limited status reporting over HID. + /// We can SET mic mute but cannot query device state. + /// Override to prevent "No response" errors. + fn active_refresh_state(&mut self) -> Result<(), DeviceError> { + // Cloud III S doesn't respond to status queries. + // Just mark as connected since we successfully opened the device. + self.state.connected = Some(true); + + // Listen briefly for any incoming events (button presses, etc.) + let events = self.wait_for_updates(Duration::from_millis(100)); + if let Some(events) = events { + for event in events { + self.state.update_self_with_event(&event); + } + } + + Ok(()) + } +} diff --git a/src/devices/mod.rs b/src/devices/mod.rs index 5ec756d..0a0eb93 100644 --- a/src/devices/mod.rs +++ b/src/devices/mod.rs @@ -1,12 +1,13 @@ pub mod cloud_ii_wireless; pub mod cloud_ii_wireless_dts; +pub mod cloud_iii_s_wireless; pub mod cloud_iii_wireless; use crate::{ debug_println, devices::{ cloud_ii_wireless::CloudIIWireless, cloud_ii_wireless_dts::CloudIIWirelessDTS, - cloud_iii_wireless::CloudIIIWireless, + cloud_iii_s_wireless::CloudIIISWireless, cloud_iii_wireless::CloudIIIWireless, }, }; use hidapi::{HidApi, HidDevice, HidError}; @@ -16,7 +17,7 @@ use thistermination::TerminationFull; // Possible vendor IDs [HyperX, HP] const VENDOR_IDS: [u16; 2] = [0x0951, 0x03F0]; // All supported product IDs -const PRODUCT_IDS: [u16; 6] = [0x1718, 0x018B, 0x0D93, 0x0696, 0x0b92, 0x05B7]; +const PRODUCT_IDS: [u16; 7] = [0x1718, 0x018B, 0x0D93, 0x0696, 0x0b92, 0x05B7, 0x06BE]; const RESPONSE_BUFFER_SIZE: usize = 256; const RESPONSE_DELAY: Duration = Duration::from_millis(50); @@ -41,6 +42,12 @@ pub fn connect_compatible_device() -> Result, DeviceError> { { Box::new(CloudIIWirelessDTS::new_from_state(state)) } + (v, p) + if cloud_iii_s_wireless::VENDOR_IDS.contains(&v) + && cloud_iii_s_wireless::PRODUCT_IDS.contains(&p) => + { + Box::new(CloudIIISWireless::new_from_state(state)) + } (v, p) if cloud_iii_wireless::VENDOR_IDS.contains(&v) && cloud_iii_wireless::PRODUCT_IDS.contains(&p) =>