diff --git a/README.md b/README.md index bc9d938..0a05e55 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ 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 III S Wireless - HyperX Cloud Stinger 2 Wireless - HyperX Cloud Flight S @@ -63,6 +64,7 @@ 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" SUBSYSTEMS=="usb", ATTRS{idProduct}=="16ea", ATTRS{idVendor}=="0951", MODE="0666" SUBSYSTEMS=="usb", ATTRS{idProduct}=="0c9d", ATTRS{idVendor}=="03f0", MODE="0666" @@ -71,6 +73,7 @@ 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" KERNEL=="hidraw*", ATTRS{idProduct}=="16ea", ATTRS{idVendor}=="0951", MODE="0666" KERNEL=="hidraw*", ATTRS{idProduct}=="0c9d", ATTRS{idVendor}=="03f0", MODE="0666" ``` diff --git a/src/devices/cloud_iii_s_wireless.rs b/src/devices/cloud_iii_s_wireless.rs new file mode 100644 index 0000000..cc17f9f --- /dev/null +++ b/src/devices/cloud_iii_s_wireless.rs @@ -0,0 +1,249 @@ +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; + +// Auto-shutdown control (via SET_REPORT, report ID 0x0c) +// Packet structure: 0c 02 03 00 00 4a XX 00... (64 bytes total) +// XX values: 00=disabled, 02=10min, 04=20min, 07=30min +const AUTO_SHUTDOWN_REPORT_ID: u8 = 0x0c; +const AUTO_SHUTDOWN_CMD: [u8; 5] = [0x02, 0x03, 0x00, 0x00, 0x4a]; +const AUTO_SHUTDOWN_PACKET_SIZE: usize = 64; + +// Equalizer control (via SET_REPORT, report ID 0x0c) +// Packet structure: 0c 02 03 00 00 5f [band] [value_hi] [value_lo] 00... (64 bytes total) +// band: 0-9 (32Hz, 64Hz, 125Hz, 250Hz, 500Hz, 1kHz, 2kHz, 4kHz, 8kHz, 16kHz) +// value: signed 16-bit big-endian, dB * 100 (e.g., +6.0dB = 600, -3.5dB = -350) +const EQ_REPORT_ID: u8 = 0x0c; +const EQ_CMD: [u8; 5] = [0x02, 0x03, 0x00, 0x00, 0x5f]; +const EQ_PACKET_SIZE: usize = 64; + +// 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 +} + +fn make_auto_shutdown_packet(minutes: u64) -> Vec { + let mut packet = vec![0u8; AUTO_SHUTDOWN_PACKET_SIZE]; + packet[0] = AUTO_SHUTDOWN_REPORT_ID; + packet[1..6].copy_from_slice(&AUTO_SHUTDOWN_CMD); + // Value is 16-bit big-endian seconds + let seconds = (minutes * 60) as u16; + packet[6] = (seconds >> 8) as u8; // High byte + packet[7] = (seconds & 0xFF) as u8; // Low byte + packet +} + +fn make_equalizer_band_packet(band_index: u8, db_value: f32) -> Vec { + let mut packet = vec![0u8; EQ_PACKET_SIZE]; + packet[0] = EQ_REPORT_ID; + packet[1..6].copy_from_slice(&EQ_CMD); + packet[6] = band_index; + // Convert dB to device units (dB * 100), clamp to ±12dB range + let value_int = (db_value * 100.0).clamp(-1200.0, 1200.0) as i16; + let value_bytes = value_int.to_be_bytes(); + packet[7] = value_bytes[0]; // High byte + packet[8] = value_bytes[1]; // Low byte + 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 via SET_REPORT (report ID 0x0c) + fn set_automatic_shut_down_packet(&self, shutdown_after: Duration) -> Option> { + let minutes = shutdown_after.as_secs() / 60; + Some(make_auto_shutdown_packet(minutes)) + } + + 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 + } + + // Cloud III S: Equalizer control - CONFIRMED WORKING + fn set_equalizer_band_packet(&self, band_index: u8, db_value: f32) -> Option> { + if band_index > 9 { + return None; + } + Some(make_equalizer_band_packet(band_index, db_value)) + } + + 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 9080fcb..9dec88d 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; 8] = [0x1718, 0x018B, 0x0D93, 0x0696, 0x0b92, 0x05B7, 0x16EA, 0x0c9d]; +const PRODUCT_IDS: [u16; 9] = [0x1718, 0x018B, 0x0D93, 0x0696, 0x0b92, 0x05B7, 0x16EA, 0x0c9d, 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) => @@ -83,6 +90,7 @@ pub struct DeviceState { pub can_set_side_tone_volume: bool, pub can_set_voice_prompt: bool, pub can_set_silent_mode: bool, + pub can_set_equalizer: bool, } impl Display for DeviceState { @@ -138,6 +146,7 @@ impl DeviceState { can_set_side_tone_volume: false, can_set_voice_prompt: false, can_set_silent_mode: false, + can_set_equalizer: false, }) } @@ -414,6 +423,11 @@ pub trait Device { fn reset_sirk_packet(&self) -> Option>; fn get_silent_mode_packet(&self) -> Option>; fn set_silent_mode_packet(&self, silence: bool) -> Option>; + /// Set equalizer band (0-9) to dB value (-12.0 to +12.0) + /// Bands: 0=32Hz, 1=64Hz, 2=125Hz, 3=250Hz, 4=500Hz, 5=1kHz, 6=2kHz, 7=4kHz, 8=8kHz, 9=16kHz + fn set_equalizer_band_packet(&self, _band_index: u8, _db_value: f32) -> 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; @@ -444,6 +458,9 @@ pub trait Device { fn can_set_silent_mode(&self) -> bool { self.set_silent_mode_packet(false).is_some() } + fn can_set_equalizer(&self) -> bool { + self.set_equalizer_band_packet(0, 0.0).is_some() + } // Initialize capability flags in device state fn init_capabilities(&mut self) { @@ -455,6 +472,7 @@ pub trait Device { let can_set_side_tone_volume = self.can_set_side_tone_volume(); 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(); // Now set them in device state let state = self.get_device_state_mut(); @@ -465,6 +483,7 @@ pub trait Device { state.can_set_side_tone_volume = can_set_side_tone_volume; 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; } fn execute_headset_specific_functionality(&mut self) -> Result<(), DeviceError> {