From 54e95a0c17d40e556bae1b3315cea867a53a4b06 Mon Sep 17 00:00:00 2001 From: Lennard Kittner Date: Tue, 24 Feb 2026 13:14:48 +0100 Subject: [PATCH] Add initial code for supporting cloud II core wireless --- 99-HyperHeadset.rules | 2 + README.md | 3 + src/devices/cloud_ii_core_wireless.rs | 268 ++++++++++++++++++++++++++ src/devices/mod.rs | 51 ++++- 4 files changed, 314 insertions(+), 10 deletions(-) create mode 100644 src/devices/cloud_ii_core_wireless.rs diff --git a/99-HyperHeadset.rules b/99-HyperHeadset.rules index bfeb58f..bd3c368 100644 --- a/99-HyperHeadset.rules +++ b/99-HyperHeadset.rules @@ -10,6 +10,7 @@ SUBSYSTEMS=="usb", ATTRS{idProduct}=="0c9d", ATTRS{idVendor}=="03f0", MODE="0666 SUBSYSTEMS=="usb", ATTRS{idProduct}=="098d", ATTRS{idVendor}=="03f0", MODE="0666" SUBSYSTEMS=="usb", ATTRS{idProduct}=="1765", ATTRS{idVendor}=="03f0", MODE="0666" SUBSYSTEMS=="usb", ATTRS{idProduct}=="1743", ATTRS{idVendor}=="03f0", MODE="0666" +SUBSYSTEMS=="usb", ATTRS{idProduct}=="0aa0", ATTRS{idVendor}=="03f0", MODE="0666" KERNEL=="hidraw*", ATTRS{idProduct}=="0d93", ATTRS{idVendor}=="03f0", MODE="0666" @@ -24,3 +25,4 @@ KERNEL=="hidraw*", ATTRS{idProduct}=="0c9d", ATTRS{idVendor}=="03f0", MODE="0666 KERNEL=="hidraw*", ATTRS{idProduct}=="098d", ATTRS{idVendor}=="03f0", MODE="0666" KERNEL=="hidraw*", ATTRS{idProduct}=="1765", ATTRS{idVendor}=="03f0", MODE="0666" KERNEL=="hidraw*", ATTRS{idProduct}=="1743", ATTRS{idVendor}=="03f0", MODE="0666" +KERNEL=="hidraw*", ATTRS{idProduct}=="0aa0", ATTRS{idVendor}=="03f0", MODE="0666" diff --git a/README.md b/README.md index 6148f41..8fac7a1 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ However, the tray application is only functional on Linux. **Supported Headsets**: - HyperX Cloud II Wireless HP vendorID - HyperX Cloud II Wireless HyperX vendorID +- HyperX Cloud II Core Wireless - HyperX Cloud III Wireless - HyperX Cloud III S Wireless - HyperX Cloud Stinger 2 Wireless @@ -103,6 +104,7 @@ SUBSYSTEMS=="usb", ATTRS{idProduct}=="0c9d", ATTRS{idVendor}=="03f0", MODE="0666 SUBSYSTEMS=="usb", ATTRS{idProduct}=="098d", ATTRS{idVendor}=="03f0", MODE="0666" SUBSYSTEMS=="usb", ATTRS{idProduct}=="1765", ATTRS{idVendor}=="03f0", MODE="0666" SUBSYSTEMS=="usb", ATTRS{idProduct}=="1743", ATTRS{idVendor}=="03f0", MODE="0666" +SUBSYSTEMS=="usb", ATTRS{idProduct}=="0aa0", 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" @@ -116,6 +118,7 @@ KERNEL=="hidraw*", ATTRS{idProduct}=="0c9d", ATTRS{idVendor}=="03f0", MODE="0666 KERNEL=="hidraw*", ATTRS{idProduct}=="098d", ATTRS{idVendor}=="03f0", MODE="0666" KERNEL=="hidraw*", ATTRS{idProduct}=="1765", ATTRS{idVendor}=="03f0", MODE="0666" KERNEL=="hidraw*", ATTRS{idProduct}=="1743", ATTRS{idVendor}=="03f0", MODE="0666" +KERNEL=="hidraw*", ATTRS{idProduct}=="0aa0", ATTRS{idVendor}=="03f0", MODE="0666" ``` Once created, replug the wireless dongle. diff --git a/src/devices/cloud_ii_core_wireless.rs b/src/devices/cloud_ii_core_wireless.rs new file mode 100644 index 0000000..bd3db9d --- /dev/null +++ b/src/devices/cloud_ii_core_wireless.rs @@ -0,0 +1,268 @@ +use crate::{ + debug_println, + devices::{ChargingStatus, 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] = [0x0aa0]; + +const BASE_PACKET: [u8; 64] = { + let mut packet = [0; 64]; + packet[0] = 102; + packet +}; + +const GET_CHARGING_CMD_ID: u8 = 138; +const CHARGING_RESPONSE_ID: u8 = 12; +const GET_MIC_CONNECTED_CMD_ID: u8 = 140; +const GET_BATTERY_CMD_ID: u8 = 137; +const BATTERY_RESPONSE_ID: u8 = 13; +const GET_AUTO_SHUTDOWN_CMD_ID: u8 = 133; +const SET_AUTO_SHUTDOWN_CMD_ID: u8 = 2; +const GET_MUTE_CMD_ID: u8 = 134; +const MUTE_RESPONSE_ID: u8 = 10; +const SET_MUTE_CMD_ID: u8 = 3; +const GET_PAIRING_CMD_ID: u8 = 129; +const GET_SIDE_TONE_ON_CMD_ID: u8 = 132; +const SIDE_TONE_RESPONSE_ID: u8 = 9; +const SET_SIDE_TONE_ON_CMD_ID: u8 = 1; +const GET_SIDE_TONE_VOLUME_CMD_ID: u8 = 136; +const SET_SIDE_TONE_VOLUME_CMD_ID: u8 = 5; +const GET_WIRELESS_STATUS_CMD_ID: u8 = 130; +const WIRELESS_STATUS_RESPONSE_ID: u8 = 11; +const GET_PLAY_BACK_MUTE_CMD_ID: u8 = 135; +const SET_PLAY_BACK_MUTE_CMD_ID: u8 = 4; +const GET_NOISE_GATE_CMD_ID: u8 = 141; +const SET_NOISE_GATE_CMD_ID: u8 = 15; + +pub struct CloudIICoreWireless { + state: DeviceState, +} + +impl CloudIICoreWireless { + pub fn new_from_state(state: DeviceState) -> Self { + let mut state = state; + state.connected = Some(true); + CloudIICoreWireless { state } + } + + pub fn new() -> Result { + let mut state = DeviceState::new(&PRODUCT_IDS, &VENDOR_IDS)?; + state.connected = Some(true); + Ok(CloudIICoreWireless { state }) + } +} + +impl Device for CloudIICoreWireless { + fn get_charging_packet(&self) -> Option> { + let mut tmp = BASE_PACKET.to_vec(); + tmp[1] = GET_CHARGING_CMD_ID; + Some(tmp) + } + + fn get_battery_packet(&self) -> Option> { + let mut tmp = BASE_PACKET.to_vec(); + tmp[1] = GET_BATTERY_CMD_ID; + Some(tmp) + } + + fn set_automatic_shut_down_packet(&self, shutdown_after: Duration) -> Option> { + let mut tmp = BASE_PACKET.to_vec(); + tmp[1] = SET_AUTO_SHUTDOWN_CMD_ID; + tmp[2] = (shutdown_after.as_secs() / 60) as u8; + Some(tmp) + } + + fn get_automatic_shut_down_packet(&self) -> Option> { + let mut tmp = BASE_PACKET.to_vec(); + tmp[1] = GET_AUTO_SHUTDOWN_CMD_ID; + Some(tmp) + } + + fn get_mute_packet(&self) -> Option> { + let mut tmp = BASE_PACKET.to_vec(); + tmp[1] = GET_MUTE_CMD_ID; + Some(tmp) + } + + fn set_mute_packet(&self, mute: bool) -> Option> { + let mut tmp = BASE_PACKET.to_vec(); + tmp[1] = SET_MUTE_CMD_ID; + tmp[2] = mute as u8; + Some(tmp) + } + + 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> { + let mut tmp = BASE_PACKET.to_vec(); + tmp[1] = GET_MIC_CONNECTED_CMD_ID; + Some(tmp) + } + + fn get_pairing_info_packet(&self) -> Option> { + let mut tmp = BASE_PACKET.to_vec(); + tmp[1] = GET_PAIRING_CMD_ID; + Some(tmp) + } + + fn get_product_color_packet(&self) -> Option> { + None + } + + fn get_side_tone_packet(&self) -> Option> { + let mut tmp = BASE_PACKET.to_vec(); + tmp[1] = GET_SIDE_TONE_ON_CMD_ID; + Some(tmp) + } + + fn set_side_tone_packet(&self, side_tone_on: bool) -> Option> { + let mut tmp = BASE_PACKET.to_vec(); + tmp[1] = SET_SIDE_TONE_ON_CMD_ID; + tmp[2] = side_tone_on as u8; + Some(tmp) + } + + fn get_side_tone_volume_packet(&self) -> Option> { + let mut tmp = BASE_PACKET.to_vec(); + tmp[1] = GET_SIDE_TONE_VOLUME_CMD_ID; + Some(tmp) + } + + fn set_side_tone_volume_packet(&self, volume: u8) -> Option> { + let mut tmp = BASE_PACKET.to_vec(); + tmp[1] = SET_SIDE_TONE_VOLUME_CMD_ID; + tmp[2] = (volume as i8).clamp(-5, 5) as u8; + Some(tmp) + } + + 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> { + let mut tmp = BASE_PACKET.to_vec(); + tmp[1] = GET_WIRELESS_STATUS_CMD_ID; + Some(tmp) + } + + fn get_sirk_packet(&self) -> Option> { + None + } + + fn reset_sirk_packet(&self) -> Option> { + None + } + + fn get_silent_mode_packet(&self) -> Option> { + let mut tmp = BASE_PACKET.to_vec(); + tmp[1] = GET_PLAY_BACK_MUTE_CMD_ID; + Some(tmp) + } + + fn set_silent_mode_packet(&self, silence: bool) -> Option> { + let mut tmp = BASE_PACKET.to_vec(); + tmp[1] = SET_PLAY_BACK_MUTE_CMD_ID; + tmp[2] = silence as u8; + Some(tmp) + } + + fn get_noise_gate_packet(&self) -> Option> { + let mut tmp = BASE_PACKET.to_vec(); + tmp[1] = GET_NOISE_GATE_CMD_ID; + Some(tmp) + } + + fn set_noise_gate_packet(&self, enable: bool) -> Option> { + let mut tmp = BASE_PACKET.to_vec(); + tmp[1] = SET_NOISE_GATE_CMD_ID; + tmp[2] = enable as u8; + Some(tmp) + } + + fn get_event_from_device_response(&self, response: &[u8]) -> Option> { + debug_println!("Read packet: {:?}", response); + if response[0] != 102 { + return None; + } + match (response[1], response[2], response[3], response[4]) { + (GET_CHARGING_CMD_ID, status, _, _) | (CHARGING_RESPONSE_ID, status, _, _) => { + Some(vec![DeviceEvent::Charging(ChargingStatus::from(status))]) + } + (GET_MIC_CONNECTED_CMD_ID, status, _, _) => { + Some(vec![DeviceEvent::MicConnected(status == 1)]) + } + (GET_BATTERY_CMD_ID, b2, b3, level) | (BATTERY_RESPONSE_ID, b2, b3, level) => { + if b2 != 0 || b3 != 0 { + Some(vec![DeviceEvent::BatterLevel(level)]) + } else { + None + } + } + (GET_AUTO_SHUTDOWN_CMD_ID, time, _, _) | (SET_AUTO_SHUTDOWN_CMD_ID, time, _, _) => { + Some(vec![DeviceEvent::AutomaticShutdownAfter( + Duration::from_secs(time as u64 * 60), + )]) + } + (SET_MUTE_CMD_ID, status, _, _) + | (GET_MUTE_CMD_ID, status, _, _) + | (MUTE_RESPONSE_ID, status, _, _) => Some(vec![DeviceEvent::Muted(status == 1)]), + (GET_PAIRING_CMD_ID, status, _, _) => Some(vec![DeviceEvent::PairingInfo(status)]), + (GET_SIDE_TONE_ON_CMD_ID, status, _, _) + | (SET_SIDE_TONE_ON_CMD_ID, status, _, _) + | (SIDE_TONE_RESPONSE_ID, status, _, _) => { + Some(vec![DeviceEvent::SideToneOn(status == 1)]) + } + (GET_SIDE_TONE_VOLUME_CMD_ID, status, _, _) + | (SET_SIDE_TONE_VOLUME_CMD_ID, status, _, _) => { + let status = if status >= 251 { + (status as i32 | -256i32) as u8 + } else if (0..=5).contains(&status) { + status + } else { + 0u8 + }; + Some(vec![DeviceEvent::SideToneVolume(status)]) + } + (GET_WIRELESS_STATUS_CMD_ID, status, _, _) + | (WIRELESS_STATUS_RESPONSE_ID, status, _, _) => { + Some(vec![DeviceEvent::WirelessConnected(status == 1)]) + } + (GET_PLAY_BACK_MUTE_CMD_ID, status, _, _) + | (SET_PLAY_BACK_MUTE_CMD_ID, status, _, _) => { + Some(vec![DeviceEvent::Silent(status == 1)]) + } + (GET_NOISE_GATE_CMD_ID, status, _, _) | (SET_NOISE_GATE_CMD_ID, status, _, _) => { + Some(vec![DeviceEvent::NoiseGateActive(status == 1)]) + } + _ => { + debug_println!("Unknown device event: {:?}", response); + None + } + } + } + + fn get_device_state(&self) -> &DeviceState { + &self.state + } + + fn get_device_state_mut(&mut self) -> &mut DeviceState { + &mut self.state + } + + fn allow_passive_refresh(&mut self) -> bool { + true + } +} diff --git a/src/devices/mod.rs b/src/devices/mod.rs index 3af4b40..29adb0b 100644 --- a/src/devices/mod.rs +++ b/src/devices/mod.rs @@ -1,4 +1,5 @@ pub mod cloud_alpha_wireless; +pub mod cloud_ii_core_wireless; pub mod cloud_ii_wireless; pub mod cloud_ii_wireless_dts; pub mod cloud_iii_s_wireless; @@ -7,9 +8,9 @@ pub mod cloud_iii_wireless; use crate::{ debug_println, devices::{ - cloud_alpha_wireless::CloudAlphaWireless, cloud_ii_wireless::CloudIIWireless, - cloud_ii_wireless_dts::CloudIIWirelessDTS, cloud_iii_s_wireless::CloudIIISWireless, - cloud_iii_wireless::CloudIIIWireless, + cloud_alpha_wireless::CloudAlphaWireless, cloud_ii_core_wireless::CloudIICoreWireless, + cloud_ii_wireless::CloudIIWireless, cloud_ii_wireless_dts::CloudIIWirelessDTS, + cloud_iii_s_wireless::CloudIIISWireless, cloud_iii_wireless::CloudIIIWireless, }, }; use hidapi::{HidApi, HidDevice, HidError}; @@ -50,6 +51,11 @@ const DEVICE_REGISTER: &[DeviceEntry] = &[ product_ids: &cloud_alpha_wireless::PRODUCT_IDS, factory: |s| Box::new(CloudAlphaWireless::new_from_state(s)), }, + DeviceEntry { + vendor_ids: &cloud_ii_core_wireless::VENDOR_IDS, + product_ids: &cloud_ii_core_wireless::PRODUCT_IDS, + factory: |s| Box::new(CloudIICoreWireless::new_from_state(s)), + }, ]; const RESPONSE_BUFFER_SIZE: usize = 256; @@ -103,6 +109,7 @@ pub struct DeviceState { pub voice_prompt_on: Option, pub connected: Option, pub silent: Option, + pub noise_gate_active: Option, // Capability flags - set once during device initialization pub can_set_mute: bool, pub can_set_surround_sound: bool, @@ -112,6 +119,7 @@ pub struct DeviceState { pub can_set_voice_prompt: bool, pub can_set_silent_mode: bool, pub can_set_equalizer: bool, + pub can_set_noise_gate: bool, } impl Display for DeviceState { @@ -208,6 +216,7 @@ impl DeviceState { voice_prompt_on: None, connected: None, silent: None, + noise_gate_active: None, // Capability flags - will be set by init_capabilities() can_set_mute: false, can_set_surround_sound: false, @@ -217,6 +226,7 @@ impl DeviceState { can_set_voice_prompt: false, can_set_silent_mode: false, can_set_equalizer: false, + can_set_noise_gate: false, }) } @@ -289,18 +299,24 @@ impl DeviceState { "", !self.can_set_voice_prompt, ), - ( - "Connected:", - self.connected.map(|c| c.to_string()), - "", - false, - ), ( "Playback muted:", self.silent.map(|c| c.to_string()), "", !self.can_set_silent_mode, ), + ( + "Noise gate active:", + self.noise_gate_active.map(|c| c.to_string()), + "", + !self.can_set_noise_gate, + ), + ( + "Connected:", + self.connected.map(|c| c.to_string()), + "", + false, + ), ] } @@ -351,8 +367,9 @@ impl DeviceState { DeviceEvent::WirelessConnected(connected) => self.connected = Some(*connected), DeviceEvent::Silent(silent) => self.silent = Some(*silent), DeviceEvent::RequireSIRKReset(reset) => { - println!("requested SIRK reset {reset}") + debug_println!("requested SIRK reset {reset}"); } + DeviceEvent::NoiseGateActive(on) => self.noise_gate_active = Some(*on), }; } @@ -370,6 +387,7 @@ impl DeviceState { self.voice_prompt_on = None; self.connected = None; self.silent = None; + self.noise_gate_active = None; } } @@ -403,6 +421,7 @@ pub enum DeviceEvent { SurroundSound(bool), Silent(bool), RequireSIRKReset(bool), + NoiseGateActive(bool), } #[derive(Debug, Copy, Clone)] @@ -504,6 +523,12 @@ pub trait Device { fn set_equalizer_band_packet(&self, _band_index: u8, _db_value: f32) -> Option> { None } + fn get_noise_gate_packet(&self) -> Option> { + None + } + fn set_noise_gate_packet(&self, _enable: bool) -> Option> { + None + } fn get_event_from_device_response(&self, response: &[u8]) -> Option>; fn get_device_state(&self) -> &DeviceState; fn get_device_state_mut(&mut self) -> &mut DeviceState; @@ -537,6 +562,9 @@ pub trait Device { fn can_set_equalizer(&self) -> bool { self.set_equalizer_band_packet(0, 0.0).is_some() } + fn can_set_noise_gate(&self) -> bool { + self.set_noise_gate_packet(true).is_none() + } // Initialize capability flags in device state fn init_capabilities(&mut self) { @@ -549,6 +577,7 @@ pub trait Device { let can_set_voice_prompt = self.can_set_voice_prompt(); let can_set_silent_mode = self.can_set_silent_mode(); let can_set_equalizer = self.can_set_equalizer(); + let can_set_noise_gate = self.can_set_noise_gate(); // Now set them in device state let state = self.get_device_state_mut(); @@ -560,6 +589,7 @@ pub trait Device { state.can_set_voice_prompt = can_set_voice_prompt; state.can_set_silent_mode = can_set_silent_mode; state.can_set_equalizer = can_set_equalizer; + state.can_set_noise_gate = can_set_noise_gate; } fn execute_headset_specific_functionality(&mut self) -> Result<(), DeviceError> { @@ -597,6 +627,7 @@ pub trait Device { self.get_voice_prompt_packet(), self.get_sirk_packet(), self.get_silent_mode_packet(), + self.get_noise_gate_packet(), ]; self.execute_headset_specific_functionality()?;