diff --git a/manager-ui/src/components/SoundModeCard/soundModeCard.tsx b/manager-ui/src/components/SoundModeCard/soundModeCard.tsx index 16a604c..2e5eebc 100644 --- a/manager-ui/src/components/SoundModeCard/soundModeCard.tsx +++ b/manager-ui/src/components/SoundModeCard/soundModeCard.tsx @@ -1,10 +1,10 @@ -import { Button, Grid, Icon, Paper, Stack, styled } from '@mui/material'; +import { Button, Collapse, Grid, Icon, Paper, Stack, styled } from '@mui/material'; import { useSoundcoreStore } from '@stores/useSoundcoreStore'; import ANCIcon from '../../assets/ambient_icon_anc.png'; import NormalIcon from '../../assets/ambient_icon_off.png'; import TransIcon from '../../assets/ambient_icon_trans.png'; import React, { useCallback, useEffect, useState } from 'react'; -import { CurrentSoundMode, DeviceFeatureSet, SoundMode } from '@generated-types/soundcore-lib.d.ts'; +import { CurrentSoundMode, DeviceFeatureSet, SoundMode } from '@generated-types/soundcore-lib'; import { useUpdateDeviceSoundMode } from '@hooks/useDeviceCommand'; export const SoundModeCard: React.FC<{ features: DeviceFeatureSet }> = ({ features }) => { @@ -98,10 +98,12 @@ export const SoundModeCard: React.FC<{ features: DeviceFeatureSet }> = ({ featur const currentSoundModeKey = mapModeToCurrentSoundModeKey(selectedSoundMode.current); let currentSubValue; let currentSoundModeType: string; + let hasCustomValueSlider: boolean = false; if (currentSoundModeKey) { currentSubValue = selectedSoundMode[currentSoundModeKey].value; currentSoundModeType = selectedSoundMode[currentSoundModeKey].type; + hasCustomValueSlider = currentSoundModeType.toLowerCase() === 'custom'; } return ( @@ -129,10 +131,10 @@ export const SoundModeCard: React.FC<{ features: DeviceFeatureSet }> = ({ featur icon={ANCIcon} setSliderIcon={setIcon} setSliderPosition={() => - setSelectedSoundMode((prev) => ({ - ...prev, + useUpdateDeviceSoundMode(deviceAddr, { + ...selectedSoundMode, current: CurrentSoundMode.ANC - })) + }) } /> )} @@ -142,10 +144,10 @@ export const SoundModeCard: React.FC<{ features: DeviceFeatureSet }> = ({ featur icon={NormalIcon} setSliderIcon={setIcon} setSliderPosition={() => - setSelectedSoundMode((prev) => ({ - ...prev, + useUpdateDeviceSoundMode(deviceAddr, { + ...selectedSoundMode, current: CurrentSoundMode.Normal - })) + }) } /> )} @@ -155,28 +157,31 @@ export const SoundModeCard: React.FC<{ features: DeviceFeatureSet }> = ({ featur icon={TransIcon} setSliderIcon={setIcon} setSliderPosition={() => - setSelectedSoundMode((prev) => ({ - ...prev, + useUpdateDeviceSoundMode(deviceAddr, { + ...selectedSoundMode, current: CurrentSoundMode.Transparency - })) + }) } /> )} - {modeButtons && modeButtons.length > 0 && currentSoundModeKey && ( + 0} timeout="auto"> - useUpdateDeviceSoundMode(deviceAddr, { - ...selectedSoundMode, - [currentSoundModeKey]: { type: currentSoundModeType, value } - }) - } + onClick={(value) => { + if (currentSoundModeKey) { + useUpdateDeviceSoundMode(deviceAddr, { + ...selectedSoundMode, + [currentSoundModeKey]: { type: currentSoundModeType, value } + }); + } + }} selectedValue={currentSubValue} /> - )} + + {hasCustomValueSlider &&

Custom

} {/* */} @@ -195,16 +200,17 @@ const ModeGroupButtons: React.FC<{ container direction="row" spacing={1} - sx={{ display: 'flex', justifyContent: 'space-evenly', pt: 2 }}> + sx={{ display: 'flex', justifyContent: 'space-evenly', pt: 2 }}> + {buttons.map((button) => ( + onClick(button.value)}> + {button.title} + + ))} + - {buttons.map((button) => ( - onClick(button.value)}> - {button.title} - - ))} ); }; diff --git a/soundcore-lib/src/ble/btleplug/connection.rs b/soundcore-lib/src/ble/btleplug/connection.rs index ef90f91..1191bf3 100644 --- a/soundcore-lib/src/ble/btleplug/connection.rs +++ b/soundcore-lib/src/ble/btleplug/connection.rs @@ -173,6 +173,7 @@ impl BLEConnection for BtlePlugConnection { self.write_characteristic.clone(), bytes.to_owned(), ); + trace!("Writing bytes: {:#X?} to characteristic: {:?}", bytes, self.write_characteristic); tokio::spawn(async move { peripheral .write(&writer_characteristic, &bytes, write_type.into()) diff --git a/soundcore-lib/src/device.rs b/soundcore-lib/src/device.rs index e4117a8..41fa4b5 100644 --- a/soundcore-lib/src/device.rs +++ b/soundcore-lib/src/device.rs @@ -12,7 +12,7 @@ use crate::error::{SoundcoreLibError, SoundcoreLibResult}; use crate::models::{EQConfiguration, SoundMode}; use crate::packets::{ DeviceStateResponse, RequestPacketBuilder, RequestPacketKind, ResponsePacket, - StateTransformationPacket, + SoundModeCommandBuilder, StateTransformationPacket, }; use crate::parsers::TaggedData; use crate::types::SupportedModels; @@ -41,7 +41,7 @@ where ); let state_sender = Arc::new(Mutex::new(watch::channel(initial_state.data.clone()).0)); let packet_handler = Self::spawn_packet_handler(state_sender.to_owned(), byte_channel); - + let model = if let Some(sn) = initial_state.data.serial { sn.to_model().unwrap_or(initial_state.tag) } else { @@ -134,6 +134,10 @@ where tokio::task::spawn(async move { while let Some(bytes) = byte_channel.recv().await { trace!("Received bytes: {:?}", bytes); + if bytes.is_empty() { + continue; + } + match ResponsePacket::from_bytes(&bytes) { Ok(packet) => { let state_sender = state_sender.lock().await; @@ -165,7 +169,26 @@ where } pub async fn set_sound_mode(&self, sound_mode: SoundMode) -> SoundcoreLibResult<()> { - todo!() + // TODO: perform some validation on the sound mode/features + // TODO: Check if https://github.com/Oppzippy/OpenSCQ30/blob/dec0ad3f2659205ff6efdb8d12ec333ba9f3a0b4/lib/src/soundcore_device/device/device_command_dispatcher.rs#L28 + // is valid for all models or device-specific + let command = SoundModeCommandBuilder::new(sound_mode, self.model).build(); + let latest_state = self.latest_state().await; + + if latest_state.sound_mode == sound_mode { + return Ok(()); + } + + self.connection + .write(&command, WriteType::WithoutResponse) + .await?; + + let state_sender = self.state_channel.lock().await; + let mut new_state = state_sender.borrow().clone(); + new_state.sound_mode = sound_mode; + state_sender.send_replace(new_state); + + Ok(()) } pub async fn set_eq(&self, eq: EQConfiguration) -> SoundcoreLibResult<()> { diff --git a/soundcore-lib/src/devices/a3040.rs b/soundcore-lib/src/devices/a3040.rs index 02c6244..aba382c 100644 --- a/soundcore-lib/src/devices/a3040.rs +++ b/soundcore-lib/src/devices/a3040.rs @@ -1,3 +1,5 @@ mod features; +mod sound_mode_update_command; -pub use features::*; \ No newline at end of file +pub use features::*; +pub use sound_mode_update_command::*; \ No newline at end of file diff --git a/soundcore-lib/src/devices/a3040/sound_mode_update_command.rs b/soundcore-lib/src/devices/a3040/sound_mode_update_command.rs new file mode 100644 index 0000000..7ec3dcd --- /dev/null +++ b/soundcore-lib/src/devices/a3040/sound_mode_update_command.rs @@ -0,0 +1,53 @@ +use crate::{models::SoundMode, packets::Packet}; + +pub struct A3040SoundModeUpdateCommand { + sound_mode: SoundMode, +} + +impl A3040SoundModeUpdateCommand { + pub fn new(sound_mode: SoundMode) -> Self { + Self { sound_mode } + } +} + +impl Packet for A3040SoundModeUpdateCommand { + fn command(&self) -> [u8; 7] { + [0x08, 0xEE, 0x00, 0x00, 0x00, 0x06, 0x81] + } + + fn payload(&self) -> Vec { + self.sound_mode.to_bytes_with_custom_transparency().to_vec() + } +} + +#[cfg(test)] +mod tests { + use std::vec; + + use super::*; + use crate::models::{ + ANCMode, AdaptiveANCMode, CurrentSoundMode, CustomANCValue, CustomTransparencyValue, + CustomizableTransparencyMode, TransparencyMode, + }; + + #[test] + fn test_sound_mode_update_command() { + let command = A3040SoundModeUpdateCommand { + sound_mode: SoundMode { + current: CurrentSoundMode::ANC, + anc_mode: ANCMode::Adaptive(AdaptiveANCMode::Adaptive), + custom_anc: CustomANCValue::from_u8(5), + trans_mode: TransparencyMode::Customizable(CustomizableTransparencyMode::Custom), + custom_trans: Some(CustomTransparencyValue::from_u8(3)), + }, + }; + + assert_eq!( + command.bytes(), + vec![ + 0x08, 0xee, 0x00, 0x00, 0x00, 0x06, 0x81, 0x10, 0x00, 0x00, 0x51, 0x01, 0x01, 0x00, + 0x03, 0xe3, + ] + ); + } +} diff --git a/soundcore-lib/src/models/curr_sound_mode.rs b/soundcore-lib/src/models/curr_sound_mode.rs index 3a85bf7..c59a8b0 100644 --- a/soundcore-lib/src/models/curr_sound_mode.rs +++ b/soundcore-lib/src/models/curr_sound_mode.rs @@ -18,7 +18,7 @@ FromRepr, Display, Hash, )] -#[serde(rename_all = "camelCase")] +#[serde(rename_all = "lowercase")] #[typeshare] pub enum CurrentSoundMode { ANC = 0, diff --git a/soundcore-lib/src/models/custom_anc_value.rs b/soundcore-lib/src/models/custom_anc_value.rs index 8ba29cb..1fd117b 100644 --- a/soundcore-lib/src/models/custom_anc_value.rs +++ b/soundcore-lib/src/models/custom_anc_value.rs @@ -11,7 +11,8 @@ impl CustomANCValue { trace!("CustomANC::from_u8({})", value); match value { 255 => CustomANCValue(255), - _ => CustomANCValue(value.clamp(0, 10)), + // TODO: Check if any other device has a different range and implement it + _ => CustomANCValue(value.clamp(0, 5)), } } diff --git a/soundcore-lib/src/models/sound_mode.rs b/soundcore-lib/src/models/sound_mode.rs index f641b91..35851ca 100644 --- a/soundcore-lib/src/models/sound_mode.rs +++ b/soundcore-lib/src/models/sound_mode.rs @@ -6,7 +6,7 @@ use crate::models::custom_trans_value::CustomTransparencyValue; use super::{ANCMode, CurrentSoundMode, CustomANCValue, TransparencyMode}; #[derive( -Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Clone, Copy, Default, Hash, + Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Clone, Copy, Default, Hash, )] #[typeshare] #[serde(rename_all = "camelCase", tag = "type")] @@ -28,12 +28,13 @@ impl SoundMode { ] } - pub fn to_bytes_with_custom_transparency(&self) -> [u8; 5] { + pub fn to_bytes_with_custom_transparency(&self) -> [u8; 6] { [ self.current.as_u8(), - self.anc_mode.as_u8(), + (self.custom_anc.as_u8() << 4) | 0x01, // TODO: 0x01 mask is unknown if it is constant self.trans_mode.as_u8(), - self.custom_anc.as_u8(), + self.anc_mode.as_u8(), + 0x00, self.custom_trans.unwrap_or_default().as_u8(), ] } diff --git a/soundcore-lib/src/packets.rs b/soundcore-lib/src/packets.rs index 510d638..bca613d 100644 --- a/soundcore-lib/src/packets.rs +++ b/soundcore-lib/src/packets.rs @@ -1,12 +1,12 @@ -pub use request::*; -pub use response::*; - -use crate::parsers::generate_checksum; - mod command; mod request; mod response; +use crate::parsers::generate_checksum; +pub use command::*; +pub use request::*; +pub use response::*; + const PACKET_SIZE_LENGTH: usize = 2; const CHECKSUM_BIT_LENGTH: usize = 1; @@ -18,7 +18,7 @@ pub trait Packet { let length_bytes: [u8; PACKET_SIZE_LENGTH] = ((command.len() + PACKET_SIZE_LENGTH + payload.len() + CHECKSUM_BIT_LENGTH) as u16) .to_le_bytes(); - let mut bytes = vec![command.to_vec(), length_bytes.to_vec(), payload].concat(); + let mut bytes = [command.to_vec(), length_bytes.to_vec(), payload].concat(); bytes.push(generate_checksum(&bytes)); bytes diff --git a/soundcore-lib/src/packets/command.rs b/soundcore-lib/src/packets/command.rs index 8b13789..117872c 100644 --- a/soundcore-lib/src/packets/command.rs +++ b/soundcore-lib/src/packets/command.rs @@ -1 +1,3 @@ +mod sound_mode; +pub use sound_mode::*; \ No newline at end of file diff --git a/soundcore-lib/src/packets/command/sound_mode.rs b/soundcore-lib/src/packets/command/sound_mode.rs new file mode 100644 index 0000000..5033135 --- /dev/null +++ b/soundcore-lib/src/packets/command/sound_mode.rs @@ -0,0 +1,23 @@ +use crate::{ + devices::A3040SoundModeUpdateCommand, models::SoundMode, packets::Packet, + types::SupportedModels, +}; + +pub struct SoundModeCommandBuilder { + sound_mode: SoundMode, + model: SupportedModels, +} + +impl SoundModeCommandBuilder { + pub fn new(sound_mode: SoundMode, model: SupportedModels) -> Self { + Self { sound_mode, model } + } + + pub fn build(self) -> Vec { + match self.model { + SupportedModels::A3040 => A3040SoundModeUpdateCommand::new(self.sound_mode).bytes(), + // TODO: use a default comamnd A3951? + _ => panic!("Unsupported model"), + } + } +} diff --git a/soundcore-lib/src/packets/response.rs b/soundcore-lib/src/packets/response.rs index 6279a62..329ea29 100644 --- a/soundcore-lib/src/packets/response.rs +++ b/soundcore-lib/src/packets/response.rs @@ -1,4 +1,4 @@ -use log::error; +use log::{error, warn}; use nom::error::VerboseError; pub use info::*; @@ -17,6 +17,7 @@ pub enum ResponsePacket { DeviceState(TaggedData), SoundModeUpdate(SoundModeUpdateResponse), DeviceInfo(DeviceInfoResponse), + Unknown } pub trait StateTransformationPacket { @@ -36,7 +37,14 @@ impl ResponsePacket { Self::SoundModeUpdate(parse_sound_mode_update_packet(bytes)?.1) } ResponsePacketKind::InfoUpdate => Self::DeviceInfo(parse_device_info_packet(bytes)?.1), - _ => unimplemented!(), + _ => { + // TODO: Have an array of Acks and handle those properly + error!( + "Unexpected or unhandled packet kind {:?} and bytes {:?}", + packet_header.kind, bytes + ); + ResponsePacket::Unknown + }, }) }