diff --git a/lib/src/devices/a3933/device_profile.rs b/lib/src/devices/a3933/device_profile.rs index b6af4468..ef76ea83 100644 --- a/lib/src/devices/a3933/device_profile.rs +++ b/lib/src/devices/a3933/device_profile.rs @@ -1,18 +1,36 @@ -use std::sync::Arc; +use std::{ + collections::HashMap, + sync::{ + atomic::{self, AtomicI32}, + Arc, + }, +}; + +use nom::error::VerboseError; use crate::{ device_profiles::{ DeviceProfile, NoiseCancelingModeType, SoundModeProfile, TransparencyModeType, }, - devices::a3945::device_profile::A3945Dispatcher, + devices::standard::{ + packets::outbound::{OutboundPacket, OutboundPacketBytes, SetEqualizerPacket}, + state::DeviceState, + structures::{EqualizerConfiguration, STATE_UPDATE}, + }, + soundcore_device::device::{ + device_command_dispatcher::DeviceCommandDispatcher, + packet_handlers::state_update::state_update_handler, soundcore_command::CommandResponse, + }, }; +use super::packets::inbound::take_a3933_state_update_packet; + pub const A3933_DEVICE_PROFILE: DeviceProfile = DeviceProfile { sound_mode: Some(SoundModeProfile { noise_canceling_mode_type: NoiseCancelingModeType::Basic, transparency_mode_type: TransparencyModeType::Custom, }), - has_hear_id: false, + has_hear_id: true, num_equalizer_channels: 2, num_equalizer_bands: 8, has_dynamic_range_compression: true, @@ -22,6 +40,204 @@ pub const A3933_DEVICE_PROFILE: DeviceProfile = DeviceProfile { has_touch_tone: false, has_auto_power_off: false, has_ambient_sound_mode_cycle: true, - // The A3933 has the same quirks as the A3945 - custom_dispatchers: Some(|| Arc::new(A3945Dispatcher::default())), + custom_dispatchers: Some(|| Arc::new(A3933Dispatcher::default())), }; + +#[derive(Debug, Default)] +pub struct A3933Dispatcher { + // The official app only displays 8 bands, so I have no idea what bands 9 and 10 do. We'll just keep track + // of their initial value and resend that. + band_9_and_10_left_and_right: Arc, +} + +impl DeviceCommandDispatcher for A3933Dispatcher { + fn packet_handlers( + &self, + ) -> HashMap<[u8; 7], Box DeviceState + Send + Sync>> { + let band_9_and_10 = self.band_9_and_10_left_and_right.to_owned(); + let mut handlers: HashMap< + [u8; 7], + Box DeviceState + Send + Sync>, + > = HashMap::new(); + + handlers.insert( + STATE_UPDATE, + Box::new(move |packet_bytes, state| { + let result = take_a3933_state_update_packet::>(packet_bytes); + let packet = match result { + Ok((_, packet)) => packet, + Err(err) => { + tracing::error!("failed to parse packet: {err:?}"); + return state; + } + }; + let squashed_bytes = i32::from_ne_bytes([ + packet.left_band_9_and_10[0], + packet.left_band_9_and_10[1], + packet.right_band_9_and_10[0], + packet.right_band_9_and_10[1], + ]); + band_9_and_10.store(squashed_bytes, atomic::Ordering::Relaxed); + + // We only needed to capture information. The actual state transformation is passed on to the default handler.. + state_update_handler(packet_bytes, state) + }), + ); + + handlers + } + + fn set_equalizer_configuration( + &self, + state: DeviceState, + equalizer_configuration: EqualizerConfiguration, + ) -> crate::Result { + let left_channel = &equalizer_configuration; + let right_channel = &equalizer_configuration; + let band_9_and_10 = self + .band_9_and_10_left_and_right + .load(atomic::Ordering::Relaxed) + .to_ne_bytes(); + + let packet_bytes = CustomSetEqualizerPacket { + left_channel, + right_channel, + left_band_9_and_10: [band_9_and_10[0], band_9_and_10[1]], + right_band_9_and_10: [band_9_and_10[2], band_9_and_10[3]], + } + .bytes(); + + Ok(CommandResponse { + packets: vec![packet_bytes], + new_state: DeviceState { + equalizer_configuration, + ..state + }, + }) + } +} + +struct CustomSetEqualizerPacket<'a> { + pub left_channel: &'a EqualizerConfiguration, + pub right_channel: &'a EqualizerConfiguration, + pub left_band_9_and_10: [u8; 2], + pub right_band_9_and_10: [u8; 2], +} + +impl<'a> OutboundPacket for CustomSetEqualizerPacket<'a> { + fn command(&self) -> [u8; 7] { + SetEqualizerPacket::COMMAND + } + + fn body(&self) -> Vec { + self.left_channel + .profile_id() + .to_le_bytes() + .into_iter() + .chain(self.left_channel.volume_adjustments().bytes()) + .chain(self.left_band_9_and_10) + .chain(self.right_channel.volume_adjustments().bytes()) + .chain(self.right_band_9_and_10) + .collect::>() + } +} + +#[cfg(test)] +mod tests { + use nom::error::VerboseError; + + use crate::devices::{ + a3933::{ + device_profile::{CustomSetEqualizerPacket, A3933_DEVICE_PROFILE}, + packets::inbound::take_a3933_state_update_packet, + }, + standard::{ + packets::{ + inbound::{state_update_packet::StateUpdatePacket, take_inbound_packet_body}, + outbound::{OutboundPacket, OutboundPacketBytes}, + }, + state::DeviceState, + structures::{EqualizerConfiguration, PresetEqualizerProfile, STATE_UPDATE}, + }, + }; + + struct A3933TestStateUpdatePacket { + body: Vec, + } + impl OutboundPacket for A3933TestStateUpdatePacket { + fn command(&self) -> [u8; 7] { + STATE_UPDATE + } + + fn body(&self) -> Vec { + self.body.to_owned() + } + } + + #[test] + fn it_remembers_eq_band_9_and_10_values() { + let data = A3933TestStateUpdatePacket { + body: vec![ + 0x00, // host device + 0x00, // tws status + 0x00, 0x00, 0x00, 0x00, // dual battery + b'0', b'0', b'.', b'0', b'0', // left firmware version + b'0', b'0', b'.', b'0', b'0', // right firmware version + b'0', b'0', b'0', b'0', b'0', b'0', b'0', b'0', b'0', b'0', b'0', b'0', b'0', b'0', + b'0', b'0', // serial number + 0x00, 0x00, // eq profile id + 120, 120, 120, 120, 120, 120, 120, 120, 121, 122, // left eq + 120, 120, 120, 120, 120, 120, 120, 120, 123, 124, // right eq + 0x00, // age range + 0x01, // hear id enabled + 120, 120, 120, 120, 120, 120, 120, 120, 125, 126, // left hear id + 120, 120, 120, 120, 120, 120, 120, 120, 127, 0, // right hear id + 0x00, 0x00, 0x00, 0x00, // hear id time + 0x00, // hear id type + 120, 120, 120, 120, 120, 120, 120, 120, 1, 2, // left hear id custom + 120, 120, 120, 120, 120, 120, 120, 120, 3, 4, // right hear id custom + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, // custom button model + 0x07, // ambient sound mode cycle + 0x00, // ambient sound mode + 0x00, // noise canceling mode + 0x00, // transparency mode + 0x00, // custom noise canceling + 0x00, // touch tone + 0x00, // wear detection + 0x00, // gaming mode + 0x00, // case battery + 0x00, // ? + 0x00, // device color + 0x00, // wind noise detection + ], + } + .bytes(); + + let body = take_inbound_packet_body(&data).unwrap().1; + let state_update = take_a3933_state_update_packet::>(body) + .unwrap() + .1; + let state: DeviceState = StateUpdatePacket::from(state_update).into(); + let dispatchers = A3933_DEVICE_PROFILE.custom_dispatchers.unwrap()(); + let state = (&dispatchers.packet_handlers()[&STATE_UPDATE])(&body, state); + + let equalizer_configuration = + EqualizerConfiguration::new_from_preset_profile(PresetEqualizerProfile::TrebleReducer); + let command_response = dispatchers + .set_equalizer_configuration(state, equalizer_configuration.to_owned()) + .unwrap(); + + assert_eq!(1, command_response.packets.len()); + assert_eq!( + &CustomSetEqualizerPacket { + left_channel: &equalizer_configuration, + right_channel: &equalizer_configuration, + left_band_9_and_10: [121, 122], + right_band_9_and_10: [123, 124], + } + .bytes(), + command_response.packets.first().unwrap(), + ); + } +} diff --git a/lib/src/devices/a3933/packets/inbound/state_update_packet.rs b/lib/src/devices/a3933/packets/inbound/state_update_packet.rs index b56c58ce..dc3ff06f 100644 --- a/lib/src/devices/a3933/packets/inbound/state_update_packet.rs +++ b/lib/src/devices/a3933/packets/inbound/state_update_packet.rs @@ -15,12 +15,12 @@ use crate::devices::{ take_ambient_sound_mode_cycle, take_battery_level, take_bool, take_custom_button_model, take_custom_hear_id_without_music_type, take_dual_battery, take_equalizer_configuration, take_firmware_version, - take_serial_number, take_sound_modes, ParseResult, + take_serial_number, take_sound_modes, take_volume_adjustments, ParseResult, }, }, structures::{ AmbientSoundModeCycle, BatteryLevel, CustomButtonModel, CustomHearId, DualBattery, - EqualizerConfiguration, FirmwareVersion, SerialNumber, SoundModes, + EqualizerConfiguration, FirmwareVersion, HearId, SerialNumber, SoundModes, }, }, }; @@ -29,24 +29,27 @@ use crate::devices::{ // Despite EQ being 10 bands, only the first 8 seem to be used? #[derive(Debug, Clone, PartialEq)] pub struct A3933StateUpdatePacket { - host_device: u8, - tws_status: bool, - battery: DualBattery, - left_firmware: FirmwareVersion, - right_firmware: FirmwareVersion, - serial_number: SerialNumber, - equalizer_configuration: EqualizerConfiguration, // 10 bands mono - age_range: u8, - hear_id: CustomHearId, // 10 bands - custom_button_model: CustomButtonModel, - ambient_sound_mode_cycle: AmbientSoundModeCycle, - sound_modes: SoundModes, - touch_tone_switch: bool, - wear_detection_switch: bool, - game_mode_switch: bool, - charging_case_battery_level: BatteryLevel, - device_color: u8, - wind_noise_detection: bool, + pub host_device: u8, + pub tws_status: bool, + pub battery: DualBattery, + pub left_firmware: FirmwareVersion, + pub right_firmware: FirmwareVersion, + pub serial_number: SerialNumber, + pub left_equalizer_configuration: EqualizerConfiguration, + pub left_band_9_and_10: [u8; 2], + pub right_equalizer_configuration: EqualizerConfiguration, + pub right_band_9_and_10: [u8; 2], + pub age_range: u8, + pub hear_id: CustomHearId, // 10 bands + pub custom_button_model: CustomButtonModel, + pub ambient_sound_mode_cycle: AmbientSoundModeCycle, + pub sound_modes: SoundModes, + pub touch_tone_switch: bool, + pub wear_detection_switch: bool, + pub game_mode_switch: bool, + pub charging_case_battery_level: BatteryLevel, + pub device_color: u8, + pub wind_noise_detection: bool, } impl From for StateUpdatePacket { @@ -54,11 +57,11 @@ impl From for StateUpdatePacket { Self { device_profile: A3933_DEVICE_PROFILE, battery: packet.battery.into(), - equalizer_configuration: packet.equalizer_configuration, + equalizer_configuration: packet.left_equalizer_configuration, sound_modes: Some(packet.sound_modes), age_range: None, gender: None, - hear_id: None, + hear_id: Some(HearId::Custom(packet.hear_id)), custom_button_model: Some(packet.custom_button_model), firmware_version: Some(packet.left_firmware.min(packet.right_firmware)), serial_number: Some(packet.serial_number), @@ -80,8 +83,10 @@ pub fn take_a3933_state_update_packet<'a, E: ParseError<&'a [u8]> + ContextError take_firmware_version, take_firmware_version, take_serial_number, - take_equalizer_configuration(10), - take(10usize), // ??, maybe stereo eq? + take_equalizer_configuration(8), + take(2usize), + take_volume_adjustments(8), + take(2usize), le_u8, take_custom_hear_id_without_music_type(10), take_custom_button_model, @@ -96,8 +101,10 @@ pub fn take_a3933_state_update_packet<'a, E: ParseError<&'a [u8]> + ContextError left_firmware, right_firmware, serial_number, - equalizer_configuration, // 10 bands mono - _, + left_equalizer_configuration, + left_band_9_and_10, + right_volume_adjustments, + right_band_9_and_10, age_range, hear_id, custom_button_model, @@ -112,7 +119,17 @@ pub fn take_a3933_state_update_packet<'a, E: ParseError<&'a [u8]> + ContextError left_firmware, right_firmware, serial_number, - equalizer_configuration, + right_equalizer_configuration: if left_equalizer_configuration + .preset_profile() + .is_some() + { + left_equalizer_configuration.to_owned() + } else { + EqualizerConfiguration::new_custom_profile(right_volume_adjustments) + }, + right_band_9_and_10: right_band_9_and_10.try_into().unwrap(), + left_equalizer_configuration, + left_band_9_and_10: left_band_9_and_10.try_into().unwrap(), age_range, hear_id, custom_button_model,