From b0265629a467676aa748aef59542a61d6d6d7fc1 Mon Sep 17 00:00:00 2001 From: Lennard Kittner Date: Sun, 19 Oct 2025 14:05:10 +0200 Subject: [PATCH] Add WIP cloud 3 support --- src/bin/packet_tester.rs | 2 - src/devices/cloud_ii_wireless.rs | 40 +++-- src/devices/cloud_ii_wireless_dts.rs | 20 ++- src/devices/cloud_iii_wireless.rs | 232 +++++++++++++++++++++++++++ src/devices/mod.rs | 46 +++++- src/main.rs | 2 - 6 files changed, 315 insertions(+), 27 deletions(-) create mode 100644 src/devices/cloud_iii_wireless.rs diff --git a/src/bin/packet_tester.rs b/src/bin/packet_tester.rs index 32de0fa..fd6cebf 100644 --- a/src/bin/packet_tester.rs +++ b/src/bin/packet_tester.rs @@ -1,5 +1,3 @@ -use std::u8; - use hidapi::{DeviceInfo, HidApi}; const VENDOR_IDS: [u16; 2] = [0x0951, 0x03F0]; diff --git a/src/devices/cloud_ii_wireless.rs b/src/devices/cloud_ii_wireless.rs index 07e6f91..fce7007 100644 --- a/src/devices/cloud_ii_wireless.rs +++ b/src/devices/cloud_ii_wireless.rs @@ -1,5 +1,5 @@ use crate::devices::{ChargingStatus, Device, DeviceError, DeviceEvent, DeviceState}; -use std::{time::Duration, u8}; +use std::time::Duration; const HYPERX: u16 = 0x0951; pub const VENDOR_IDS: [u16; 1] = [HYPERX]; @@ -27,8 +27,6 @@ const BASE_PACKET: [u8; 62] = { tmp }; -// I am unsure about all the other command ids - const GET_CHARGING_CMD_ID: u8 = 3; const GET_BATTERY_CMD_ID: u8 = 2; const GET_AUTO_SHUTDOWN_CMD_ID: u8 = 26; @@ -94,6 +92,21 @@ impl Device for CloudIIWireless { None } + fn get_surround_sound_packet(&self) -> Option> { + let mut tmp = [0u8; 62]; + tmp[0] = 6; + tmp[2] = 0; + tmp[4] = u8::MAX; + tmp[7] = 104; + tmp[8] = 74; + tmp[9] = 142; + Some(tmp.to_vec()) + } + + fn set_surround_sound_packet(&self, _surround_sound: bool) -> Option> { + None + } + fn get_mic_connected_packet(&self) -> Option> { None } @@ -137,18 +150,19 @@ impl Device for CloudIIWireless { None } - fn get_surround_sound_packet(&self) -> Option> { - let mut tmp = [0u8; 62]; - tmp[0] = 6; - tmp[2] = 0; - tmp[4] = u8::MAX; - tmp[7] = 104; - tmp[8] = 74; - tmp[9] = 142; - Some(tmp.to_vec()) + fn get_sirk_packet(&self) -> Option> { + None } - fn set_surround_sound_packet(&self, _surround_sound: bool) -> Option> { + 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 } diff --git a/src/devices/cloud_ii_wireless_dts.rs b/src/devices/cloud_ii_wireless_dts.rs index 4fd47dd..57b708f 100644 --- a/src/devices/cloud_ii_wireless_dts.rs +++ b/src/devices/cloud_ii_wireless_dts.rs @@ -91,6 +91,14 @@ impl Device for CloudIIWirelessDTS { 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[3] = GET_MIC_CONNECTED_CMD_ID; @@ -159,11 +167,19 @@ impl Device for CloudIIWirelessDTS { Some(tmp) } - fn get_surround_sound_packet(&self) -> Option> { + fn get_sirk_packet(&self) -> Option> { None } - fn set_surround_sound_packet(&self, _surround_sound: bool) -> Option> { + 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 } diff --git a/src/devices/cloud_iii_wireless.rs b/src/devices/cloud_iii_wireless.rs new file mode 100644 index 0000000..408aa94 --- /dev/null +++ b/src/devices/cloud_iii_wireless.rs @@ -0,0 +1,232 @@ +use crate::devices::{ChargingStatus, Color, Device, DeviceError, DeviceEvent, DeviceState}; +use std::{time::Duration, vec}; + +const HP: u16 = 0x03F0; +pub const VENDOR_IDS: [u16; 1] = [HP]; +pub const PRODUCT_IDS: [u16; 1] = [0x05B7]; + +const BASE_PACKET: [u8; 62] = { + let mut packet = [0; 62]; + packet[0] = 102; + packet +}; + +// sirk probably stands for Set Identity Resolving Key +const RESET_SIRK_CMD_ID: u8 = 30; +const GET_SIRK_CMD_ID: u8 = 131; +const GET_SILENT_MODE_CMD_ID: u8 = 135; +const SET_SILENT_MODE_CMD_ID: u8 = 4; +const GET_CHARGING_CMD_ID: u8 = 138; +const CHARGING_RESPONSE_ID: u8 = 12; +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_PRODUCT_COLOR_CMD_ID: u8 = 143; +const GET_SIDE_TONE_ON_CMD_ID: u8 = 132; +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; + +// OR GetDongleStatus +const GET_WIRELESS_STATUS_CMD_ID: u8 = 130; +const WIRELESS_STATUS_RESPONSE_ID: u8 = 11; + +pub struct CloudIIIWireless { + state: DeviceState, +} + +impl CloudIIIWireless { + pub fn new_from_state(state: DeviceState) -> Self { + CloudIIIWireless { state } + } + + pub fn new() -> Result { + let state = DeviceState::new(&PRODUCT_IDS, &VENDOR_IDS)?; + Ok(CloudIIIWireless { state }) + } +} + +impl Device for CloudIIIWireless { + 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> { + None + } + + fn get_pairing_info_packet(&self) -> Option> { + None + } + + fn get_product_color_packet(&self) -> Option> { + let mut tmp = BASE_PACKET.to_vec(); + tmp[1] = GET_PRODUCT_COLOR_CMD_ID; + Some(tmp) + } + + 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; + 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> { + let mut tmp = BASE_PACKET.to_vec(); + tmp[1] = GET_SIRK_CMD_ID; + Some(tmp) + } + + fn reset_sirk_packet(&self) -> Option> { + let mut tmp = BASE_PACKET.to_vec(); + tmp[1] = RESET_SIRK_CMD_ID; + Some(tmp) + } + + fn get_silent_mode_packet(&self) -> Option> { + let mut tmp = BASE_PACKET.to_vec(); + tmp[1] = GET_SILENT_MODE_CMD_ID; + Some(tmp) + } + + fn set_silent_mode_packet(&self, silence: bool) -> Option> { + let mut tmp = BASE_PACKET.to_vec(); + tmp[1] = SET_SILENT_MODE_CMD_ID; + tmp[2] = silence as u8; + Some(tmp) + } + + fn get_event_from_device_response(&self, response: &[u8]) -> Option> { + if response[0] != 102 { + return None; + } + match (response[1], response[2], response[3], response[4]) { + (GET_MUTE_CMD_ID, mute, ..) | (MUTE_RESPONSE_ID, mute, ..) => { + Some(vec![DeviceEvent::Muted(mute == 1)]) + } + (GET_WIRELESS_STATUS_CMD_ID, connected, ..) + | (WIRELESS_STATUS_RESPONSE_ID, connected, ..) => { + Some(vec![DeviceEvent::WirelessConnected(connected == 1)]) + } + (GET_CHARGING_CMD_ID, charging, ..) | (CHARGING_RESPONSE_ID, charging, ..) => { + Some(vec![DeviceEvent::Charging(ChargingStatus::from(charging))]) + } + (GET_BATTERY_CMD_ID, state1, state2, level) + | (BATTERY_RESPONSE_ID, state1, state2, level) => { + if state1 != 0 || state2 != 0 { + Some(vec![DeviceEvent::BatterLevel(level)]) + } else { + None + } + } + (GET_AUTO_SHUTDOWN_CMD_ID, off_after, ..) => { + Some(vec![DeviceEvent::AutomaticShutdownAfter( + Duration::from_secs(off_after as u64 * 60), + )]) + } + (GET_PRODUCT_COLOR_CMD_ID, color, ..) => { + Some(vec![DeviceEvent::ProductColor(Color::from(color))]) + } + (GET_SILENT_MODE_CMD_ID, silent, ..) => Some(vec![DeviceEvent::Silent(silent == 1)]), + (GET_SIRK_CMD_ID, ..) => { + let mut flag = false; + for i in 2..18 { + if response[i] != 0 { + flag = true; + break; + } + } + Some(vec![DeviceEvent::RequireSIRKReset(flag)]) + } + _ => None, + } + } + + fn get_device_state(&self) -> &DeviceState { + &self.state + } + + fn get_device_state_mut(&mut self) -> &mut DeviceState { + &mut self.state + } +} diff --git a/src/devices/mod.rs b/src/devices/mod.rs index e590829..4720f76 100644 --- a/src/devices/mod.rs +++ b/src/devices/mod.rs @@ -1,8 +1,10 @@ pub mod cloud_ii_wireless; pub mod cloud_ii_wireless_dts; +pub mod cloud_iii_wireless; use crate::devices::{ cloud_ii_wireless::CloudIIWireless, cloud_ii_wireless_dts::CloudIIWirelessDTS, + cloud_iii_wireless::CloudIIIWireless, }; use hidapi::{HidApi, HidDevice, HidError}; use std::{fmt::Display, time::Duration}; @@ -10,8 +12,8 @@ use thistermination::TerminationFull; // Possible vendor IDs [HyperX, HP] const VENDOR_IDS: [u16; 2] = [0x0951, 0x03F0]; -// Possible Cloud II Wireless product IDs -const PRODUCT_IDS: [u16; 5] = [0x1718, 0x018B, 0x0D93, 0x0696, 0x0b92]; +// All supported product IDs +const PRODUCT_IDS: [u16; 6] = [0x1718, 0x018B, 0x0D93, 0x0696, 0x0b92, 0x05B7]; const RESPONSE_BUFFER_SIZE: usize = 256; const RESPONSE_DELAY: Duration = Duration::from_millis(50); @@ -36,6 +38,12 @@ pub fn connect_compatible_device() -> Result, DeviceError> { { Ok(Box::new(CloudIIWirelessDTS::new_from_state(state))) } + (v, p) + if cloud_iii_wireless::VENDOR_IDS.contains(&v) + && cloud_iii_wireless::PRODUCT_IDS.contains(&p) => + { + Ok(Box::new(CloudIIIWireless::new_from_state(state))) + } (_, _) => Err(DeviceError::NoDeviceFound()), } } @@ -58,6 +66,7 @@ pub struct DeviceState { pub surround_sound: Option, pub voice_prompt_on: Option, pub connected: Option, + pub silent: Option, } impl Display for DeviceState { @@ -77,7 +86,8 @@ Side tone on: {} Side tone volume: {} Surround sound: {} Voice prompt on: {} -Connected: {}", +Connected: {} +Silent: {}", self.device_name.clone().unwrap_or("Unknown".to_string()), self.battery_level .map_or(unknown.clone(), |l| format!("{l}%")), @@ -98,6 +108,7 @@ Connected: {}", self.voice_prompt_on .map_or(unknown.clone(), |v| v.to_string()), self.connected.map_or(unknown.clone(), |c| c.to_string()), + self.silent.map_or(unknown.clone(), |s| s.to_string()), ) } } @@ -140,6 +151,7 @@ impl DeviceState { side_tone_volume: None, voice_prompt_on: None, connected: None, + silent: None, }) } @@ -157,7 +169,8 @@ Side tone on: {} Side tone volume: {} Surround sound: {} Voice prompt on: {} -Connected: {}", +Connected: {} +Silent: {}", self.battery_level .map_or(unknown.clone(), |l| format!("{l}%")), self.charging.map_or(unknown.clone(), |c| c.to_string()), @@ -177,6 +190,7 @@ Connected: {}", self.voice_prompt_on .map_or(unknown.clone(), |v| v.to_string()), self.connected.map_or(unknown.clone(), |c| c.to_string()), + self.silent.map_or(unknown.clone(), |s| s.to_string()), ) } @@ -196,6 +210,10 @@ Connected: {}", DeviceEvent::SurroundSound(status) => self.surround_sound = Some(*status), DeviceEvent::VoicePrompt(on) => self.voice_prompt_on = Some(*on), DeviceEvent::WirelessConnected(connected) => self.connected = Some(*connected), + DeviceEvent::Silent(silent) => self.silent = Some(*silent), + DeviceEvent::RequireSIRKReset(reset) => { + println!("requested SIRK reset {reset}") + } }; } @@ -212,6 +230,7 @@ Connected: {}", self.side_tone_volume = None; self.voice_prompt_on = None; self.connected = None; + self.silent = None; } } @@ -243,11 +262,14 @@ pub enum DeviceEvent { VoicePrompt(bool), WirelessConnected(bool), SurroundSound(bool), + Silent(bool), + RequireSIRKReset(bool), } #[derive(Debug, Copy, Clone)] pub enum Color { - Red, + BlackBlack, + BlackRed, UnknownColor(u8), } @@ -257,7 +279,8 @@ impl Display for Color { f, "{}", match self { - Color::Red => "Red".to_string(), + Color::BlackBlack => "Black".to_string(), + Color::BlackRed => "Red".to_string(), Color::UnknownColor(n) => format!("Unknown color {}", n), } ) @@ -267,7 +290,8 @@ impl Display for Color { impl From for Color { fn from(color: u8) -> Self { match color { - 0 => Color::Red, + 0 => Color::BlackBlack, + 2 => Color::BlackRed, _ => Color::UnknownColor(color), } } @@ -326,6 +350,10 @@ 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_sirk_packet(&self) -> Option>; + fn reset_sirk_packet(&self) -> Option>; + fn get_silent_mode_packet(&self) -> Option>; + fn set_silent_mode_packet(&self, silence: bool) -> 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; @@ -363,6 +391,8 @@ pub trait Device { self.get_side_tone_packet(), self.get_side_tone_volume_packet(), self.get_voice_prompt_packet(), + self.get_sirk_packet(), + self.get_silent_mode_packet(), ]; self.execute_headset_specific_functionality()?; @@ -380,7 +410,7 @@ pub trait Device { } responded = true; } - if !self.get_device_state().connected.map_or(true, |c| c) { + if !self.get_device_state().connected.is_none_or(|c| c) { break; } } diff --git a/src/main.rs b/src/main.rs index 06d163a..efa763e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -44,8 +44,6 @@ fn main() { Ok(()) => (), Err(error) => { eprintln!("{error}"); - //TODO: only set to none on headsets where connection can be detected - // device.get_device_state_mut().connected = None; tray_handler.update(device.get_device_state()); break; // try to reconnect }