From 3065cfc19868a7d8a016523a5af13fc5a471f71e Mon Sep 17 00:00:00 2001 From: Lennard Kittner Date: Wed, 18 Feb 2026 12:57:44 +0100 Subject: [PATCH] Implement cloud alpha support --- src/devices/cloud_alpha_wireless.rs | 239 ++++++++++++++++++++++++++++ src/devices/mod.rs | 22 ++- 2 files changed, 256 insertions(+), 5 deletions(-) create mode 100644 src/devices/cloud_alpha_wireless.rs diff --git a/src/devices/cloud_alpha_wireless.rs b/src/devices/cloud_alpha_wireless.rs new file mode 100644 index 0000000..0c863d2 --- /dev/null +++ b/src/devices/cloud_alpha_wireless.rs @@ -0,0 +1,239 @@ +use crate::{ + debug_println, + devices::{ChargingStatus, Color, Device, DeviceError, DeviceEvent, DeviceState}, +}; +use std::time::Duration; + +const HP: u16 = 0x03F0; +pub const VENDOR_IDS: [u16; 1] = [HP]; +// Possible Cloud Alpha Wireless product IDs +pub const PRODUCT_IDS: [u16; 3] = [0x1743, 0x1765, 0x098D]; + +const BASE_PACKET: [u8; 64] = { + let mut packet = [0; 64]; + packet[0] = 33; + packet[1] = 187; + packet +}; + +const RESPONSE_BUFFER_SIZE: usize = 256; + +const GET_CHARGING_CMD_ID: u8 = 12; +const GET_CHARGING_RESPONSE_CODE: u8 = 38; +const GET_MIC_CONNECTED_CMD_ID: u8 = 8; +const GET_BATTERY_CMD_ID: u8 = 11; +const GET_BATTERY_RESPONSE_CODE: u8 = 37; +const GET_AUTO_SHUTDOWN_CMD_ID: u8 = 7; +const SET_AUTO_SHUTDOWN_CMD_ID: u8 = 18; +const GET_MUTE_CMD_ID: u8 = 10; +const GET_MUTE_RESPONSE_CODE: u8 = 35; +const SET_MUTE_CMD_ID: u8 = 21; +const GET_PAIRING_CMD_ID: u8 = 4; +const GET_PRODUCT_COLOR_CMD_ID: u8 = 14; +const GET_SIDE_TONE_ON_CMD_ID: u8 = 5; +const GET_SIDE_TONE_ON_RESPONSE_CODE: u8 = 34; +const SET_SIDE_TONE_ON_CMD_ID: u8 = 16; +const GET_SIDE_TONE_VOLUME_CMD_ID: u8 = 6; +const SET_SIDE_TONE_VOLUME_CMD_ID: u8 = 17; +const GET_VOICE_PROMPT_CMD_ID: u8 = 9; +const SET_VOICE_PROMPT_CMD_ID: u8 = 19; +const GET_WIRELESS_STATUS_CMD_ID: u8 = 3; +const GET_WIRELESS_STATUS_RESPONSE_CODE: u8 = 36; + +pub struct CloudAlphaWireless { + state: DeviceState, +} + +impl CloudAlphaWireless { + pub fn new_from_state(state: DeviceState) -> Self { + CloudAlphaWireless { state } + } + + pub fn new() -> Result { + let state = DeviceState::new(&PRODUCT_IDS, &VENDOR_IDS)?; + Ok(CloudAlphaWireless { state }) + } +} + +impl Device for CloudAlphaWireless { + fn get_response_buffer(&self) -> Vec { + let mut tmp = [0u8; RESPONSE_BUFFER_SIZE].to_vec(); + tmp[0] = 33; + tmp + } + + fn get_charging_packet(&self) -> Option> { + let mut tmp = BASE_PACKET.to_vec(); + tmp[2] = GET_CHARGING_CMD_ID; + Some(tmp) + } + + fn get_battery_packet(&self) -> Option> { + let mut tmp = BASE_PACKET.to_vec(); + tmp[2] = 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[2] = SET_AUTO_SHUTDOWN_CMD_ID; + tmp[3] = (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[2] = GET_AUTO_SHUTDOWN_CMD_ID; + Some(tmp) + } + + fn get_mute_packet(&self) -> Option> { + let mut tmp = BASE_PACKET.to_vec(); + tmp[2] = GET_MUTE_CMD_ID; + Some(tmp) + } + + fn set_mute_packet(&self, mute: bool) -> Option> { + let mut tmp = BASE_PACKET.to_vec(); + tmp[2] = SET_MUTE_CMD_ID; + tmp[3] = 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[2] = GET_MIC_CONNECTED_CMD_ID; + Some(tmp) + } + + fn get_pairing_info_packet(&self) -> Option> { + let mut tmp = BASE_PACKET.to_vec(); + tmp[2] = GET_PAIRING_CMD_ID; // Or 13 + Some(tmp) + } + + fn get_product_color_packet(&self) -> Option> { + let mut tmp = BASE_PACKET.to_vec(); + tmp[2] = GET_PRODUCT_COLOR_CMD_ID; + Some(tmp) + } + + fn get_side_tone_packet(&self) -> Option> { + let mut tmp = BASE_PACKET.to_vec(); + tmp[2] = 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[2] = SET_SIDE_TONE_ON_CMD_ID; + tmp[3] = side_tone_on as u8; + Some(tmp) + } + + fn get_side_tone_volume_packet(&self) -> Option> { + let mut tmp = BASE_PACKET.to_vec(); + tmp[2] = 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[2] = SET_SIDE_TONE_VOLUME_CMD_ID; + tmp[3] = volume; // correct? + Some(tmp) + } + + fn get_voice_prompt_packet(&self) -> Option> { + let mut tmp = BASE_PACKET.to_vec(); + tmp[2] = GET_VOICE_PROMPT_CMD_ID; + Some(tmp) + } + + fn set_voice_prompt_packet(&self, enable: bool) -> Option> { + let mut tmp = BASE_PACKET.to_vec(); + tmp[2] = SET_VOICE_PROMPT_CMD_ID; + tmp[3] = enable as u8; + Some(tmp) + } + + fn get_wireless_connected_status_packet(&self) -> Option> { + let mut tmp = BASE_PACKET.to_vec(); + tmp[2] = 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> { + None + } + + fn set_silent_mode_packet(&self, _silence: bool) -> Option> { + None + } + + fn get_event_from_device_response(&self, response: &[u8]) -> Option> { + debug_println!("Read packet: {:?}", response); + if response[0] != BASE_PACKET[0] || response[1] != BASE_PACKET[1] { + return None; + } + match response[2] { + GET_CHARGING_RESPONSE_CODE | GET_CHARGING_CMD_ID => Some(vec![DeviceEvent::Charging( + ChargingStatus::from(response[3]), + )]), + GET_MIC_CONNECTED_CMD_ID => Some(vec![DeviceEvent::MicConnected(response[3] == 1)]), + GET_BATTERY_RESPONSE_CODE | GET_BATTERY_CMD_ID => { + Some(vec![DeviceEvent::BatterLevel(response[3])]) + } + GET_AUTO_SHUTDOWN_CMD_ID => Some(vec![DeviceEvent::AutomaticShutdownAfter( + Duration::from_secs(response[3] as u64 * 60), + )]), + GET_MUTE_RESPONSE_CODE | GET_MUTE_CMD_ID => { + Some(vec![DeviceEvent::Muted(response[3] == 1)]) + } + GET_PAIRING_CMD_ID => Some(vec![DeviceEvent::PairingInfo(response[3])]), + GET_SIDE_TONE_ON_RESPONSE_CODE | GET_SIDE_TONE_ON_CMD_ID => { + Some(vec![DeviceEvent::SideToneOn(response[3] == 1)]) + } + GET_SIDE_TONE_VOLUME_CMD_ID => Some(vec![DeviceEvent::SideToneVolume(response[3])]), //Correct? + GET_WIRELESS_STATUS_RESPONSE_CODE | GET_WIRELESS_STATUS_CMD_ID => { + Some(vec![DeviceEvent::WirelessConnected(response[3] == 2)]) + } + GET_VOICE_PROMPT_CMD_ID => Some(vec![DeviceEvent::VoicePrompt(response[3] == 1)]), + GET_PRODUCT_COLOR_CMD_ID => { + Some(vec![DeviceEvent::ProductColor(Color::from(response[3]))]) + } + _ => { + 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 + } +} diff --git a/src/devices/mod.rs b/src/devices/mod.rs index 71cca88..c55c43d 100644 --- a/src/devices/mod.rs +++ b/src/devices/mod.rs @@ -1,3 +1,4 @@ +pub mod cloud_alpha_wireless; pub mod cloud_ii_wireless; pub mod cloud_ii_wireless_dts; pub mod cloud_iii_s_wireless; @@ -6,8 +7,9 @@ pub mod cloud_iii_wireless; use crate::{ debug_println, devices::{ - cloud_ii_wireless::CloudIIWireless, cloud_ii_wireless_dts::CloudIIWirelessDTS, - cloud_iii_s_wireless::CloudIIISWireless, cloud_iii_wireless::CloudIIIWireless, + cloud_alpha_wireless::CloudAlphaWireless, cloud_ii_wireless::CloudIIWireless, + cloud_ii_wireless_dts::CloudIIWirelessDTS, cloud_iii_s_wireless::CloudIIISWireless, + cloud_iii_wireless::CloudIIIWireless, }, }; use hidapi::{HidApi, HidDevice, HidError}; @@ -17,8 +19,9 @@ use thistermination::TerminationFull; // Possible vendor IDs [HyperX, HP] const VENDOR_IDS: [u16; 2] = [0x0951, 0x03F0]; // All supported product IDs -const PRODUCT_IDS: [u16; 10] = [ - 0x1718, 0x018B, 0x0D93, 0x0696, 0x0b92, 0x05B7, 0x16EA, 0x16EB, 0x0c9d, 0x06BE, +const PRODUCT_IDS: [u16; 13] = [ + 0x1718, 0x018B, 0x0D93, 0x0696, 0x0b92, 0x05B7, 0x16EA, 0x16EB, 0x0c9d, 0x06BE, 0x1743, 0x1765, + 0x098D, ]; const RESPONSE_BUFFER_SIZE: usize = 256; @@ -57,6 +60,12 @@ pub fn connect_compatible_device() -> Result, DeviceError> { { Box::new(CloudIIIWireless::new_from_state(state)) } + (v, p) + if cloud_alpha_wireless::VENDOR_IDS.contains(&v) + && cloud_alpha_wireless::PRODUCT_IDS.contains(&p) => + { + Box::new(CloudAlphaWireless::new_from_state(state)) + } (_, _) => return Err(DeviceError::NoDeviceFound()), }; @@ -421,6 +430,9 @@ impl From for ChargingStatus { } pub trait Device { + fn get_response_buffer(&self) -> Vec { + [0u8; RESPONSE_BUFFER_SIZE].to_vec() + } fn get_charging_packet(&self) -> Option>; fn get_battery_packet(&self) -> Option>; fn set_automatic_shut_down_packet(&self, shutdown_after: Duration) -> Option>; @@ -510,7 +522,7 @@ pub trait Device { Ok(()) } fn wait_for_updates(&mut self, duration: Duration) -> Option> { - let mut buf = [0u8; RESPONSE_BUFFER_SIZE]; + let mut buf = self.get_response_buffer(); let res = self .get_device_state() .hid_device