Add Cloud III S Wireless support (partial)
Add initial support for HyperX Cloud III S Wireless (PID 0x06BE). Working features: - Mic mute control via --mute true/false - Button event detection (volume up/down, play/pause) Not yet implemented (protocol not discovered): - Battery level query - Sidetone control - Auto-shutdown timer Cloud III S uses a different protocol than Cloud III: - Header 0x05 for mic control (20-byte packets) - Header 0x0f for button events (2-byte incoming) - Does not respond to Cloud III protocol (0x66 header) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,8 @@ 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 Stinger 2 Wireless
|
||||
- HyperX Cloud III S Wireless
|
||||
- HyperX Cloud Stinger 2 Wireless
|
||||
|
||||
It should be possible to add support for other HyperX headsets.
|
||||
|
||||
@@ -62,12 +63,14 @@ 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"
|
||||
|
||||
KERNEL=="hidraw*", ATTRS{idProduct}=="0d93", ATTRS{idVendor}=="03f0", MODE="0666"
|
||||
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"
|
||||
```
|
||||
|
||||
Once created, replug the wireless dongle.
|
||||
|
||||
201
src/devices/cloud_iii_s_wireless.rs
Normal file
201
src/devices/cloud_iii_s_wireless.rs
Normal file
@@ -0,0 +1,201 @@
|
||||
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;
|
||||
|
||||
// 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<u8> {
|
||||
let mut packet = vec![0u8; PACKET_SIZE];
|
||||
packet[0] = MIC_HEADER;
|
||||
packet[1] = if mute { MIC_OFF_CMD } else { MIC_ON_CMD };
|
||||
packet
|
||||
}
|
||||
|
||||
pub struct CloudIIISWireless {
|
||||
state: DeviceState,
|
||||
}
|
||||
|
||||
impl CloudIIISWireless {
|
||||
pub fn new_from_state(state: DeviceState) -> Self {
|
||||
CloudIIISWireless { state }
|
||||
}
|
||||
|
||||
pub fn new() -> Result<Self, DeviceError> {
|
||||
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<Vec<u8>> {
|
||||
None
|
||||
}
|
||||
|
||||
// Cloud III S: Battery query not discovered yet
|
||||
fn get_battery_packet(&self) -> Option<Vec<u8>> {
|
||||
None
|
||||
}
|
||||
|
||||
// Cloud III S: Auto shutdown not discovered yet
|
||||
fn set_automatic_shut_down_packet(&self, _shutdown_after: Duration) -> Option<Vec<u8>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn get_automatic_shut_down_packet(&self) -> Option<Vec<u8>> {
|
||||
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<Vec<u8>> {
|
||||
None
|
||||
}
|
||||
|
||||
// Cloud III S: Mic control - CONFIRMED WORKING
|
||||
fn set_mute_packet(&self, mute: bool) -> Option<Vec<u8>> {
|
||||
Some(make_mic_packet(mute))
|
||||
}
|
||||
|
||||
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>> {
|
||||
None
|
||||
}
|
||||
|
||||
// Cloud III S: Sidetone not discovered yet
|
||||
fn get_side_tone_packet(&self) -> Option<Vec<u8>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_side_tone_packet(&self, _side_tone_on: bool) -> Option<Vec<u8>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn get_side_tone_volume_packet(&self) -> Option<Vec<u8>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_side_tone_volume_packet(&self, _volume: u8) -> Option<Vec<u8>> {
|
||||
None
|
||||
}
|
||||
|
||||
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>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn get_sirk_packet(&self) -> Option<Vec<u8>> {
|
||||
None
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
fn get_event_from_device_response(&self, response: &[u8]) -> Option<Vec<DeviceEvent>> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
@@ -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; 6] = [0x1718, 0x018B, 0x0D93, 0x0696, 0x0b92, 0x05B7];
|
||||
const PRODUCT_IDS: [u16; 7] = [0x1718, 0x018B, 0x0D93, 0x0696, 0x0b92, 0x05B7, 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<Box<dyn Device>, 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) =>
|
||||
|
||||
Reference in New Issue
Block a user