Add WIP cloud 3 support

This commit is contained in:
Lennard Kittner
2025-10-19 14:05:10 +02:00
parent ba53b55d48
commit b0265629a4
6 changed files with 315 additions and 27 deletions

View File

@@ -1,5 +1,3 @@
use std::u8;
use hidapi::{DeviceInfo, HidApi};
const VENDOR_IDS: [u16; 2] = [0x0951, 0x03F0];

View File

@@ -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<Vec<u8>> {
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<Vec<u8>> {
None
}
fn get_mic_connected_packet(&self) -> Option<Vec<u8>> {
None
}
@@ -137,18 +150,19 @@ impl Device for CloudIIWireless {
None
}
fn get_surround_sound_packet(&self) -> Option<Vec<u8>> {
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<Vec<u8>> {
None
}
fn set_surround_sound_packet(&self, _surround_sound: bool) -> Option<Vec<u8>> {
fn reset_sirk_packet(&self) -> Option<Vec<u8>> {
None
}
fn get_silent_mode_packet(&self) -> Option<Vec<u8>> {
None
}
fn set_silent_mode_packet(&self, _silence: bool) -> Option<Vec<u8>> {
None
}

View File

@@ -91,6 +91,14 @@ impl Device for CloudIIWirelessDTS {
Some(tmp)
}
fn get_surround_sound_packet(&self) -> Option<Vec<u8>> {
None
}
fn set_surround_sound_packet(&self, _surround_sound: bool) -> Option<Vec<u8>> {
None
}
fn get_mic_connected_packet(&self) -> Option<Vec<u8>> {
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<Vec<u8>> {
fn get_sirk_packet(&self) -> Option<Vec<u8>> {
None
}
fn set_surround_sound_packet(&self, _surround_sound: bool) -> Option<Vec<u8>> {
fn reset_sirk_packet(&self) -> Option<Vec<u8>> {
None
}
fn get_silent_mode_packet(&self) -> Option<Vec<u8>> {
None
}
fn set_silent_mode_packet(&self, _silence: bool) -> Option<Vec<u8>> {
None
}

View File

@@ -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<Self, DeviceError> {
let state = DeviceState::new(&PRODUCT_IDS, &VENDOR_IDS)?;
Ok(CloudIIIWireless { state })
}
}
impl Device for CloudIIIWireless {
fn get_charging_packet(&self) -> Option<Vec<u8>> {
let mut tmp = BASE_PACKET.to_vec();
tmp[1] = GET_CHARGING_CMD_ID;
Some(tmp)
}
fn get_battery_packet(&self) -> Option<Vec<u8>> {
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<Vec<u8>> {
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<Vec<u8>> {
let mut tmp = BASE_PACKET.to_vec();
tmp[1] = GET_AUTO_SHUTDOWN_CMD_ID;
Some(tmp)
}
fn get_mute_packet(&self) -> Option<Vec<u8>> {
let mut tmp = BASE_PACKET.to_vec();
tmp[1] = GET_MUTE_CMD_ID;
Some(tmp)
}
fn set_mute_packet(&self, mute: bool) -> Option<Vec<u8>> {
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<Vec<u8>> {
None
}
fn set_surround_sound_packet(&self, _surround_sound: bool) -> Option<Vec<u8>> {
None
}
fn get_mic_connected_packet(&self) -> Option<Vec<u8>> {
None
}
fn get_pairing_info_packet(&self) -> Option<Vec<u8>> {
None
}
fn get_product_color_packet(&self) -> Option<Vec<u8>> {
let mut tmp = BASE_PACKET.to_vec();
tmp[1] = GET_PRODUCT_COLOR_CMD_ID;
Some(tmp)
}
fn get_side_tone_packet(&self) -> Option<Vec<u8>> {
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<Vec<u8>> {
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<Vec<u8>> {
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<Vec<u8>> {
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<Vec<u8>> {
None
}
fn set_voice_prompt_packet(&self, _enable: bool) -> Option<Vec<u8>> {
None
}
fn get_wireless_connected_status_packet(&self) -> Option<Vec<u8>> {
let mut tmp = BASE_PACKET.to_vec();
tmp[1] = GET_WIRELESS_STATUS_CMD_ID;
Some(tmp)
}
fn get_sirk_packet(&self) -> Option<Vec<u8>> {
let mut tmp = BASE_PACKET.to_vec();
tmp[1] = GET_SIRK_CMD_ID;
Some(tmp)
}
fn reset_sirk_packet(&self) -> Option<Vec<u8>> {
let mut tmp = BASE_PACKET.to_vec();
tmp[1] = RESET_SIRK_CMD_ID;
Some(tmp)
}
fn get_silent_mode_packet(&self) -> Option<Vec<u8>> {
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<Vec<u8>> {
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<Vec<DeviceEvent>> {
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
}
}

View File

@@ -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<Box<dyn Device>, 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<bool>,
pub voice_prompt_on: Option<bool>,
pub connected: Option<bool>,
pub silent: Option<bool>,
}
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<u8> 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<Vec<u8>>;
fn set_voice_prompt_packet(&self, enable: bool) -> Option<Vec<u8>>;
fn get_wireless_connected_status_packet(&self) -> Option<Vec<u8>>;
fn get_sirk_packet(&self) -> Option<Vec<u8>>;
fn reset_sirk_packet(&self) -> Option<Vec<u8>>;
fn get_silent_mode_packet(&self) -> Option<Vec<u8>>;
fn set_silent_mode_packet(&self, silence: bool) -> Option<Vec<u8>>;
fn get_event_from_device_response(&self, response: &[u8]) -> Option<Vec<DeviceEvent>>;
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;
}
}

View File

@@ -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
}