From 6840cec0f21a773ac128f035f7790eb90885e9ba Mon Sep 17 00:00:00 2001 From: Lennard Kittner Date: Sun, 24 Aug 2025 12:15:40 +0200 Subject: [PATCH] Add support for old cloud 2 --- src/bin/hyper_headset_cli.rs | 27 ++-- src/devices/cloud_ii_wireless.rs | 195 +++++++++++++++++++++++++++ src/devices/cloud_ii_wireless_dts.rs | 44 ++++-- src/devices/mod.rs | 43 ++++-- src/main.rs | 2 +- 5 files changed, 279 insertions(+), 32 deletions(-) create mode 100644 src/devices/cloud_ii_wireless.rs diff --git a/src/bin/hyper_headset_cli.rs b/src/bin/hyper_headset_cli.rs index 223df40..5699b72 100644 --- a/src/bin/hyper_headset_cli.rs +++ b/src/bin/hyper_headset_cli.rs @@ -21,7 +21,7 @@ fn main() { Arg::new("mute") .long("mute") .required(false) - .help("Mute or un mute the headset.") + .help("Mute or unmute the headset.") .value_parser(clap::value_parser!(bool)), ) .arg( @@ -45,6 +45,13 @@ fn main() { .help("Enable voice prompt. This may not be supported on your device.") .value_parser(clap::value_parser!(bool)), ) + .arg( + Arg::new("surround_sound") + .long("surround_sound") + .required(false) + .help("Enables surround sound. This may be on by default and cannot be changed on your device.") + .value_parser(clap::value_parser!(bool)), + ) .get_matches(); let mut device = match connect_compatible_device() { @@ -108,6 +115,16 @@ fn main() { } } + if let Some(surround_sound) = matches.get_one::("surround_sound") { + if let Some(packet) = device.set_surround_sound_packet(*surround_sound) { + if let Err(err) = device.get_device_state().hid_device.write(&packet) { + println!("Failed to set surround sound with error: {:?}", err) + } + } else { + println!("Can't change surround sound on this device") + } + } + std::thread::sleep(Duration::from_secs_f64(0.5)); if let Err(error) = device.active_refresh_state() { @@ -116,11 +133,3 @@ fn main() { }; println!("{}", device.get_device_state()); } - -#[test] -fn test_basic_device_access() { - let _ = match CloudIIWirelessDTS::new() { - Ok(device) => device, - Err(_) => return, - }; -} diff --git a/src/devices/cloud_ii_wireless.rs b/src/devices/cloud_ii_wireless.rs new file mode 100644 index 0000000..284fa4e --- /dev/null +++ b/src/devices/cloud_ii_wireless.rs @@ -0,0 +1,195 @@ +use crate::devices::{ChargingStatus, Device, DeviceError, DeviceEvent, DeviceState}; +use std::time::Duration; + +const HYPERX: u16 = 0x0951; +pub const VENDOR_IDS: [u16; 1] = [HYPERX]; +// Possible Cloud II Wireless product IDs +pub const PRODUCT_IDS: [u16; 3] = [0x1718, 0x018B, 0x0b92]; + +const BASE_PACKET: [u8; 62] = { + let mut tmp = [0u8; 62]; + tmp[0] = 0x06; + tmp[1] = 0x00; + tmp[2] = 0x02; + tmp[3] = 0x00; + tmp[4] = 0x9A; + tmp[5] = 0x00; + tmp[6] = 0x00; + tmp[7] = 0x68; + tmp[8] = 0x4A; + tmp[9] = 0x8E; + tmp[10] = 0x0A; + tmp[11] = 0x00; + tmp[12] = 0x00; + tmp[13] = 0x00; + tmp[14] = 0xBB; + tmp[15] = 0x01; + tmp +}; + +// I am unsure about all the other command ids + +const GET_CHARGING_CMD_ID: u8 = 3; +// const GET_MIC_CONNECTED_CMD_ID: u8 = 8; +const GET_BATTERY_CMD_ID: u8 = 2; +const GET_AUTO_SHUTDOWN_CMD_ID: u8 = 26; +const SET_AUTO_SHUTDOWN_CMD_ID: u8 = 24; +// includes also some other information such as side tone and surround sound +const GET_MUTE_CMD_ID: u8 = 1; +// const SET_MUTE_CMD_ID: u8 = 32; +// const GET_PAIRING_CMD_ID: u8 = 9; +// const GET_PRODUCT_COLOR_CMD_ID: u8 = 14; +// const GET_SIDE_TONE_ON_CMD_ID: u8 = 6; +const SET_SIDE_TONE_ON_CMD_ID: u8 = 25; +// const GET_SIDE_TONE_VOLUME_CMD_ID: u8 = 11; +// const SET_SIDE_TONE_VOLUME_CMD_ID: u8 = 35; +// const GET_VOICE_PROMPT_CMD_ID: u8 = 9; +// const SET_VOICE_PROMPT_CMD_ID: u8 = 19; +// const GET_WIRELESS_STATUS_CMD_ID: u8 = 1; + +pub struct CloudIIWireless { + state: DeviceState, +} + +impl CloudIIWireless { + pub fn new_from_state(state: DeviceState) -> Self { + CloudIIWireless { state } + } + + pub fn new() -> Result { + let state = DeviceState::new(&PRODUCT_IDS, &VENDOR_IDS)?; + Ok(CloudIIWireless { state }) + } +} + +impl Device for CloudIIWireless { + fn get_charging_packet(&self) -> Option> { + let mut tmp = BASE_PACKET.to_vec(); + tmp[15] = GET_CHARGING_CMD_ID; + Some(tmp) + } + + fn get_battery_packet(&self) -> Option> { + let mut tmp = BASE_PACKET.to_vec(); + tmp[15] = 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[15] = SET_AUTO_SHUTDOWN_CMD_ID; + tmp[16] = (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[15] = GET_AUTO_SHUTDOWN_CMD_ID; + Some(tmp) + } + + fn get_mute_packet(&self) -> Option> { + let mut tmp = BASE_PACKET.to_vec(); + tmp[15] = GET_MUTE_CMD_ID; + Some(tmp) + } + + fn set_mute_packet(&self, _mute: 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 + } + + fn get_side_tone_packet(&self) -> Option> { + None + } + + fn set_side_tone_packet(&self, side_tone_on: bool) -> Option> { + let mut tmp = BASE_PACKET.to_vec(); + tmp[15] = SET_SIDE_TONE_ON_CMD_ID; + tmp[16] = side_tone_on as u8; + Some(tmp) + } + + 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_surround_sound_packet(&self) -> Option> { + None + } + + fn set_surround_sound_packet(&self, _surround_sound: bool) -> Option> { + None + } + + fn get_event_from_device_response(&self, response: &[u8]) -> Option> { + if response.len() < 7 { + return None; + } + println!("Received packet: {:?}", response); + match (response[3], response[7], response[12], response[14]) { + (GET_BATTERY_CMD_ID, level, _, _) => Some(vec![DeviceEvent::BatterLevel(level)]), + (GET_CHARGING_CMD_ID, status, _, _) => { + Some(vec![DeviceEvent::Charging(ChargingStatus::from(status))]) + } + (GET_AUTO_SHUTDOWN_CMD_ID, shutdown, _, _) => { + Some(vec![DeviceEvent::AutomaticShutdownAfter( + Duration::from_secs(shutdown as u64 * 60), + )]) + } + (GET_MUTE_CMD_ID, _, surround, other) => Some(vec![ + DeviceEvent::SideToneOn((other & 16) != 0), + DeviceEvent::Muted((other & 2) != 0), + DeviceEvent::SurroundSound((surround & 2) != 0), + ]), + _ => { + 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 prepare_write(&mut self) { + let mut input_report_buffer = [0u8; 64]; + input_report_buffer[0] = 6; + self.state + .hid_device + .get_input_report(&mut input_report_buffer) + .unwrap(); + } +} diff --git a/src/devices/cloud_ii_wireless_dts.rs b/src/devices/cloud_ii_wireless_dts.rs index ba1ed01..b856af6 100644 --- a/src/devices/cloud_ii_wireless_dts.rs +++ b/src/devices/cloud_ii_wireless_dts.rs @@ -159,35 +159,51 @@ impl Device for CloudIIWirelessDTS { Some(tmp) } - fn get_event_from_device_response(&self, response: &[u8]) -> Option { + fn get_surround_sound_packet(&self) -> Option> { + None + } + + fn set_surround_sound_packet(&self, _surround_sound: bool) -> Option> { + None + } + + fn get_event_from_device_response(&self, response: &[u8]) -> Option> { if response.len() < 7 { return None; } match (response[2], response[3], response[4], response[7]) { (_, GET_CHARGING_CMD_ID, status, _) => { - Some(DeviceEvent::Charging(ChargingStatus::from(status))) + Some(vec![DeviceEvent::Charging(ChargingStatus::from(status))]) } (_, GET_MIC_CONNECTED_CMD_ID, status, _) => { - Some(DeviceEvent::MicConnected(status == 1)) + Some(vec![DeviceEvent::MicConnected(status == 1)]) + } + (_, GET_BATTERY_CMD_ID, _, level) => Some(vec![DeviceEvent::BatterLevel(level)]), + (_, GET_AUTO_SHUTDOWN_CMD_ID, time, _) => { + Some(vec![DeviceEvent::AutomaticShutdownAfter( + Duration::from_secs(time as u64 * 60), + )]) } - (_, GET_BATTERY_CMD_ID, _, level) => Some(DeviceEvent::BatterLevel(level)), - (_, GET_AUTO_SHUTDOWN_CMD_ID, time, _) => Some(DeviceEvent::AutomaticShutdownAfter( - Duration::from_secs(time as u64 * 60), - )), (_, SET_MUTE_CMD_ID, status, _) | (_, GET_MUTE_CMD_ID, status, _) => { - Some(DeviceEvent::Muted(status == 1)) + Some(vec![DeviceEvent::Muted(status == 1)]) + } + (_, GET_PAIRING_CMD_ID, status, _) => Some(vec![DeviceEvent::PairingInfo(status)]), + (_, GET_SIDE_TONE_ON_CMD_ID, status, _) => { + Some(vec![DeviceEvent::SideToneOn(status == 1)]) } - (_, GET_PAIRING_CMD_ID, status, _) => Some(DeviceEvent::PairingInfo(status)), - (_, GET_SIDE_TONE_ON_CMD_ID, status, _) => Some(DeviceEvent::SideToneOn(status == 1)), (_, GET_SIDE_TONE_VOLUME_CMD_ID, status, _) => { - Some(DeviceEvent::SideToneVolume(status)) + Some(vec![DeviceEvent::SideToneVolume(status)]) } (_, GET_WIRELESS_STATUS_CMD_ID, status, _) => { - Some(DeviceEvent::WirelessConnected(status == 1 || status == 4)) + Some(vec![DeviceEvent::WirelessConnected( + status == 1 || status == 4, + )]) + } + (GET_VOICE_PROMPT_CMD_ID, status, _, _) => { + Some(vec![DeviceEvent::VoicePrompt(status == 1)]) } - (GET_VOICE_PROMPT_CMD_ID, status, _, _) => Some(DeviceEvent::VoicePrompt(status == 1)), (GET_PRODUCT_COLOR_CMD_ID, status, _, _) => { - Some(DeviceEvent::ProductColor(Color::from(status))) + Some(vec![DeviceEvent::ProductColor(Color::from(status))]) } _ => { println!("Unknown device event: {:?}", response); diff --git a/src/devices/mod.rs b/src/devices/mod.rs index e3c3010..f7ec8ac 100644 --- a/src/devices/mod.rs +++ b/src/devices/mod.rs @@ -52,6 +52,7 @@ pub struct DeviceState { pub product_color: Option, pub side_tone_on: Option, pub side_tone_volume: Option, + pub surround_sound: Option, pub voice_prompt_on: Option, pub connected: Option, } @@ -71,6 +72,7 @@ Pairing info: {} Product color: {} Side tone on: {} Side tone volume: {} +Surround sound: {} Voice prompt on: {} Connected: {}", self.device_name.clone().unwrap_or("Unknown".to_string()), @@ -88,6 +90,8 @@ Connected: {}", self.side_tone_on.map_or(unknown.clone(), |s| s.to_string()), self.side_tone_volume .map_or(unknown.clone(), |s| s.to_string()), + self.surround_sound + .map_or(unknown.clone(), |s| s.to_string()), self.voice_prompt_on .map_or(unknown.clone(), |v| v.to_string()), self.connected.map_or(unknown.clone(), |c| c.to_string()), @@ -124,6 +128,7 @@ impl DeviceState { charging: None, battery_level: None, muted: None, + surround_sound: None, mic_connected: None, automatic_shutdown_after: None, pairing_info: None, @@ -147,6 +152,7 @@ Pairing info: {} Product color: {} Side tone on: {} Side tone volume: {} +Surround sound: {} Voice prompt on: {} Connected: {}", self.battery_level @@ -163,6 +169,8 @@ Connected: {}", self.side_tone_on.map_or(unknown.clone(), |s| s.to_string()), self.side_tone_volume .map_or(unknown.clone(), |s| s.to_string()), + self.surround_sound + .map_or(unknown.clone(), |s| s.to_string()), self.voice_prompt_on .map_or(unknown.clone(), |v| v.to_string()), self.connected.map_or(unknown.clone(), |c| c.to_string()), @@ -182,6 +190,7 @@ Connected: {}", DeviceEvent::ProductColor(color) => self.product_color = Some(*color), DeviceEvent::SideToneOn(side) => self.side_tone_on = Some(*side), DeviceEvent::SideToneVolume(volume) => self.side_tone_volume = Some(*volume), + DeviceEvent::SurroundSound(status) => self.surround_sound = Some(*status), DeviceEvent::VoicePrompt(on) => self.voice_prompt_on = Some(*on), DeviceEvent::WirelessConnected(connected) => self.connected = Some(*connected), }; @@ -191,7 +200,15 @@ Connected: {}", self.charging = None; self.battery_level = None; self.muted = None; + self.surround_sound = None; self.mic_connected = None; + self.automatic_shutdown_after = None; + self.pairing_info = None; + self.product_color = None; + self.side_tone_on = None; + self.side_tone_volume = None; + self.voice_prompt_on = None; + self.connected = None; } } @@ -222,6 +239,7 @@ pub enum DeviceEvent { SideToneVolume(u8), VoicePrompt(bool), WirelessConnected(bool), + SurroundSound(bool), } #[derive(Debug, Copy, Clone)] @@ -293,6 +311,8 @@ pub trait Device { fn get_automatic_shut_down_packet(&self) -> Option>; fn get_mute_packet(&self) -> Option>; fn set_mute_packet(&self, mute: bool) -> Option>; + fn get_surround_sound_packet(&self) -> Option>; + fn set_surround_sound_packet(&self, surround_sound: bool) -> Option>; fn get_mic_connected_packet(&self) -> Option>; fn get_pairing_info_packet(&self) -> Option>; fn get_product_color_packet(&self) -> Option>; @@ -303,11 +323,11 @@ pub trait Device { fn get_voice_prompt_packet(&self) -> Option>; fn set_voice_prompt_packet(&self, enable: bool) -> Option>; fn get_wireless_connected_status_packet(&self) -> Option>; - fn get_event_from_device_response(&self, response: &[u8]) -> Option; + 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; fn prepare_write(&mut self) {} - fn wait_for_updates(&mut self, duration: Duration) -> Option { + fn wait_for_updates(&mut self, duration: Duration) -> Option> { let mut buf = [0u8; 8]; let res = self .get_device_state() @@ -330,6 +350,7 @@ pub trait Device { self.get_battery_packet(), self.get_automatic_shut_down_packet(), self.get_mute_packet(), + self.get_surround_sound_packet(), self.get_mic_connected_packet(), self.get_pairing_info_packet(), self.get_product_color_packet(), @@ -342,8 +363,10 @@ pub trait Device { for packet in packets.into_iter().flatten() { self.prepare_write(); self.get_device_state().hid_device.write(&packet)?; - if let Some(event) = self.wait_for_updates(Duration::from_secs(1)) { - self.get_device_state_mut().update_self_with_event(&event); + if let Some(events) = self.wait_for_updates(Duration::from_secs(1)) { + for event in events { + self.get_device_state_mut().update_self_with_event(&event); + } responded = true; } if !self.get_device_state().connected.map_or(true, |c| c) { @@ -361,13 +384,17 @@ pub trait Device { /// Refreshes the state by listening for events /// Only the battery level is actively queried because it is not communicated by the device on its own fn passive_refresh_state(&mut self) -> Result<(), DeviceError> { - if let Some(event) = self.wait_for_updates(Duration::from_secs(1)) { - self.get_device_state_mut().update_self_with_event(&event); + if let Some(events) = self.wait_for_updates(Duration::from_secs(1)) { + for event in events { + self.get_device_state_mut().update_self_with_event(&event); + } } if let Some(batter_packet) = self.get_battery_packet() { self.get_device_state().hid_device.write(&batter_packet)?; - if let Some(event) = self.wait_for_updates(Duration::from_secs(1)) { - self.get_device_state_mut().update_self_with_event(&event); + if let Some(events) = self.wait_for_updates(Duration::from_secs(1)) { + for event in events { + self.get_device_state_mut().update_self_with_event(&event); + } } } diff --git a/src/main.rs b/src/main.rs index 1ed6908..bb07c27 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,7 +35,7 @@ fn main() { loop { std::thread::sleep(refresh_interval); // with the default refresh_interval the state is only actively queried every 3min - // quiting the device to frequently can lead to instability + // querying the device to frequently can lead to instability match if run_counter % 60 == 0 { device.active_refresh_state() } else {