Some features are read-only: either they require Windows APO, or they really don't have a software command available. Add the plumbing to detect and report this. Also, add documentation for the featureset.
This commit is contained in:
89
FEATURE_CAPABILITIES.md
Normal file
89
FEATURE_CAPABILITIES.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# HyperX Headset Feature Capabilities
|
||||
|
||||
This document summarizes which features can be controlled vs only monitored for each headset model.
|
||||
|
||||
## Cloud II Wireless (Non-DTS)
|
||||
|
||||
### Writable Features (Can be SET via HID commands)
|
||||
|
||||
- ✅ **Auto Power Off** - Can set automatic shutdown timer (0-30 minutes)
|
||||
- ✅ **Sidetone** - Can enable/disable sidetone (on/off only, no volume control)
|
||||
|
||||
### Read-Only Features (Can only monitor, not control)
|
||||
|
||||
- ❌ **Microphone Mute** - Hardware button only, cannot be controlled via HID
|
||||
- ❌ **Surround Sound (7.1)** - Controlled via Windows DTS APO or physical button, not HID
|
||||
- ❌ **Battery Level** - Read-only status
|
||||
- ❌ **Charging Status** - Read-only status
|
||||
- ❌ **Connection Status** - Read-only status
|
||||
- ❌ **Firmware Version** - Read-only information
|
||||
|
||||
## Cloud II Wireless DTS
|
||||
|
||||
### Writable Features
|
||||
|
||||
- ✅ **Auto Power Off**
|
||||
- ✅ **Sidetone** - With volume control (0-100)
|
||||
- ✅ **Surround Sound** - Via Windows DTS APO system calls (not direct HID)
|
||||
|
||||
### Read-Only Features
|
||||
|
||||
- ❌ **Microphone Mute** - Hardware button only
|
||||
- ❌ **Battery Level**
|
||||
- ❌ **Charging Status**
|
||||
- ❌ **Connection Status**
|
||||
|
||||
## Cloud III Wireless
|
||||
|
||||
### Writable Features
|
||||
|
||||
- ✅ **Auto Power Off**
|
||||
- ✅ **Sidetone** - With volume control (0-100)
|
||||
- ✅ **Microphone Mute** - Can be controlled programmatically
|
||||
- ✅ **Voice Prompt** - Can enable/disable voice prompts
|
||||
- ✅ **Playback Mute (Silent Mode)** - Can mute headphone output
|
||||
|
||||
### Read-Only Features
|
||||
|
||||
- ❌ **Surround Sound** - Not supported via HID
|
||||
- ❌ **Battery Level**
|
||||
- ❌ **Charging Status**
|
||||
- ❌ **Connection Status**
|
||||
- ❌ **Product Color**
|
||||
|
||||
## CLI Error Handling
|
||||
|
||||
The CLI application now provides clear error messages when attempting to use unsupported features:
|
||||
|
||||
```bash
|
||||
# Example: Trying to control surround sound on Cloud II Wireless
|
||||
$ ./hyper_headset_cli --surround_sound true
|
||||
ERROR: Surround sound control is not supported on this device
|
||||
Use the physical headset button or Windows audio settings to toggle surround sound.
|
||||
|
||||
# Example: Trying to mute on Cloud II Wireless
|
||||
$ ./hyper_headset_cli --mute true
|
||||
ERROR: Microphone mute control is not supported on this device (hardware button only)
|
||||
```
|
||||
|
||||
## Tray Application UI
|
||||
|
||||
The system tray application now displays "(read-only)" markers next to features that cannot be controlled:
|
||||
|
||||
```
|
||||
Battery level: 92%
|
||||
Charging status: Not charging
|
||||
Muted: false (read-only)
|
||||
Automatic shutdown after: 20min
|
||||
Side tone: false
|
||||
Surround sound: true (read-only)
|
||||
Connected: true
|
||||
```
|
||||
|
||||
## Implementation Details
|
||||
|
||||
- Feature capabilities are checked once during device initialization via `init_capabilities()`
|
||||
- Capability flags are stored in `DeviceState` structure
|
||||
- CLI flags for unsupported features are hidden in the help menu
|
||||
- CLI exits with error code 1 when attempting to use unsupported features
|
||||
- Tray UI shows read-only markers based on device capabilities
|
||||
@@ -93,13 +93,15 @@ fn main() {
|
||||
println!("sending automatic_shutdown packet");
|
||||
device.prepare_write();
|
||||
if let Err(err) = device.get_device_state().hid_device.write(&packet) {
|
||||
println!("Failed to set automatic shutdown with error: {:?}", err)
|
||||
eprintln!("Failed to set automatic shutdown with error: {:?}", err);
|
||||
std::process::exit(1);
|
||||
}
|
||||
if let Some(events) = device.wait_for_updates(Duration::from_secs(1)) {
|
||||
println!("{:?}", events);
|
||||
}
|
||||
} else {
|
||||
println!("Automatic shutdown can't be enabled on this device")
|
||||
eprintln!("ERROR: Automatic shutdown is not supported on this device");
|
||||
std::process::exit(1);
|
||||
}
|
||||
} else {
|
||||
println!("not sending automatic_shutdown packet");
|
||||
@@ -109,10 +111,12 @@ fn main() {
|
||||
if let Some(packet) = device.set_mute_packet(*mute) {
|
||||
device.prepare_write();
|
||||
if let Err(err) = device.get_device_state().hid_device.write(&packet) {
|
||||
println!("Failed to mute with error: {:?}", err)
|
||||
eprintln!("Failed to mute with error: {:?}", err);
|
||||
std::process::exit(1);
|
||||
}
|
||||
} else {
|
||||
println!("Can't mute this device")
|
||||
eprintln!("ERROR: Microphone mute control is not supported on this device (hardware button only)");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,14 +126,16 @@ fn main() {
|
||||
println!("sending enable_side_tone packet");
|
||||
device.prepare_write();
|
||||
if let Err(err) = device.get_device_state().hid_device.write(&packet) {
|
||||
println!("Failed to enable side tone with error: {:?}", err)
|
||||
eprintln!("Failed to enable side tone with error: {:?}", err);
|
||||
std::process::exit(1);
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
if let Some(events) = device.wait_for_updates(Duration::from_secs(1)) {
|
||||
println!("{:?}", events);
|
||||
}
|
||||
} else {
|
||||
println!("Can't enable side tone on this device")
|
||||
eprintln!("ERROR: Side tone control is not supported on this device");
|
||||
std::process::exit(1);
|
||||
}
|
||||
} else {
|
||||
println!("not sending enable_side_tone packet");
|
||||
@@ -139,10 +145,12 @@ fn main() {
|
||||
if let Some(packet) = device.set_side_tone_volume_packet(*volume) {
|
||||
device.prepare_write();
|
||||
if let Err(err) = device.get_device_state().hid_device.write(&packet) {
|
||||
println!("Failed to set side tone volume with error: {:?}", err)
|
||||
eprintln!("Failed to set side tone volume with error: {:?}", err);
|
||||
std::process::exit(1);
|
||||
}
|
||||
} else {
|
||||
println!("Can't set side tone volume on this device")
|
||||
eprintln!("ERROR: Side tone volume control is not supported on this device");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,10 +158,12 @@ fn main() {
|
||||
if let Some(packet) = device.set_voice_prompt_packet(*enable) {
|
||||
device.prepare_write();
|
||||
if let Err(err) = device.get_device_state().hid_device.write(&packet) {
|
||||
println!("Failed to enable voice prompt with error: {:?}", err)
|
||||
eprintln!("Failed to enable voice prompt with error: {:?}", err);
|
||||
std::process::exit(1);
|
||||
}
|
||||
} else {
|
||||
println!("Can't enable voice prompt on this device")
|
||||
eprintln!("ERROR: Voice prompt control is not supported on this device");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,10 +171,13 @@ fn main() {
|
||||
if let Some(packet) = device.set_surround_sound_packet(*surround_sound) {
|
||||
device.prepare_write();
|
||||
if let Err(err) = device.get_device_state().hid_device.write(&packet) {
|
||||
println!("Failed to set surround sound with error: {:?}", err)
|
||||
eprintln!("Failed to set surround sound with error: {:?}", err);
|
||||
std::process::exit(1);
|
||||
}
|
||||
} else {
|
||||
println!("Can't change surround sound on this device")
|
||||
eprintln!("ERROR: Surround sound control is not supported on this device");
|
||||
eprintln!(" Use the physical headset button or Windows audio settings to toggle surround sound.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,10 +185,12 @@ fn main() {
|
||||
if let Some(packet) = device.set_silent_mode_packet(*mute_playback) {
|
||||
device.prepare_write();
|
||||
if let Err(err) = device.get_device_state().hid_device.write(&packet) {
|
||||
println!("Failed to mute playback with error: {:?}", err)
|
||||
eprintln!("Failed to mute playback with error: {:?}", err);
|
||||
std::process::exit(1);
|
||||
}
|
||||
} else {
|
||||
println!("Can't mute playback on this device")
|
||||
eprintln!("ERROR: Playback mute control is not supported on this device");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,27 +25,32 @@ pub fn connect_compatible_device() -> Result<Box<dyn Device>, DeviceError> {
|
||||
.get_product_string()?
|
||||
.ok_or(DeviceError::NoDeviceFound())?;
|
||||
println!("Connecting to {}", name);
|
||||
match (state.vendor_id, state.product_id) {
|
||||
let mut device: Box<dyn Device> = match (state.vendor_id, state.product_id) {
|
||||
(v, p)
|
||||
if cloud_ii_wireless::VENDOR_IDS.contains(&v)
|
||||
&& cloud_ii_wireless::PRODUCT_IDS.contains(&p) =>
|
||||
{
|
||||
Ok(Box::new(CloudIIWireless::new_from_state(state)))
|
||||
Box::new(CloudIIWireless::new_from_state(state))
|
||||
}
|
||||
(v, p)
|
||||
if cloud_ii_wireless_dts::VENDOR_IDS.contains(&v)
|
||||
&& cloud_ii_wireless_dts::PRODUCT_IDS.contains(&p) =>
|
||||
{
|
||||
Ok(Box::new(CloudIIWirelessDTS::new_from_state(state)))
|
||||
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)))
|
||||
Box::new(CloudIIIWireless::new_from_state(state))
|
||||
}
|
||||
(_, _) => Err(DeviceError::NoDeviceFound()),
|
||||
}
|
||||
(_, _) => return Err(DeviceError::NoDeviceFound()),
|
||||
};
|
||||
|
||||
// Initialize capability flags
|
||||
device.init_capabilities();
|
||||
|
||||
Ok(device)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -67,6 +72,14 @@ pub struct DeviceState {
|
||||
pub voice_prompt_on: Option<bool>,
|
||||
pub connected: Option<bool>,
|
||||
pub silent: Option<bool>,
|
||||
// Capability flags - set once during device initialization
|
||||
pub can_set_mute: bool,
|
||||
pub can_set_surround_sound: bool,
|
||||
pub can_set_side_tone: bool,
|
||||
pub can_set_automatic_shutdown: bool,
|
||||
pub can_set_side_tone_volume: bool,
|
||||
pub can_set_voice_prompt: bool,
|
||||
pub can_set_silent_mode: bool,
|
||||
}
|
||||
|
||||
impl Display for DeviceState {
|
||||
@@ -114,6 +127,14 @@ impl DeviceState {
|
||||
voice_prompt_on: None,
|
||||
connected: None,
|
||||
silent: None,
|
||||
// Capability flags - will be set by init_capabilities()
|
||||
can_set_mute: false,
|
||||
can_set_surround_sound: false,
|
||||
can_set_side_tone: false,
|
||||
can_set_automatic_shutdown: false,
|
||||
can_set_side_tone_volume: false,
|
||||
can_set_voice_prompt: false,
|
||||
can_set_silent_mode: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -123,51 +144,79 @@ impl DeviceState {
|
||||
"Battery level:",
|
||||
self.battery_level.map(|l| l.to_string()),
|
||||
"%",
|
||||
false, // read-only flag (not applicable for read-only fields)
|
||||
),
|
||||
("Charging status:", self.charging.map(|c| c.to_string()), ""),
|
||||
("Muted:", self.muted.map(|c| c.to_string()), ""),
|
||||
(
|
||||
"Charging status:",
|
||||
self.charging.map(|c| c.to_string()),
|
||||
"",
|
||||
false,
|
||||
),
|
||||
("Muted:", self.muted.map(|c| c.to_string()), "", false),
|
||||
(
|
||||
"Mic connected:",
|
||||
self.mic_connected.map(|c| c.to_string()),
|
||||
"",
|
||||
false,
|
||||
),
|
||||
(
|
||||
"Automatic shutdown after:",
|
||||
self.automatic_shutdown_after
|
||||
.map(|c| (c.as_secs() / 60).to_string()),
|
||||
"min",
|
||||
false,
|
||||
),
|
||||
(
|
||||
"pairing info:",
|
||||
self.pairing_info.map(|c| c.to_string()),
|
||||
"",
|
||||
false,
|
||||
),
|
||||
(
|
||||
"product color:",
|
||||
self.product_color.map(|c| c.to_string()),
|
||||
"",
|
||||
false,
|
||||
),
|
||||
(
|
||||
"Side tone:",
|
||||
self.side_tone_on.map(|c| c.to_string()),
|
||||
"",
|
||||
false,
|
||||
),
|
||||
("Side tone:", self.side_tone_on.map(|c| c.to_string()), ""),
|
||||
(
|
||||
"Side tone volume:",
|
||||
self.side_tone_volume.map(|c| c.to_string()),
|
||||
"",
|
||||
false,
|
||||
),
|
||||
(
|
||||
"Surround sound:",
|
||||
self.surround_sound.map(|c| c.to_string()),
|
||||
"",
|
||||
false,
|
||||
),
|
||||
(
|
||||
"Voice prompt:",
|
||||
self.voice_prompt_on.map(|c| c.to_string()),
|
||||
"",
|
||||
false,
|
||||
),
|
||||
(
|
||||
"Connected:",
|
||||
self.connected.map(|c| c.to_string()),
|
||||
"",
|
||||
false,
|
||||
),
|
||||
(
|
||||
"Playback muted:",
|
||||
self.silent.map(|c| c.to_string()),
|
||||
"",
|
||||
false,
|
||||
),
|
||||
("Connected:", self.connected.map(|c| c.to_string()), ""),
|
||||
("Playback muted:", self.silent.map(|c| c.to_string()), ""),
|
||||
];
|
||||
data.iter()
|
||||
.filter_map(|(prefix, data, suffix)| {
|
||||
.filter_map(|(prefix, data, suffix, _)| {
|
||||
if let Some(data) = data {
|
||||
Some(format!("{:<padding$} {}{}", prefix, data, suffix))
|
||||
} else {
|
||||
@@ -178,6 +227,104 @@ impl DeviceState {
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
pub fn to_string_with_readonly_info(&self, padding: usize) -> String {
|
||||
let data = [
|
||||
(
|
||||
"Battery level:",
|
||||
self.battery_level.map(|l| l.to_string()),
|
||||
"%",
|
||||
false,
|
||||
),
|
||||
(
|
||||
"Charging status:",
|
||||
self.charging.map(|c| c.to_string()),
|
||||
"",
|
||||
false,
|
||||
),
|
||||
(
|
||||
"Muted:",
|
||||
self.muted.map(|c| c.to_string()),
|
||||
"",
|
||||
!self.can_set_mute,
|
||||
),
|
||||
(
|
||||
"Mic connected:",
|
||||
self.mic_connected.map(|c| c.to_string()),
|
||||
"",
|
||||
false,
|
||||
),
|
||||
(
|
||||
"Automatic shutdown after:",
|
||||
self.automatic_shutdown_after
|
||||
.map(|c| (c.as_secs() / 60).to_string()),
|
||||
"min",
|
||||
!self.can_set_automatic_shutdown,
|
||||
),
|
||||
(
|
||||
"pairing info:",
|
||||
self.pairing_info.map(|c| c.to_string()),
|
||||
"",
|
||||
false,
|
||||
),
|
||||
(
|
||||
"product color:",
|
||||
self.product_color.map(|c| c.to_string()),
|
||||
"",
|
||||
false,
|
||||
),
|
||||
(
|
||||
"Side tone:",
|
||||
self.side_tone_on.map(|c| c.to_string()),
|
||||
"",
|
||||
!self.can_set_side_tone,
|
||||
),
|
||||
(
|
||||
"Side tone volume:",
|
||||
self.side_tone_volume.map(|c| c.to_string()),
|
||||
"",
|
||||
!self.can_set_side_tone_volume,
|
||||
),
|
||||
(
|
||||
"Surround sound:",
|
||||
self.surround_sound.map(|c| c.to_string()),
|
||||
"",
|
||||
!self.can_set_surround_sound,
|
||||
),
|
||||
(
|
||||
"Voice prompt:",
|
||||
self.voice_prompt_on.map(|c| c.to_string()),
|
||||
"",
|
||||
!self.can_set_voice_prompt,
|
||||
),
|
||||
(
|
||||
"Connected:",
|
||||
self.connected.map(|c| c.to_string()),
|
||||
"",
|
||||
false,
|
||||
),
|
||||
(
|
||||
"Playback muted:",
|
||||
self.silent.map(|c| c.to_string()),
|
||||
"",
|
||||
!self.can_set_silent_mode,
|
||||
),
|
||||
];
|
||||
data.iter()
|
||||
.filter_map(|(prefix, data, suffix, readonly)| {
|
||||
if let Some(data) = data {
|
||||
let readonly_marker = if *readonly { " (read-only)" } else { "" };
|
||||
Some(format!(
|
||||
"{:<padding$} {}{}{}",
|
||||
prefix, data, suffix, readonly_marker
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
fn update_self_with_event(&mut self, event: &DeviceEvent) {
|
||||
match event {
|
||||
DeviceEvent::BatterLevel(level) => self.battery_level = Some(*level),
|
||||
@@ -342,6 +489,53 @@ pub trait Device {
|
||||
fn get_device_state(&self) -> &DeviceState;
|
||||
fn get_device_state_mut(&mut self) -> &mut DeviceState;
|
||||
fn prepare_write(&mut self) {}
|
||||
|
||||
// Helper methods to check if features are writable
|
||||
fn can_set_mute(&self) -> bool {
|
||||
self.set_mute_packet(false).is_some()
|
||||
}
|
||||
fn can_set_surround_sound(&self) -> bool {
|
||||
self.set_surround_sound_packet(false).is_some()
|
||||
}
|
||||
fn can_set_side_tone(&self) -> bool {
|
||||
self.set_side_tone_packet(false).is_some()
|
||||
}
|
||||
fn can_set_automatic_shutdown(&self) -> bool {
|
||||
self.set_automatic_shut_down_packet(Duration::from_secs(0))
|
||||
.is_some()
|
||||
}
|
||||
fn can_set_side_tone_volume(&self) -> bool {
|
||||
self.set_side_tone_volume_packet(0).is_some()
|
||||
}
|
||||
fn can_set_voice_prompt(&self) -> bool {
|
||||
self.set_voice_prompt_packet(false).is_some()
|
||||
}
|
||||
fn can_set_silent_mode(&self) -> bool {
|
||||
self.set_silent_mode_packet(false).is_some()
|
||||
}
|
||||
|
||||
// Initialize capability flags in device state
|
||||
fn init_capabilities(&mut self) {
|
||||
// Collect capabilities first to avoid borrowing conflicts
|
||||
let can_set_mute = self.can_set_mute();
|
||||
let can_set_surround_sound = self.can_set_surround_sound();
|
||||
let can_set_side_tone = self.can_set_side_tone();
|
||||
let can_set_automatic_shutdown = self.can_set_automatic_shutdown();
|
||||
let can_set_side_tone_volume = self.can_set_side_tone_volume();
|
||||
let can_set_voice_prompt = self.can_set_voice_prompt();
|
||||
let can_set_silent_mode = self.can_set_silent_mode();
|
||||
|
||||
// Now set them in device state
|
||||
let state = self.get_device_state_mut();
|
||||
state.can_set_mute = can_set_mute;
|
||||
state.can_set_surround_sound = can_set_surround_sound;
|
||||
state.can_set_side_tone = can_set_side_tone;
|
||||
state.can_set_automatic_shutdown = can_set_automatic_shutdown;
|
||||
state.can_set_side_tone_volume = can_set_side_tone_volume;
|
||||
state.can_set_voice_prompt = can_set_voice_prompt;
|
||||
state.can_set_silent_mode = can_set_silent_mode;
|
||||
}
|
||||
|
||||
fn execute_headset_specific_functionality(&mut self) -> Result<(), DeviceError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ impl TrayHandler {
|
||||
device_state.device_name.clone(),
|
||||
),
|
||||
Some(true) => (
|
||||
device_state.to_string_with_padding(0),
|
||||
device_state.to_string_with_readonly_info(0),
|
||||
device_state.device_name.clone(),
|
||||
),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user