Refactor changing headset properties
Tray app can now change headset properties
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -401,7 +401,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper_headset"
|
name = "hyper_headset"
|
||||||
version = "1.5.3"
|
version = "1.6.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap 4.5.58",
|
"clap 4.5.58",
|
||||||
"dialog",
|
"dialog",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "hyper_headset"
|
name = "hyper_headset"
|
||||||
version = "1.5.3"
|
version = "1.6.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["Lennard Kittner"]
|
authors = ["Lennard Kittner"]
|
||||||
description = "A CLI and tray application for monitoring and managing HyperX headsets."
|
description = "A CLI and tray application for monitoring and managing HyperX headsets."
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use clap::{Arg, Command};
|
use clap::{Arg, Command};
|
||||||
use hyper_headset::devices::connect_compatible_device;
|
use hyper_headset::devices::{connect_compatible_device, DeviceEvent};
|
||||||
|
|
||||||
const SHOW_ALL_OPTIONS: bool = false;
|
const SHOW_ALL_OPTIONS: bool = false;
|
||||||
|
|
||||||
@@ -111,110 +111,45 @@ fn main() {
|
|||||||
)
|
)
|
||||||
.get_matches();
|
.get_matches();
|
||||||
|
|
||||||
|
let mut commands = Vec::new();
|
||||||
if let Some(delay) = matches.get_one::<u8>("automatic_shutdown") {
|
if let Some(delay) = matches.get_one::<u8>("automatic_shutdown") {
|
||||||
let delay = *delay as u64;
|
let delay = *delay as u64;
|
||||||
if let Some(packet) =
|
commands.push(DeviceEvent::AutomaticShutdownAfter(Duration::from_secs(
|
||||||
device.set_automatic_shut_down_packet(Duration::from_secs(delay * 60u64))
|
delay * 60u64,
|
||||||
{
|
)));
|
||||||
device.prepare_write();
|
|
||||||
if let Err(err) = device.get_device_state().hid_device.write(&packet) {
|
|
||||||
eprintln!("Failed to set automatic shutdown with error: {:?}", err);
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
eprintln!("ERROR: Automatic shutdown is not supported on this device");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(mute) = matches.get_one::<bool>("mute") {
|
if let Some(mute) = matches.get_one::<bool>("mute") {
|
||||||
if let Some(packet) = device.set_mute_packet(*mute) {
|
commands.push(DeviceEvent::Muted(*mute));
|
||||||
device.prepare_write();
|
|
||||||
if let Err(err) = device.get_device_state().hid_device.write(&packet) {
|
|
||||||
eprintln!("Failed to mute with error: {:?}", err);
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
eprintln!("ERROR: Microphone mute control is not supported on this device (hardware button only)");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(enable) = matches.get_one::<bool>("enable_side_tone") {
|
if let Some(enable) = matches.get_one::<bool>("enable_side_tone") {
|
||||||
if let Some(packet) = device.set_side_tone_packet(*enable) {
|
commands.push(DeviceEvent::SideToneOn(*enable));
|
||||||
device.prepare_write();
|
|
||||||
if let Err(err) = device.get_device_state().hid_device.write(&packet) {
|
|
||||||
eprintln!("Failed to enable side tone with error: {:?}", err);
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
eprintln!("ERROR: Side tone control is not supported on this device");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(volume) = matches.get_one::<u8>("side_tone_volume") {
|
if let Some(volume) = matches.get_one::<u8>("side_tone_volume") {
|
||||||
if let Some(packet) = device.set_side_tone_volume_packet(*volume) {
|
commands.push(DeviceEvent::SideToneVolume(*volume));
|
||||||
device.prepare_write();
|
|
||||||
if let Err(err) = device.get_device_state().hid_device.write(&packet) {
|
|
||||||
eprintln!("Failed to set side tone volume with error: {:?}", err);
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
eprintln!("ERROR: Side tone volume control is not supported on this device");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(enable) = matches.get_one::<bool>("enable_voice_prompt") {
|
if let Some(enable) = matches.get_one::<bool>("enable_voice_prompt") {
|
||||||
if let Some(packet) = device.set_voice_prompt_packet(*enable) {
|
commands.push(DeviceEvent::VoicePrompt(*enable));
|
||||||
device.prepare_write();
|
|
||||||
if let Err(err) = device.get_device_state().hid_device.write(&packet) {
|
|
||||||
eprintln!("Failed to enable voice prompt with error: {:?}", err);
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
eprintln!("ERROR: Voice prompt control is not supported on this device");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(surround_sound) = matches.get_one::<bool>("surround_sound") {
|
if let Some(surround_sound) = matches.get_one::<bool>("surround_sound") {
|
||||||
if let Some(packet) = device.set_surround_sound_packet(*surround_sound) {
|
commands.push(DeviceEvent::SurroundSound(*surround_sound));
|
||||||
device.prepare_write();
|
|
||||||
if let Err(err) = device.get_device_state().hid_device.write(&packet) {
|
|
||||||
eprintln!("Failed to set surround sound with error: {:?}", err);
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(mute_playback) = matches.get_one::<bool>("mute_playback") {
|
if let Some(mute_playback) = matches.get_one::<bool>("mute_playback") {
|
||||||
if let Some(packet) = device.set_silent_mode_packet(*mute_playback) {
|
commands.push(DeviceEvent::Silent(*mute_playback));
|
||||||
device.prepare_write();
|
|
||||||
if let Err(err) = device.get_device_state().hid_device.write(&packet) {
|
|
||||||
eprintln!("Failed to mute playback with error: {:?}", err);
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
eprintln!("ERROR: Playback mute control is not supported on this device");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(activate) = matches.get_one::<bool>("activate_noise_gate") {
|
if let Some(activate) = matches.get_one::<bool>("activate_noise_gate") {
|
||||||
if let Some(packet) = device.set_noise_gate_packet(*activate) {
|
commands.push(DeviceEvent::NoiseGateActive(*activate));
|
||||||
device.prepare_write();
|
}
|
||||||
if let Err(err) = device.get_device_state().hid_device.write(&packet) {
|
|
||||||
eprintln!("Failed to activate noise gate with error: {:?}", err);
|
for command in commands {
|
||||||
std::process::exit(1);
|
if let Err(e) = device.try_apply(command) {
|
||||||
}
|
eprintln!("{e}");
|
||||||
} else {
|
|
||||||
eprintln!("ERROR: Activating noise gate is not supported on this device");
|
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -233,5 +168,5 @@ fn main() {
|
|||||||
eprintln!("{error}");
|
eprintln!("{error}");
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
};
|
};
|
||||||
println!("{}", device.get_device_state());
|
println!("{}", device.get_device_state().device_properties);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,13 +45,13 @@ pub struct CloudIICoreWireless {
|
|||||||
impl CloudIICoreWireless {
|
impl CloudIICoreWireless {
|
||||||
pub fn new_from_state(state: DeviceState) -> Self {
|
pub fn new_from_state(state: DeviceState) -> Self {
|
||||||
let mut state = state;
|
let mut state = state;
|
||||||
state.connected = Some(true);
|
state.device_properties.connected = Some(true);
|
||||||
CloudIICoreWireless { state }
|
CloudIICoreWireless { state }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new() -> Result<Self, DeviceError> {
|
pub fn new() -> Result<Self, DeviceError> {
|
||||||
let mut state = DeviceState::new(&PRODUCT_IDS, &VENDOR_IDS)?;
|
let mut state = DeviceState::new(&PRODUCT_IDS, &VENDOR_IDS)?;
|
||||||
state.connected = Some(true);
|
state.device_properties.connected = Some(true);
|
||||||
Ok(CloudIICoreWireless { state })
|
Ok(CloudIICoreWireless { state })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,13 +48,13 @@ pub struct CloudIIWireless {
|
|||||||
impl CloudIIWireless {
|
impl CloudIIWireless {
|
||||||
pub fn new_from_state(state: DeviceState) -> Self {
|
pub fn new_from_state(state: DeviceState) -> Self {
|
||||||
let mut tmp_state = state;
|
let mut tmp_state = state;
|
||||||
tmp_state.connected = Some(true);
|
tmp_state.device_properties.connected = Some(true);
|
||||||
CloudIIWireless { state: tmp_state }
|
CloudIIWireless { state: tmp_state }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new() -> Result<Self, DeviceError> {
|
pub fn new() -> Result<Self, DeviceError> {
|
||||||
let mut state = DeviceState::new(&PRODUCT_IDS, &VENDOR_IDS)?;
|
let mut state = DeviceState::new(&PRODUCT_IDS, &VENDOR_IDS)?;
|
||||||
state.connected = Some(true);
|
state.device_properties.connected = Some(true);
|
||||||
Ok(CloudIIWireless { state })
|
Ok(CloudIIWireless { state })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,13 +49,13 @@ pub struct CloudIIWirelessDTS {
|
|||||||
impl CloudIIWirelessDTS {
|
impl CloudIIWirelessDTS {
|
||||||
pub fn new_from_state(state: DeviceState) -> Self {
|
pub fn new_from_state(state: DeviceState) -> Self {
|
||||||
let mut state = state;
|
let mut state = state;
|
||||||
state.connected = Some(true);
|
state.device_properties.connected = Some(true);
|
||||||
CloudIIWirelessDTS { state }
|
CloudIIWirelessDTS { state }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new() -> Result<Self, DeviceError> {
|
pub fn new() -> Result<Self, DeviceError> {
|
||||||
let mut state = DeviceState::new(&PRODUCT_IDS, &VENDOR_IDS)?;
|
let mut state = DeviceState::new(&PRODUCT_IDS, &VENDOR_IDS)?;
|
||||||
state.connected = Some(true);
|
state.device_properties.connected = Some(true);
|
||||||
Ok(CloudIIWirelessDTS { state })
|
Ok(CloudIIWirelessDTS { state })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,11 @@ use crate::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
use hidapi::{HidApi, HidDevice, HidError};
|
use hidapi::{HidApi, HidDevice, HidError};
|
||||||
use std::{collections::HashSet, fmt::Display, time::Duration};
|
use std::{
|
||||||
|
collections::HashSet,
|
||||||
|
fmt::{Debug, Display},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
use thistermination::TerminationFull;
|
use thistermination::TerminationFull;
|
||||||
|
|
||||||
const PASSIVE_REFRESH_TIME_OUT: Duration = Duration::from_secs(2);
|
const PASSIVE_REFRESH_TIME_OUT: Duration = Duration::from_secs(2);
|
||||||
@@ -61,7 +65,7 @@ const DEVICE_REGISTER: &[DeviceEntry] = &[
|
|||||||
];
|
];
|
||||||
|
|
||||||
const RESPONSE_BUFFER_SIZE: usize = 256;
|
const RESPONSE_BUFFER_SIZE: usize = 256;
|
||||||
const RESPONSE_DELAY: Duration = Duration::from_millis(50);
|
pub const RESPONSE_DELAY: Duration = Duration::from_millis(50);
|
||||||
|
|
||||||
pub fn connect_compatible_device() -> Result<Box<dyn Device>, DeviceError> {
|
pub fn connect_compatible_device() -> Result<Box<dyn Device>, DeviceError> {
|
||||||
let all_product_ids: Vec<u16> = DEVICE_REGISTER
|
let all_product_ids: Vec<u16> = DEVICE_REGISTER
|
||||||
@@ -82,7 +86,8 @@ pub fn connect_compatible_device() -> Result<Box<dyn Device>, DeviceError> {
|
|||||||
let entry = DEVICE_REGISTER
|
let entry = DEVICE_REGISTER
|
||||||
.iter()
|
.iter()
|
||||||
.find(|e| {
|
.find(|e| {
|
||||||
e.vendor_ids.contains(&state.vendor_id) && e.product_ids.contains(&state.product_id)
|
e.vendor_ids.contains(&state.device_properties.vendor_id)
|
||||||
|
&& e.product_ids.contains(&state.device_properties.product_id)
|
||||||
})
|
})
|
||||||
.ok_or(DeviceError::NoDeviceFound())?;
|
.ok_or(DeviceError::NoDeviceFound())?;
|
||||||
|
|
||||||
@@ -95,6 +100,11 @@ pub fn connect_compatible_device() -> Result<Box<dyn Device>, DeviceError> {
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct DeviceState {
|
pub struct DeviceState {
|
||||||
pub hid_device: HidDevice,
|
pub hid_device: HidDevice,
|
||||||
|
pub device_properties: DeviceProperties,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DeviceProperties {
|
||||||
pub product_id: u16,
|
pub product_id: u16,
|
||||||
pub vendor_id: u16,
|
pub vendor_id: u16,
|
||||||
pub device_name: Option<String>,
|
pub device_name: Option<String>,
|
||||||
@@ -124,7 +134,7 @@ pub struct DeviceState {
|
|||||||
pub can_set_noise_gate: bool,
|
pub can_set_noise_gate: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for DeviceState {
|
impl Display for DeviceProperties {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
write!(f, "{}", self.to_string_with_readonly_info(25))
|
write!(f, "{}", self.to_string_with_readonly_info(25))
|
||||||
}
|
}
|
||||||
@@ -202,24 +212,98 @@ impl DeviceState {
|
|||||||
let device_name = hid_device.get_product_string()?;
|
let device_name = hid_device.get_product_string()?;
|
||||||
Ok(DeviceState {
|
Ok(DeviceState {
|
||||||
hid_device,
|
hid_device,
|
||||||
|
device_properties: DeviceProperties::new(product_id, vendor_id, device_name),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_self_with_event(&mut self, event: &DeviceEvent) {
|
||||||
|
match event {
|
||||||
|
DeviceEvent::BatterLevel(level) => self.device_properties.battery_level = Some(*level),
|
||||||
|
DeviceEvent::Charging(status) => self.device_properties.charging = Some(*status),
|
||||||
|
DeviceEvent::Muted(status) => self.device_properties.muted = Some(*status),
|
||||||
|
DeviceEvent::MicConnected(status) => {
|
||||||
|
self.device_properties.mic_connected = Some(*status)
|
||||||
|
}
|
||||||
|
DeviceEvent::AutomaticShutdownAfter(duration) => {
|
||||||
|
self.device_properties.automatic_shutdown_after = Some(*duration)
|
||||||
|
}
|
||||||
|
DeviceEvent::PairingInfo(info) => self.device_properties.pairing_info = Some(*info),
|
||||||
|
DeviceEvent::ProductColor(color) => self.device_properties.product_color = Some(*color),
|
||||||
|
DeviceEvent::SideToneOn(side) => self.device_properties.side_tone_on = Some(*side),
|
||||||
|
DeviceEvent::SideToneVolume(volume) => {
|
||||||
|
self.device_properties.side_tone_volume = Some(*volume)
|
||||||
|
}
|
||||||
|
DeviceEvent::SurroundSound(status) => {
|
||||||
|
self.device_properties.surround_sound = Some(*status)
|
||||||
|
}
|
||||||
|
DeviceEvent::VoicePrompt(on) => self.device_properties.voice_prompt_on = Some(*on),
|
||||||
|
DeviceEvent::WirelessConnected(connected) => {
|
||||||
|
self.device_properties.connected = Some(*connected)
|
||||||
|
}
|
||||||
|
DeviceEvent::Silent(silent) => self.device_properties.silent = Some(*silent),
|
||||||
|
DeviceEvent::RequireSIRKReset(_reset) => {
|
||||||
|
debug_println!("requested SIRK reset {_reset}");
|
||||||
|
}
|
||||||
|
DeviceEvent::NoiseGateActive(on) => {
|
||||||
|
self.device_properties.noise_gate_active = Some(*on)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
|
||||||
|
pub enum PropertyType {
|
||||||
|
ReadOnly,
|
||||||
|
AlwaysReadOnly,
|
||||||
|
ReadWrite,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum PropertyDescriptorWrapper {
|
||||||
|
Int(PropertyDescriptor<u8>, &'static [u8]),
|
||||||
|
Bool(PropertyDescriptor<bool>),
|
||||||
|
String(PropertyDescriptor<String>),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PropertyDescriptor<T: 'static> {
|
||||||
|
pub prefix: &'static str,
|
||||||
|
pub data: Option<T>,
|
||||||
|
pub suffix: &'static str,
|
||||||
|
pub property_type: PropertyType,
|
||||||
|
pub create_event: &'static dyn Fn(T) -> Option<DeviceEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Debug> Debug for PropertyDescriptor<T> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("PropertyDescriptor")
|
||||||
|
.field("prefix", &self.prefix)
|
||||||
|
.field("data", &self.data)
|
||||||
|
.field("suffix", &self.suffix)
|
||||||
|
.field("property_type", &self.property_type)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeviceProperties {
|
||||||
|
pub fn new(product_id: u16, vendor_id: u16, device_name: Option<String>) -> DeviceProperties {
|
||||||
|
DeviceProperties {
|
||||||
product_id,
|
product_id,
|
||||||
vendor_id,
|
vendor_id,
|
||||||
device_name,
|
device_name,
|
||||||
charging: None,
|
|
||||||
battery_level: None,
|
battery_level: None,
|
||||||
|
charging: None,
|
||||||
muted: None,
|
muted: None,
|
||||||
surround_sound: None,
|
|
||||||
mic_connected: None,
|
mic_connected: None,
|
||||||
automatic_shutdown_after: None,
|
automatic_shutdown_after: None,
|
||||||
pairing_info: None,
|
pairing_info: None,
|
||||||
product_color: None,
|
product_color: None,
|
||||||
side_tone_on: None,
|
side_tone_on: None,
|
||||||
side_tone_volume: None,
|
side_tone_volume: None,
|
||||||
|
surround_sound: None,
|
||||||
voice_prompt_on: None,
|
voice_prompt_on: None,
|
||||||
connected: None,
|
connected: None,
|
||||||
silent: None,
|
silent: None,
|
||||||
noise_gate_active: None,
|
noise_gate_active: None,
|
||||||
// Capability flags - will be set by init_capabilities()
|
|
||||||
can_set_mute: false,
|
can_set_mute: false,
|
||||||
can_set_surround_sound: false,
|
can_set_surround_sound: false,
|
||||||
can_set_side_tone: false,
|
can_set_side_tone: false,
|
||||||
@@ -229,103 +313,183 @@ impl DeviceState {
|
|||||||
can_set_silent_mode: false,
|
can_set_silent_mode: false,
|
||||||
can_set_equalizer: false,
|
can_set_equalizer: false,
|
||||||
can_set_noise_gate: false,
|
can_set_noise_gate: false,
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_display_data(&self) -> Vec<(&str, Option<String>, &str, bool)> {
|
pub fn get_properties(&self) -> Vec<PropertyDescriptorWrapper> {
|
||||||
vec![
|
vec![
|
||||||
(
|
PropertyDescriptorWrapper::String(PropertyDescriptor {
|
||||||
"Battery level:",
|
prefix: "Charging status:",
|
||||||
self.battery_level.map(|l| l.to_string()),
|
data: self.charging.map(|c| c.to_string()),
|
||||||
"%",
|
suffix: "",
|
||||||
false,
|
property_type: PropertyType::AlwaysReadOnly,
|
||||||
|
create_event: &|_| None,
|
||||||
|
}),
|
||||||
|
PropertyDescriptorWrapper::Int(
|
||||||
|
PropertyDescriptor {
|
||||||
|
prefix: "Battery level:",
|
||||||
|
data: self.battery_level,
|
||||||
|
suffix: "%",
|
||||||
|
property_type: PropertyType::AlwaysReadOnly,
|
||||||
|
create_event: &|_| None,
|
||||||
|
},
|
||||||
|
&[],
|
||||||
),
|
),
|
||||||
(
|
PropertyDescriptorWrapper::Bool(PropertyDescriptor {
|
||||||
"Charging status:",
|
prefix: "Muted:",
|
||||||
self.charging.map(|c| c.to_string()),
|
data: self.muted,
|
||||||
"",
|
suffix: "",
|
||||||
false,
|
property_type: if self.can_set_mute {
|
||||||
|
PropertyType::ReadWrite
|
||||||
|
} else {
|
||||||
|
PropertyType::ReadOnly
|
||||||
|
},
|
||||||
|
create_event: &move |mute| Some(DeviceEvent::Muted(mute)),
|
||||||
|
}),
|
||||||
|
PropertyDescriptorWrapper::Bool(PropertyDescriptor {
|
||||||
|
prefix: "Mic connected:",
|
||||||
|
data: self.mic_connected,
|
||||||
|
suffix: "",
|
||||||
|
property_type: PropertyType::AlwaysReadOnly,
|
||||||
|
create_event: &|_| None,
|
||||||
|
}),
|
||||||
|
PropertyDescriptorWrapper::Int(
|
||||||
|
PropertyDescriptor {
|
||||||
|
prefix: "Automatic shutdown after:",
|
||||||
|
data: self
|
||||||
|
.automatic_shutdown_after
|
||||||
|
.map(|t| (t.as_secs() / 60) as u8),
|
||||||
|
suffix: "min",
|
||||||
|
property_type: if self.can_set_mute {
|
||||||
|
PropertyType::ReadWrite
|
||||||
|
} else {
|
||||||
|
PropertyType::ReadOnly
|
||||||
|
},
|
||||||
|
create_event: &|t| {
|
||||||
|
Some(DeviceEvent::AutomaticShutdownAfter(Duration::from_secs(
|
||||||
|
t as u64 * 60,
|
||||||
|
)))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&[0, 5, 10, 15, 20, 30, 40, 60],
|
||||||
),
|
),
|
||||||
(
|
PropertyDescriptorWrapper::Int(
|
||||||
"Muted:",
|
PropertyDescriptor {
|
||||||
self.muted.map(|c| c.to_string()),
|
prefix: "Pairing info:",
|
||||||
"",
|
data: self.pairing_info,
|
||||||
!self.can_set_mute,
|
suffix: "",
|
||||||
|
property_type: PropertyType::AlwaysReadOnly,
|
||||||
|
create_event: &|_| None,
|
||||||
|
},
|
||||||
|
&[],
|
||||||
),
|
),
|
||||||
(
|
PropertyDescriptorWrapper::String(PropertyDescriptor {
|
||||||
"Mic connected:",
|
prefix: "Product color:",
|
||||||
self.mic_connected.map(|c| c.to_string()),
|
data: self.product_color.map(|c| c.to_string()),
|
||||||
"",
|
suffix: "",
|
||||||
false,
|
property_type: PropertyType::AlwaysReadOnly,
|
||||||
),
|
create_event: &|_| None,
|
||||||
(
|
}),
|
||||||
"Automatic shutdown after:",
|
PropertyDescriptorWrapper::Bool(PropertyDescriptor {
|
||||||
self.automatic_shutdown_after
|
prefix: "Side tone:",
|
||||||
.map(|c| (c.as_secs() / 60).to_string()),
|
data: self.side_tone_on,
|
||||||
"min",
|
suffix: "",
|
||||||
!self.can_set_automatic_shutdown,
|
property_type: if self.can_set_side_tone {
|
||||||
),
|
PropertyType::ReadWrite
|
||||||
(
|
} else {
|
||||||
"Pairing info:",
|
PropertyType::ReadOnly
|
||||||
self.pairing_info.map(|c| c.to_string()),
|
},
|
||||||
"",
|
create_event: &move |enable| Some(DeviceEvent::SideToneOn(enable)),
|
||||||
false,
|
}),
|
||||||
),
|
PropertyDescriptorWrapper::Int(
|
||||||
(
|
PropertyDescriptor {
|
||||||
"Product color:",
|
prefix: "Side tone volume:",
|
||||||
self.product_color.map(|c| c.to_string()),
|
data: self.side_tone_volume,
|
||||||
"",
|
suffix: "",
|
||||||
false,
|
property_type: if self.can_set_side_tone_volume {
|
||||||
),
|
PropertyType::ReadWrite
|
||||||
(
|
} else {
|
||||||
"Side tone:",
|
PropertyType::ReadOnly
|
||||||
self.side_tone_on.map(|c| c.to_string()),
|
},
|
||||||
"",
|
create_event: &|v| Some(DeviceEvent::SideToneVolume(v)),
|
||||||
!self.can_set_side_tone,
|
},
|
||||||
),
|
&[0, 25, 50, 75, 100, 125, 150, 175, 200, 225, 250],
|
||||||
(
|
|
||||||
"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,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"Playback muted:",
|
|
||||||
self.silent.map(|c| c.to_string()),
|
|
||||||
"",
|
|
||||||
!self.can_set_silent_mode,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"Noise gate active:",
|
|
||||||
self.noise_gate_active.map(|c| c.to_string()),
|
|
||||||
"",
|
|
||||||
!self.can_set_noise_gate,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"Connected:",
|
|
||||||
self.connected.map(|c| c.to_string()),
|
|
||||||
"",
|
|
||||||
false,
|
|
||||||
),
|
),
|
||||||
|
PropertyDescriptorWrapper::Bool(PropertyDescriptor {
|
||||||
|
prefix: "Surround sound:",
|
||||||
|
data: self.surround_sound,
|
||||||
|
suffix: "",
|
||||||
|
property_type: if self.can_set_surround_sound {
|
||||||
|
PropertyType::ReadWrite
|
||||||
|
} else {
|
||||||
|
PropertyType::ReadOnly
|
||||||
|
},
|
||||||
|
create_event: &move |enable| Some(DeviceEvent::SurroundSound(enable)),
|
||||||
|
}),
|
||||||
|
PropertyDescriptorWrapper::Bool(PropertyDescriptor {
|
||||||
|
prefix: "Voice prompt:",
|
||||||
|
data: self.voice_prompt_on,
|
||||||
|
suffix: "",
|
||||||
|
property_type: if self.can_set_voice_prompt {
|
||||||
|
PropertyType::ReadWrite
|
||||||
|
} else {
|
||||||
|
PropertyType::ReadOnly
|
||||||
|
},
|
||||||
|
create_event: &move |enable| Some(DeviceEvent::VoicePrompt(enable)),
|
||||||
|
}),
|
||||||
|
PropertyDescriptorWrapper::Bool(PropertyDescriptor {
|
||||||
|
prefix: "Playback muted:",
|
||||||
|
data: self.silent,
|
||||||
|
suffix: "",
|
||||||
|
property_type: if self.can_set_silent_mode {
|
||||||
|
PropertyType::ReadWrite
|
||||||
|
} else {
|
||||||
|
PropertyType::ReadOnly
|
||||||
|
},
|
||||||
|
create_event: &move |enable| Some(DeviceEvent::Silent(enable)),
|
||||||
|
}),
|
||||||
|
PropertyDescriptorWrapper::Bool(PropertyDescriptor {
|
||||||
|
prefix: "Noise gate active:",
|
||||||
|
data: self.noise_gate_active,
|
||||||
|
suffix: "",
|
||||||
|
property_type: if self.can_set_noise_gate {
|
||||||
|
PropertyType::ReadWrite
|
||||||
|
} else {
|
||||||
|
PropertyType::ReadOnly
|
||||||
|
},
|
||||||
|
create_event: &move |enable| Some(DeviceEvent::NoiseGateActive(enable)),
|
||||||
|
}),
|
||||||
|
PropertyDescriptorWrapper::Bool(PropertyDescriptor {
|
||||||
|
prefix: "Connected:",
|
||||||
|
data: self.connected,
|
||||||
|
suffix: "",
|
||||||
|
property_type: PropertyType::AlwaysReadOnly,
|
||||||
|
create_event: &|_| None,
|
||||||
|
}),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_string_with_padding(&self, padding: usize) -> String {
|
pub fn to_string_with_padding(&self, padding: usize) -> String {
|
||||||
self.get_display_data()
|
self.get_properties()
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(prefix, data, suffix, _)| {
|
.filter_map(|prop| {
|
||||||
|
let (prefix, data, suffix) = match prop {
|
||||||
|
PropertyDescriptorWrapper::Int(property_descriptor, _) => (
|
||||||
|
property_descriptor.prefix,
|
||||||
|
&property_descriptor.data.map(|v| v.to_string()),
|
||||||
|
property_descriptor.suffix,
|
||||||
|
),
|
||||||
|
PropertyDescriptorWrapper::Bool(property_descriptor) => (
|
||||||
|
property_descriptor.prefix,
|
||||||
|
&property_descriptor.data.map(|v| v.to_string()),
|
||||||
|
property_descriptor.suffix,
|
||||||
|
),
|
||||||
|
PropertyDescriptorWrapper::String(property_descriptor) => (
|
||||||
|
property_descriptor.prefix,
|
||||||
|
&property_descriptor.data,
|
||||||
|
property_descriptor.suffix,
|
||||||
|
),
|
||||||
|
};
|
||||||
data.as_ref()
|
data.as_ref()
|
||||||
.map(|data| format!("{:<padding$} {}{}", prefix, data, suffix))
|
.map(|data| format!("{:<padding$} {}{}", prefix, data, suffix))
|
||||||
})
|
})
|
||||||
@@ -334,63 +498,42 @@ impl DeviceState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_string_with_readonly_info(&self, padding: usize) -> String {
|
pub fn to_string_with_readonly_info(&self, padding: usize) -> String {
|
||||||
self.get_display_data()
|
self.get_properties()
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(prefix, data, suffix, readonly)| {
|
.filter_map(|prop| {
|
||||||
if let Some(data) = data {
|
let (prefix, data, suffix, property_type) = match prop {
|
||||||
let readonly_marker = if *readonly { " (read-only)" } else { "" };
|
PropertyDescriptorWrapper::Int(property_descriptor, _) => (
|
||||||
Some(format!(
|
property_descriptor.prefix,
|
||||||
"{:<padding$} {}{}{}",
|
&property_descriptor.data.map(|v| v.to_string()),
|
||||||
prefix, data, suffix, readonly_marker
|
property_descriptor.suffix,
|
||||||
))
|
property_descriptor.property_type,
|
||||||
} else {
|
),
|
||||||
None
|
PropertyDescriptorWrapper::Bool(property_descriptor) => (
|
||||||
}
|
property_descriptor.prefix,
|
||||||
|
&property_descriptor.data.map(|v| v.to_string()),
|
||||||
|
property_descriptor.suffix,
|
||||||
|
property_descriptor.property_type,
|
||||||
|
),
|
||||||
|
PropertyDescriptorWrapper::String(property_descriptor) => (
|
||||||
|
property_descriptor.prefix,
|
||||||
|
&property_descriptor.data,
|
||||||
|
property_descriptor.suffix,
|
||||||
|
property_descriptor.property_type,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
data.as_ref().map(|data| {
|
||||||
|
let readonly_marker = if property_type == PropertyType::ReadOnly {
|
||||||
|
" (read-only)"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
format!("{:<padding$} {}{}{}", prefix, data, suffix, readonly_marker)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
.join("\n")
|
.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_self_with_event(&mut self, event: &DeviceEvent) {
|
|
||||||
match event {
|
|
||||||
DeviceEvent::BatterLevel(level) => self.battery_level = Some(*level),
|
|
||||||
DeviceEvent::Charging(status) => self.charging = Some(*status),
|
|
||||||
DeviceEvent::Muted(status) => self.muted = Some(*status),
|
|
||||||
DeviceEvent::MicConnected(status) => self.mic_connected = Some(*status),
|
|
||||||
DeviceEvent::AutomaticShutdownAfter(duration) => {
|
|
||||||
self.automatic_shutdown_after = Some(*duration)
|
|
||||||
}
|
|
||||||
DeviceEvent::PairingInfo(info) => self.pairing_info = Some(*info),
|
|
||||||
DeviceEvent::ProductColor(color) => self.product_color = Some(*color),
|
|
||||||
DeviceEvent::SideToneOn(side) => self.side_tone_on = Some(*side),
|
|
||||||
DeviceEvent::SideToneVolume(volume) => self.side_tone_volume = Some(*volume),
|
|
||||||
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) => {
|
|
||||||
debug_println!("requested SIRK reset {reset}");
|
|
||||||
}
|
|
||||||
DeviceEvent::NoiseGateActive(on) => self.noise_gate_active = Some(*on),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clear_state(&mut self) {
|
|
||||||
self.charging = None;
|
|
||||||
self.battery_level = None;
|
|
||||||
self.muted = None;
|
|
||||||
self.surround_sound = None;
|
|
||||||
self.mic_connected = None;
|
|
||||||
self.automatic_shutdown_after = None;
|
|
||||||
self.pairing_info = None;
|
|
||||||
self.product_color = None;
|
|
||||||
self.side_tone_on = None;
|
|
||||||
self.side_tone_volume = None;
|
|
||||||
self.voice_prompt_on = None;
|
|
||||||
self.connected = None;
|
|
||||||
self.silent = None;
|
|
||||||
self.noise_gate_active = None;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(TerminationFull)]
|
#[derive(TerminationFull)]
|
||||||
@@ -460,7 +603,7 @@ impl From<u8> for Color {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||||
pub enum ChargingStatus {
|
pub enum ChargingStatus {
|
||||||
NotCharging,
|
NotCharging,
|
||||||
Charging,
|
Charging,
|
||||||
@@ -583,20 +726,21 @@ pub trait Device {
|
|||||||
|
|
||||||
// Now set them in device state
|
// Now set them in device state
|
||||||
let state = self.get_device_state_mut();
|
let state = self.get_device_state_mut();
|
||||||
state.can_set_mute = can_set_mute;
|
state.device_properties.can_set_mute = can_set_mute;
|
||||||
state.can_set_surround_sound = can_set_surround_sound;
|
state.device_properties.can_set_surround_sound = can_set_surround_sound;
|
||||||
state.can_set_side_tone = can_set_side_tone;
|
state.device_properties.can_set_side_tone = can_set_side_tone;
|
||||||
state.can_set_automatic_shutdown = can_set_automatic_shutdown;
|
state.device_properties.can_set_automatic_shutdown = can_set_automatic_shutdown;
|
||||||
state.can_set_side_tone_volume = can_set_side_tone_volume;
|
state.device_properties.can_set_side_tone_volume = can_set_side_tone_volume;
|
||||||
state.can_set_voice_prompt = can_set_voice_prompt;
|
state.device_properties.can_set_voice_prompt = can_set_voice_prompt;
|
||||||
state.can_set_silent_mode = can_set_silent_mode;
|
state.device_properties.can_set_silent_mode = can_set_silent_mode;
|
||||||
state.can_set_equalizer = can_set_equalizer;
|
state.device_properties.can_set_equalizer = can_set_equalizer;
|
||||||
state.can_set_noise_gate = can_set_noise_gate;
|
state.device_properties.can_set_noise_gate = can_set_noise_gate;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn execute_headset_specific_functionality(&mut self) -> Result<(), DeviceError> {
|
fn execute_headset_specific_functionality(&mut self) -> Result<(), DeviceError> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wait_for_updates(&mut self, duration: Duration) -> Option<Vec<DeviceEvent>> {
|
fn wait_for_updates(&mut self, duration: Duration) -> Option<Vec<DeviceEvent>> {
|
||||||
let mut buf = self.get_response_buffer();
|
let mut buf = self.get_response_buffer();
|
||||||
let res = self
|
let res = self
|
||||||
@@ -646,7 +790,10 @@ pub trait Device {
|
|||||||
}
|
}
|
||||||
responded = true;
|
responded = true;
|
||||||
}
|
}
|
||||||
if !matches!(self.get_device_state().connected, Some(true)) {
|
if !matches!(
|
||||||
|
self.get_device_state().device_properties.connected,
|
||||||
|
Some(true)
|
||||||
|
) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -681,4 +828,121 @@ pub trait Device {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn try_apply(&mut self, command: DeviceEvent) -> Result<(), String> {
|
||||||
|
match command {
|
||||||
|
DeviceEvent::AutomaticShutdownAfter(delay) => {
|
||||||
|
if let Some(packet) = self.set_automatic_shut_down_packet(delay) {
|
||||||
|
self.prepare_write();
|
||||||
|
if let Err(err) = self.get_device_state().hid_device.write(&packet) {
|
||||||
|
Err(format!(
|
||||||
|
"Failed to set automatic shutdown with error: {:?}",
|
||||||
|
err
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err("ERROR: Automatic shutdown is not supported on this device".to_string())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DeviceEvent::Muted(mute) => {
|
||||||
|
if let Some(packet) = self.set_mute_packet(mute) {
|
||||||
|
self.prepare_write();
|
||||||
|
if let Err(err) = self.get_device_state().hid_device.write(&packet) {
|
||||||
|
Err(format!("Failed to mute with error: {:?}", err))?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err("ERROR: Microphone mute control is not supported on this device (hardware button only)")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DeviceEvent::SideToneOn(enable) => {
|
||||||
|
if let Some(packet) = self.set_side_tone_packet(enable) {
|
||||||
|
self.prepare_write();
|
||||||
|
if let Err(err) = self.get_device_state().hid_device.write(&packet) {
|
||||||
|
Err(format!("Failed to enable side tone with error: {:?}", err))?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err("ERROR: Side tone control is not supported on this device".to_string())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DeviceEvent::SideToneVolume(volume) => {
|
||||||
|
if let Some(packet) = self.set_side_tone_volume_packet(volume) {
|
||||||
|
self.prepare_write();
|
||||||
|
if let Err(err) = self.get_device_state().hid_device.write(&packet) {
|
||||||
|
Err(format!(
|
||||||
|
"Failed to set side tone volume with error: {:?}",
|
||||||
|
err
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(
|
||||||
|
"ERROR: Side tone volume control is not supported on this device"
|
||||||
|
.to_string(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DeviceEvent::VoicePrompt(enable) => {
|
||||||
|
if let Some(packet) = self.set_voice_prompt_packet(enable) {
|
||||||
|
self.prepare_write();
|
||||||
|
if let Err(err) = self.get_device_state().hid_device.write(&packet) {
|
||||||
|
Err(format!(
|
||||||
|
"Failed to enable voice prompt with error: {:?}",
|
||||||
|
err
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err("ERROR: Voice prompt control is not supported on this device")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DeviceEvent::SurroundSound(surround_sound) => {
|
||||||
|
if let Some(packet) = self.set_surround_sound_packet(surround_sound) {
|
||||||
|
self.prepare_write();
|
||||||
|
if let Err(err) = self.get_device_state().hid_device.write(&packet) {
|
||||||
|
Err(format!(
|
||||||
|
"Failed to set surround sound with error: {:?}",
|
||||||
|
err
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err("ERROR: Surround sound control is not supported on this device")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DeviceEvent::Silent(mute_playback) => {
|
||||||
|
if let Some(packet) = self.set_silent_mode_packet(mute_playback) {
|
||||||
|
self.prepare_write();
|
||||||
|
if let Err(err) = self.get_device_state().hid_device.write(&packet) {
|
||||||
|
Err(format!("Failed to mute playback with error: {:?}", err))?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err("ERROR: Playback mute control is not supported on this device")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DeviceEvent::NoiseGateActive(activate) => {
|
||||||
|
if let Some(packet) = self.set_noise_gate_packet(activate) {
|
||||||
|
self.prepare_write();
|
||||||
|
if let Err(err) = self.get_device_state().hid_device.write(&packet) {
|
||||||
|
Err(format!(
|
||||||
|
"Failed to activate noise gate with error: {:?}",
|
||||||
|
err
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err("ERROR: Activating noise gate is not supported on this device")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_state(&mut self) {
|
||||||
|
let product_id = self.get_device_state().device_properties.product_id;
|
||||||
|
let vendor_id = self.get_device_state().device_properties.vendor_id;
|
||||||
|
let device_name = self
|
||||||
|
.get_device_state()
|
||||||
|
.device_properties
|
||||||
|
.device_name
|
||||||
|
.clone();
|
||||||
|
self.get_device_state_mut().device_properties =
|
||||||
|
DeviceProperties::new(product_id, vendor_id, device_name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
42
src/main.rs
42
src/main.rs
@@ -1,16 +1,5 @@
|
|||||||
#[cfg(target_os = "linux")]
|
|
||||||
use clap::{Arg, Command};
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
use enigo::{Direction, Enigo, Key, Keyboard, Settings};
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
mod status_tray;
|
mod status_tray;
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
use hyper_headset::devices::connect_compatible_device;
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
use status_tray::{StatusTray, TrayHandler};
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
#[cfg(not(target_os = "linux"))]
|
||||||
fn main() {
|
fn main() {
|
||||||
@@ -19,6 +8,14 @@ fn main() {
|
|||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
fn main() {
|
fn main() {
|
||||||
|
use clap::{Arg, Command};
|
||||||
|
use enigo::{Direction, Enigo, Key, Keyboard, Settings};
|
||||||
|
use std::sync::mpsc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use hyper_headset::devices::connect_compatible_device;
|
||||||
|
use status_tray::{StatusTray, TrayHandler};
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
{
|
{
|
||||||
use hyper_headset::act_as_askpass_handler;
|
use hyper_headset::act_as_askpass_handler;
|
||||||
@@ -61,7 +58,8 @@ fn main() {
|
|||||||
let refresh_interval = *matches.get_one::<u64>("refresh_interval").unwrap_or(&3);
|
let refresh_interval = *matches.get_one::<u64>("refresh_interval").unwrap_or(&3);
|
||||||
let press_mute_key = *matches.get_one::<bool>("press_mute_key").unwrap_or(&true);
|
let press_mute_key = *matches.get_one::<bool>("press_mute_key").unwrap_or(&true);
|
||||||
let refresh_interval = Duration::from_secs(refresh_interval);
|
let refresh_interval = Duration::from_secs(refresh_interval);
|
||||||
let tray_handler = TrayHandler::new(StatusTray::new());
|
let (tx, rx) = mpsc::channel();
|
||||||
|
let tray_handler = TrayHandler::new(StatusTray::new(tx));
|
||||||
loop {
|
loop {
|
||||||
let mut device = loop {
|
let mut device = loop {
|
||||||
match connect_compatible_device() {
|
match connect_compatible_device() {
|
||||||
@@ -77,11 +75,7 @@ fn main() {
|
|||||||
// Run loop
|
// Run loop
|
||||||
let mut run_counter = 0;
|
let mut run_counter = 0;
|
||||||
loop {
|
loop {
|
||||||
std::thread::sleep(refresh_interval);
|
let mute_state = device.get_device_state().device_properties.muted;
|
||||||
// with the default refresh_interval the state is only actively queried every 3min
|
|
||||||
// querying the device to frequently can lead to instability
|
|
||||||
|
|
||||||
let mute_state = device.get_device_state().muted;
|
|
||||||
match if run_counter % 30 == 0 {
|
match if run_counter % 30 == 0 {
|
||||||
device.active_refresh_state()
|
device.active_refresh_state()
|
||||||
} else {
|
} else {
|
||||||
@@ -94,12 +88,24 @@ fn main() {
|
|||||||
break; // try to reconnect
|
break; // try to reconnect
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if mute_state.is_some() && mute_state != device.get_device_state().muted {
|
if mute_state.is_some()
|
||||||
|
&& mute_state != device.get_device_state().device_properties.muted
|
||||||
|
{
|
||||||
//TODO: macOS and windows have to use another key
|
//TODO: macOS and windows have to use another key
|
||||||
if press_mute_key {
|
if press_mute_key {
|
||||||
enigo.key(Key::MicMute, Direction::Click).unwrap();
|
enigo.key(Key::MicMute, Direction::Click).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// with the default refresh_interval the state is only actively queried every 3min
|
||||||
|
// querying the device to frequently can lead to instability
|
||||||
|
let first = rx.recv_timeout(refresh_interval);
|
||||||
|
for command in first.into_iter().chain(rx.try_iter()) {
|
||||||
|
let _ = device.try_apply(command);
|
||||||
|
std::thread::sleep(hyper_headset::devices::RESPONSE_DELAY);
|
||||||
|
let _ = device.active_refresh_state();
|
||||||
|
}
|
||||||
|
|
||||||
tray_handler.update(device.get_device_state());
|
tray_handler.update(device.get_device_state());
|
||||||
run_counter += 1;
|
run_counter += 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
use hyper_headset::devices::DeviceState;
|
use std::sync::mpsc::Sender;
|
||||||
use ksni::{menu::StandardItem, Handle, MenuItem, ToolTip, Tray, TrayService};
|
|
||||||
|
use hyper_headset::devices::{DeviceEvent, DeviceProperties, DeviceState, PropertyType};
|
||||||
|
use ksni::{
|
||||||
|
menu::{StandardItem, SubMenu},
|
||||||
|
Handle, MenuItem, ToolTip, Tray, TrayService,
|
||||||
|
};
|
||||||
|
|
||||||
pub struct TrayHandler {
|
pub struct TrayHandler {
|
||||||
handle: Handle<StatusTray>,
|
handle: Handle<StatusTray>,
|
||||||
}
|
}
|
||||||
|
|
||||||
const NO_COMPATIBLE_DEVICE: &str = "No compatible device found.\nIs the dongle plugged in?\nIf you are using Linux did you\nadd the Udev rules?";
|
const NO_COMPATIBLE_DEVICE: &str = "No compatible device found.\nIs the dongle plugged in?\nIf you are using Linux did you\nadd the Udev rules?";
|
||||||
|
const HEADSET_NOT_CONNECTED: &str = "Headset is not connected";
|
||||||
|
|
||||||
impl TrayHandler {
|
impl TrayHandler {
|
||||||
pub fn new(tray: StatusTray) -> Self {
|
pub fn new(tray: StatusTray) -> Self {
|
||||||
@@ -16,41 +22,28 @@ impl TrayHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(&self, device_state: &DeviceState) {
|
pub fn update(&self, device_state: &DeviceState) {
|
||||||
let (message, name) = match device_state.connected {
|
|
||||||
None => (NO_COMPATIBLE_DEVICE.to_string(), None),
|
|
||||||
Some(false) => (
|
|
||||||
"Headset is not connected".to_string(),
|
|
||||||
device_state.device_name.clone(),
|
|
||||||
),
|
|
||||||
Some(true) => (
|
|
||||||
device_state.to_string_with_padding(0),
|
|
||||||
device_state.device_name.clone(),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
self.handle.update(|tray| {
|
self.handle.update(|tray| {
|
||||||
tray.message = message;
|
tray.device_properties = Some(device_state.device_properties.clone());
|
||||||
tray.device_name = name;
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear_state(&self) {
|
pub fn clear_state(&self) {
|
||||||
self.handle.update(|tray| {
|
self.handle.update(|tray| {
|
||||||
tray.message = NO_COMPATIBLE_DEVICE.to_string();
|
tray.device_properties = None;
|
||||||
tray.device_name = None;
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct StatusTray {
|
pub struct StatusTray {
|
||||||
device_name: Option<String>,
|
device_properties: Option<DeviceProperties>,
|
||||||
message: String,
|
update_sender: Sender<DeviceEvent>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StatusTray {
|
impl StatusTray {
|
||||||
pub fn new() -> Self {
|
pub fn new(update_sender: Sender<DeviceEvent>) -> Self {
|
||||||
StatusTray {
|
StatusTray {
|
||||||
device_name: None,
|
device_properties: None,
|
||||||
message: NO_COMPATIBLE_DEVICE.to_string(),
|
update_sender,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,43 +52,183 @@ impl Tray for StatusTray {
|
|||||||
fn id(&self) -> String {
|
fn id(&self) -> String {
|
||||||
env!("CARGO_PKG_NAME").into()
|
env!("CARGO_PKG_NAME").into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn icon_name(&self) -> String {
|
fn icon_name(&self) -> String {
|
||||||
"audio-headset".into()
|
"audio-headset".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tool_tip(&self) -> ToolTip {
|
fn tool_tip(&self) -> ToolTip {
|
||||||
let description = self
|
let Some(device_properties) = self.device_properties.as_ref() else {
|
||||||
.message
|
return ToolTip {
|
||||||
.lines()
|
title: "Unknown".to_string(),
|
||||||
.filter(|l| !l.contains("Unknown"))
|
description: NO_COMPATIBLE_DEVICE.to_string(),
|
||||||
.collect::<Vec<&str>>()
|
icon_name: "audio-headset".into(),
|
||||||
.join("\n");
|
icon_pixmap: Vec::new(),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
let description = if device_properties.connected.unwrap_or(false) {
|
||||||
|
device_properties
|
||||||
|
.to_string_with_padding(0)
|
||||||
|
.lines()
|
||||||
|
.filter(|l| !l.contains("Unknown"))
|
||||||
|
.collect::<Vec<&str>>()
|
||||||
|
.join("\n")
|
||||||
|
} else {
|
||||||
|
HEADSET_NOT_CONNECTED.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
ToolTip {
|
ToolTip {
|
||||||
title: self.device_name.clone().unwrap_or("Unknown".to_string()),
|
title: device_properties
|
||||||
|
.device_name
|
||||||
|
.clone()
|
||||||
|
.unwrap_or("Unknown".to_string()),
|
||||||
description,
|
description,
|
||||||
icon_name: "audio-headset".into(),
|
icon_name: "audio-headset".into(),
|
||||||
icon_pixmap: Vec::new(),
|
icon_pixmap: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn menu(&self) -> Vec<MenuItem<Self>> {
|
fn menu(&self) -> Vec<MenuItem<Self>> {
|
||||||
let mut state_items: Vec<MenuItem<Self>> = self
|
let make_exit = || StandardItem {
|
||||||
.message
|
|
||||||
.lines()
|
|
||||||
.map(|line| {
|
|
||||||
StandardItem {
|
|
||||||
label: line.to_string(),
|
|
||||||
enabled: false,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
.into()
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let exit = StandardItem {
|
|
||||||
label: "Exit".into(),
|
label: "Exit".into(),
|
||||||
icon_name: "application-exit".into(),
|
icon_name: "application-exit".into(),
|
||||||
activate: Box::new(|_| std::process::exit(0)),
|
activate: Box::new(|_| std::process::exit(0)),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
state_items.push(exit.into());
|
let mut menu_items: Vec<MenuItem<Self>> = Vec::new();
|
||||||
state_items
|
|
||||||
|
let Some(device_properties) = self.device_properties.as_ref() else {
|
||||||
|
menu_items.push(
|
||||||
|
StandardItem {
|
||||||
|
label: NO_COMPATIBLE_DEVICE.to_string(),
|
||||||
|
enabled: false,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
menu_items.push(make_exit().into());
|
||||||
|
return menu_items;
|
||||||
|
};
|
||||||
|
|
||||||
|
if !device_properties.connected.unwrap_or(false) {
|
||||||
|
menu_items.push(
|
||||||
|
StandardItem {
|
||||||
|
label: HEADSET_NOT_CONNECTED.to_string(),
|
||||||
|
enabled: false,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
menu_items.push(make_exit().into());
|
||||||
|
return menu_items;
|
||||||
|
}
|
||||||
|
for property in device_properties.get_properties() {
|
||||||
|
match property {
|
||||||
|
hyper_headset::devices::PropertyDescriptorWrapper::Int(property, []) => {
|
||||||
|
let Some(current_value) = property.data else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let create_event = property.create_event;
|
||||||
|
menu_items.push(
|
||||||
|
StandardItem {
|
||||||
|
label: format!(
|
||||||
|
"{} {}{}",
|
||||||
|
property.prefix, current_value, property.suffix
|
||||||
|
),
|
||||||
|
enabled: false,
|
||||||
|
activate: Box::new(move |_| {
|
||||||
|
let _ = (create_event)(!current_value);
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
hyper_headset::devices::PropertyDescriptorWrapper::Int(property, options) => {
|
||||||
|
let Some(current_value) = property.data else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let create_event = property.create_event;
|
||||||
|
let sub_menu = options
|
||||||
|
.iter()
|
||||||
|
.map(|val| {
|
||||||
|
let update_sender = self.update_sender.clone();
|
||||||
|
StandardItem {
|
||||||
|
label: format!("{}{}", val, property.suffix),
|
||||||
|
enabled: property.property_type == PropertyType::ReadWrite
|
||||||
|
&& property.data.is_some(),
|
||||||
|
activate: Box::new(move |_| {
|
||||||
|
if let Some(command) = (create_event)(*val) {
|
||||||
|
let _ = update_sender.send(command);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
menu_items.push(
|
||||||
|
SubMenu {
|
||||||
|
label: format!(
|
||||||
|
"{} {}{}",
|
||||||
|
property.prefix, current_value, property.suffix
|
||||||
|
),
|
||||||
|
enabled: property.property_type == PropertyType::ReadWrite
|
||||||
|
&& property.data.is_some(),
|
||||||
|
submenu: sub_menu,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
hyper_headset::devices::PropertyDescriptorWrapper::Bool(property) => {
|
||||||
|
let Some(current_value) = property.data else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let create_event = property.create_event;
|
||||||
|
let update_sender = self.update_sender.clone();
|
||||||
|
menu_items.push(
|
||||||
|
StandardItem {
|
||||||
|
label: format!(
|
||||||
|
"{} {}{}",
|
||||||
|
property.prefix, current_value, property.suffix
|
||||||
|
),
|
||||||
|
enabled: property.property_type == PropertyType::ReadWrite
|
||||||
|
&& property.data.is_some(),
|
||||||
|
activate: Box::new(move |_| {
|
||||||
|
if let Some(command) = (create_event)(!current_value) {
|
||||||
|
let _ = update_sender.send(command);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
hyper_headset::devices::PropertyDescriptorWrapper::String(property) => {
|
||||||
|
let Some(current_value) = property.data else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let create_event = property.create_event;
|
||||||
|
menu_items.push(
|
||||||
|
StandardItem {
|
||||||
|
label: format!(
|
||||||
|
"{} {}{}",
|
||||||
|
property.prefix, current_value, property.suffix
|
||||||
|
),
|
||||||
|
enabled: false,
|
||||||
|
activate: Box::new(move |_| {
|
||||||
|
let _ = (create_event)(String::new());
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
menu_items.push(make_exit().into());
|
||||||
|
menu_items
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user