diff --git a/src/devices/cloud_ii_wireless_dts.rs b/src/devices/cloud_ii_wireless_dts.rs new file mode 100644 index 0000000..e8cdec6 --- /dev/null +++ b/src/devices/cloud_ii_wireless_dts.rs @@ -0,0 +1,173 @@ +use std::time::Duration; +use crate::devices::{ChargingStatus, Color, Device, DeviceError, DeviceEvent, DeviceState}; + +// Possible vendor IDs [HP] +const VENDOR_IDS: [u16; 1] = [0x03F0]; +// Possible Cloud II Wireless product IDs +const PRODUCT_IDS: [u16; 4] = [0x1718, 0x018B, 0x0D93, 0x0696]; + +const BASE_PACKET: [u8; 20] = { + let mut packet = [0; 20]; + (packet[0], packet[1], packet[2]) = (0x06, 0xff, 0xbb); + packet +}; + +const BASE_PACKET2: [u8; 20] = { + let mut packet = [0; 20]; + (packet[0], packet[1]) = (33, 187); + packet +}; + +const GET_CHARGING_CMD_ID: u8 = 3; +const GET_MIC_CONNECTED_CMD_ID: u8 = 8; +const GET_BATTERY_CMD_ID: u8 = 2; +const GET_AUTO_SHUTDOWN_CMD_ID: u8 = 7; +const SET_AUTO_SHUTDOWN_CMD_ID: u8 = 34; +const GET_MUTE_CMD_ID: u8 = 5; +const SET_MUTE_CMD_ID: u8 = 32; +const GET_PAIRING_CMD_ID: u8 = 9; +const GET_PRODUCT_COLOR_CMD_ID: u8 = 14; +const GET_SIDE_TONE_ON_CMD_ID: u8 = 6; +const SET_SIDE_TONE_ON_CMD_ID: u8 = 33; +const GET_SIDE_TONE_VOLUME_CMD_ID: u8 = 11; +const SET_SIDE_TONE_VOLUME_CMD_ID: u8 = 35; +const GET_VOICE_PROMPT_CMD_ID: u8 = 9; +const SET_VOICE_PROMPT_CMD_ID: u8 = 19; +const GET_WIRELESS_STATUS_CMD_ID: u8 = 1; + +pub struct CloudIIWirelessDTS { + state: DeviceState, +} + +impl CloudIIWirelessDTS { + pub fn new_from_state(state: DeviceState) -> Self { + CloudIIWirelessDTS { state } + } + + pub fn new() -> Result { + let state = DeviceState::new(&PRODUCT_IDS, &VENDOR_IDS)?; + Ok(CloudIIWirelessDTS { state }) + } +} + +impl Device for CloudIIWirelessDTS { + fn get_charging_packet(&self) -> Vec { + let mut tmp = BASE_PACKET.to_vec(); + tmp[3] = GET_CHARGING_CMD_ID; + tmp + } + + fn get_battery_packet(&self) -> Vec { + let mut tmp = BASE_PACKET.to_vec(); + tmp[3] = GET_BATTERY_CMD_ID; + tmp + } + + fn set_automatic_shut_down_packet(&self, shutdown_after: Duration) -> Vec { + let mut tmp = BASE_PACKET.to_vec(); + tmp[3] = SET_AUTO_SHUTDOWN_CMD_ID; + tmp[4] = (shutdown_after.as_secs() / 60) as u8; + tmp + } + + fn get_automatic_shut_down_packet(&self) -> Vec { + let mut tmp = BASE_PACKET.to_vec(); + tmp[3] = GET_AUTO_SHUTDOWN_CMD_ID; + tmp + } + + fn get_mute_packet(&self) -> Vec { + let mut tmp = BASE_PACKET.to_vec(); + tmp[3] = GET_MUTE_CMD_ID; + tmp + } + + fn set_mute_packet(&self, mute: bool) -> Vec { + let mut tmp = BASE_PACKET.to_vec(); + tmp[3] = SET_MUTE_CMD_ID; + tmp[4] = mute as u8; + tmp + } + + fn get_mic_connected_packet(&self) -> Vec { + let mut tmp = BASE_PACKET.to_vec(); + tmp[3] = GET_MIC_CONNECTED_CMD_ID; + tmp + } + + fn get_pairing_info_packet(&self) -> Vec { + let mut tmp = BASE_PACKET.to_vec(); + tmp[3] = GET_PAIRING_CMD_ID; + tmp + } + + fn get_product_color_packet(&self) -> Vec { + let mut tmp = BASE_PACKET2.to_vec(); + tmp[2] = GET_PRODUCT_COLOR_CMD_ID; + tmp + } + + fn get_side_tone_packet(&self) -> Vec { + let mut tmp = BASE_PACKET.to_vec(); + tmp[3] = GET_SIDE_TONE_ON_CMD_ID; + tmp + } + + fn set_side_tone_packet(&self, side_tone_on: bool) -> Vec { + let mut tmp = BASE_PACKET.to_vec(); + tmp[3] = SET_SIDE_TONE_ON_CMD_ID; + tmp[4] = side_tone_on as u8; + tmp + } + + fn get_side_tone_volume_packet(&self) -> Vec { + let mut tmp = BASE_PACKET.to_vec(); + tmp[3] = GET_SIDE_TONE_VOLUME_CMD_ID; + tmp + } + + fn set_side_tone_volume_packet(&self) -> Vec { + let mut tmp = BASE_PACKET.to_vec(); + tmp[3] = SET_SIDE_TONE_VOLUME_CMD_ID; + tmp + } + + fn get_voice_prompt_packet(&self) -> Vec { + let mut tmp = BASE_PACKET2.to_vec(); + tmp[2] = GET_VOICE_PROMPT_CMD_ID; + tmp + } + + fn set_voice_prompt_packet(&self, enable: bool) -> Vec { + let mut tmp = BASE_PACKET2.to_vec(); + tmp[2] = SET_VOICE_PROMPT_CMD_ID; + tmp + } + + fn get_wireless_connected_status_packet(&self) -> Vec { + let mut tmp = BASE_PACKET.to_vec(); + tmp[3] = GET_WIRELESS_STATUS_CMD_ID; + tmp + } + + fn get_event_from_device_response(&self, response: &[u8]) -> Option { + match (response[2], response[3], response[4], response[7]) { + (_, GET_CHARGING_CMD_ID, status, _) => Some(DeviceEvent::Charging(ChargingStatus::from(status))), + (_, GET_MIC_CONNECTED_CMD_ID, status, _) => Some(DeviceEvent::MicConnected(status == 1)), + (_, GET_BATTERY_CMD_ID, _, level) => Some(DeviceEvent::BatterLevel(level)), + (_, GET_AUTO_SHUTDOWN_CMD_ID, time, _) => Some(DeviceEvent::AutomaticShutdownAfter(Duration::from_secs(time as u64 * 60))), + (_, GET_MUTE_CMD_ID, status, _) => Some(DeviceEvent::Muted(status == 1)), + (_, GET_PAIRING_CMD_ID, status, _) => Some(DeviceEvent::PairingInfo(status)), + (_, GET_SIDE_TONE_ON_CMD_ID, status, _) => Some(DeviceEvent::SideToneOn(status == 1)), + (_, GET_SIDE_TONE_VOLUME_CMD_ID, status, _) => Some(DeviceEvent::SideToneVolume(status)), + (_, GET_WIRELESS_STATUS_CMD_ID, status, _) => Some(DeviceEvent::WirelessConnected(status == 1 || status == 4)), + (GET_VOICE_PROMPT_CMD_ID, status, _, _) => Some(DeviceEvent::VoicePrompt(status == 1)), + (GET_PRODUCT_COLOR_CMD_ID, status, _, _) => Some(DeviceEvent::ProductColor(Color::from(status))), + _ => None + } + } + + fn get_device_state(&mut self) -> &mut DeviceState { + &mut self.state + } +} \ No newline at end of file diff --git a/src/devices/mod.rs b/src/devices/mod.rs new file mode 100644 index 0000000..845db84 --- /dev/null +++ b/src/devices/mod.rs @@ -0,0 +1,207 @@ +mod cloud_ii_wireless_dts; + +use hidapi::{HidApi, HidDevice, HidError}; +use std::time::Duration; +use thistermination::TerminationFull; + +//TODO: connect to rest of code base +//TODO: remove old lib stuff + +pub struct DeviceState { + hid_device: HidDevice, + pub battery_level: Option, + pub charging: Option, + pub muted: Option, + pub mic_connected: Option, + pub automatic_shutdown_after: Option, + pub pairing_info: Option, + pub product_color: Option, + pub side_tone_on: Option, + pub side_tone_volume: Option, + pub voice_prompt_on: Option, + pub connected: Option, +} + +impl DeviceState { + pub fn new(product_ids: &[u16], vendor_ids: &[u16]) -> Result { + let hid_api = HidApi::new()?; + let hid_device = hid_api + .device_list() + .find_map(|info| { + if product_ids.contains(&info.product_id()) + && vendor_ids.contains(&info.vendor_id()) + { + Some(hid_api.open(info.vendor_id(), info.product_id())) + } else { + None + } + }) + .ok_or(DeviceError::NoDeviceFound())??; + Ok(DeviceState { + hid_device, + charging: None, + battery_level: None, + muted: None, + mic_connected: None, + automatic_shutdown_after: None, + pairing_info: None, + product_color: None, + side_tone_on: None, + side_tone_volume: None, + voice_prompt_on: None, + connected: None, + }) + } + + 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::VoicePrompt(on) => self.voice_prompt_on = Some(*on), + DeviceEvent::WirelessConnected(connected) => self.connected = Some(*connected), + }; + } + + pub fn clear_state(&mut self) { + self.charging = None; + self.battery_level = None; + self.muted = None; + self.mic_connected = None; + } +} + +#[derive(TerminationFull)] +pub enum DeviceError { + #[termination(msg("{0:?}"))] + HidError(#[from] HidError), + #[termination(msg("No device found."))] + NoDeviceFound(), + #[termination(msg("No response. Is the headset turned on?"))] + HeadSetOff(), + #[termination(msg("No response."))] + NoResponse(), + #[termination(msg("Unknown response: {0:?} with length: {1:?}"))] + UnknownResponse([u8; 8], usize), +} + +#[derive(Debug, Copy, Clone)] +pub enum DeviceEvent { + BatterLevel(u8), + Muted(bool), + MicConnected(bool), + Charging(ChargingStatus), + AutomaticShutdownAfter(Duration), + PairingInfo(u8), + ProductColor(Color), + SideToneOn(bool), + SideToneVolume(u8), + VoicePrompt(bool), + WirelessConnected(bool), +} + +#[derive(Debug, Copy, Clone)] +pub enum Color { + Red, + UnknownColor(u8), +} + +impl From for Color { + fn from(color: u8) -> Self { + match color { + 0 => Color::Red, + _ => Color::UnknownColor(color), + } + } +} + +#[derive(Debug, Copy, Clone)] +pub enum ChargingStatus { + NotCharging, + Charging, + FullyCharged, + ChargeError, +} + +impl From for ChargingStatus { + fn from(value: u8) -> ChargingStatus { + match value { + 0 => ChargingStatus::NotCharging, + 1 => ChargingStatus::Charging, + 2 => ChargingStatus::FullyCharged, + _ => ChargingStatus::ChargeError, + } + } +} + +trait Device { + fn get_charging_packet(&self) -> Vec; + fn get_battery_packet(&self) -> Vec; + fn set_automatic_shut_down_packet(&self, shutdown_after: Duration) -> Vec; + fn get_automatic_shut_down_packet(&self) -> Vec; + fn get_mute_packet(&self) -> Vec; + fn set_mute_packet(&self, mute: bool) -> Vec; + fn get_mic_connected_packet(&self) -> Vec; + fn get_pairing_info_packet(&self) -> Vec; + fn get_product_color_packet(&self) -> Vec; + fn get_side_tone_packet(&self) -> Vec; + fn set_side_tone_packet(&self, side_tone_on: bool) -> Vec; + fn get_side_tone_volume_packet(&self) -> Vec; + fn set_side_tone_volume_packet(&self) -> Vec; + fn get_voice_prompt_packet(&self) -> Vec; + fn set_voice_prompt_packet(&self, enable: bool) -> Vec; + fn get_wireless_connected_status_packet(&self) -> Vec; + fn get_event_from_device_response(&self, response: &[u8]) -> Option; + fn get_device_state(&mut self) -> &mut DeviceState; + + fn wait_for_updates(&mut self, duration: Duration) -> Option { + let mut buf = [0u8; 8]; + let res = self + .get_device_state() + .hid_device + .read_timeout(&mut buf[..], duration.as_millis() as i32) + .ok()?; + + self.get_event_from_device_response(&buf[0..res]) + } + + fn refresh_state(&mut self) -> Result<(), DeviceError> { + let packets = vec![ + self.get_charging_packet(), + self.get_battery_packet(), + self.get_automatic_shut_down_packet(), + self.get_mute_packet(), + self.get_mic_connected_packet(), + self.get_pairing_info_packet(), + self.get_product_color_packet(), + self.get_side_tone_packet(), + self.get_side_tone_volume_packet(), + self.get_voice_prompt_packet(), + self.get_wireless_connected_status_packet(), + ]; + + let mut responded = false; + for packet in packets { + self.get_device_state().hid_device.write(&packet)?; + if let Some(event) = self.wait_for_updates(Duration::from_secs(1)) { + self.get_device_state().update_self_with_event(&event); + responded = true; + } + } + + if responded { + Ok(()) + } else { + Err(DeviceError::NoResponse()) + } + } +} + diff --git a/src/lib.rs b/src/lib.rs index ff41290..1cfe429 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,8 @@ use std::time::Duration; use hidapi::{HidApi, HidDevice, HidError}; use thistermination::TerminationFull; +mod devices; + // Possible vendor IDs [hyperx , HP] const VENDOR_IDS: [u16; 2] = [0x0951, 0x03F0]; // Possible Cloud II Wireless product IDs