| // Copyright 2024 Google LLC |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use bt_common::packet_encoding::Encodable; |
| use bt_common::Uuid; |
| use bt_gatt::{ |
| client::{FromCharacteristic, PeerService, PeerServiceHandle}, |
| types::WriteMode, |
| Characteristic, Client, |
| }; |
| |
| use futures::TryFutureExt; |
| use parking_lot::Mutex; |
| use std::sync::Arc; |
| use thiserror::Error; |
| |
| pub const VCS_UUID: Uuid = Uuid::from_u16(0x1844); |
| |
| pub mod debug; |
| |
| /// Volume State Characteristic |
| /// See VCS v1.0 Section 2.3.1 |
| #[derive(Debug, Clone)] |
| pub struct VolumeState { |
| handle: bt_gatt::types::Handle, |
| /// Unitless value, step sizes are implementation-specific |
| setting: u8, |
| /// True if the audio is muted. Does not affect |
| /// [`setting`](VolumeState::setting). |
| mute: bool, |
| /// Server-incremented counter, used to invalidate commands against stale |
| /// state. Wraps around from 255 to 0. |
| change_counter: u8, |
| } |
| |
| impl VolumeState { |
| fn from_value( |
| handle: bt_gatt::types::Handle, |
| value: &[u8], |
| ) -> core::result::Result<Self, bt_common::packet_encoding::Error> { |
| let mut val = Self { handle, setting: 0, mute: false, change_counter: 0 }; |
| val.update_from_value(value)?; |
| Ok(val) |
| } |
| |
| fn update_from_value( |
| &mut self, |
| value: &[u8], |
| ) -> core::result::Result<(), bt_common::packet_encoding::Error> { |
| if value.len() < 3 { |
| return Err(bt_common::packet_encoding::Error::UnexpectedDataLength); |
| } |
| self.setting = value[0]; |
| self.mute = match value[1] { |
| 0 => false, |
| 1 => true, |
| _ => return Err(bt_common::packet_encoding::Error::OutOfRange), |
| }; |
| self.change_counter = value[2]; |
| Ok(()) |
| } |
| } |
| |
| impl FromCharacteristic for VolumeState { |
| const UUID: Uuid = Uuid::from_u16(0x2B7D); |
| |
| fn from_chr( |
| characteristic: Characteristic, |
| value: &[u8], |
| ) -> core::result::Result<Self, bt_common::packet_encoding::Error> { |
| Self::from_value(characteristic.handle, value) |
| } |
| |
| fn update( |
| &mut self, |
| new_value: &[u8], |
| ) -> core::result::Result<&mut Self, bt_common::packet_encoding::Error> { |
| self.update_from_value(new_value)?; |
| Ok(self) |
| } |
| } |
| |
| /// Volume Control Point |
| /// See VCS v1.0 Section 3.2 |
| #[derive(Debug, Clone)] |
| pub struct VolumeControlPoint { |
| handle: bt_gatt::types::Handle, |
| } |
| |
| impl VolumeControlPoint { |
| const UUID: Uuid = Uuid::from_u16(0x2B7E); |
| } |
| |
| #[derive(Debug, Clone, PartialEq)] |
| pub enum VcpProcedure { |
| RelativeVolumeDown, |
| RelativeVolumeUp, |
| UnmuteRelativeVolumeDown, |
| UnmuteRelativeVolumeUp, |
| SetAbsoluteVolume { setting: u8 }, |
| Unmute, |
| Mute, |
| } |
| |
| impl VcpProcedure { |
| fn opcode(&self) -> u8 { |
| match self { |
| VcpProcedure::RelativeVolumeDown => 0x00, |
| VcpProcedure::RelativeVolumeUp => 0x01, |
| VcpProcedure::UnmuteRelativeVolumeDown => 0x02, |
| VcpProcedure::UnmuteRelativeVolumeUp => 0x03, |
| VcpProcedure::SetAbsoluteVolume { .. } => 0x04, |
| VcpProcedure::Unmute => 0x05, |
| VcpProcedure::Mute => 0x06, |
| } |
| } |
| } |
| |
| pub struct VolumeControlPointOperation { |
| procedure: VcpProcedure, |
| change_counter: u8, |
| } |
| |
| impl Encodable for VolumeControlPointOperation { |
| type Error = bt_common::packet_encoding::Error; |
| |
| fn encoded_len(&self) -> core::primitive::usize { |
| if let VcpProcedure::SetAbsoluteVolume { .. } = self.procedure { |
| return 3; |
| } |
| // All the other operations only have a change counter parameter |
| 2 |
| } |
| |
| fn encode(&self, buf: &mut [u8]) -> core::result::Result<(), Self::Error> { |
| if buf.len() < self.encoded_len() { |
| return Err(bt_common::packet_encoding::Error::BufferTooSmall); |
| } |
| |
| buf[0] = self.procedure.opcode(); |
| buf[1] = self.change_counter; |
| if let VcpProcedure::SetAbsoluteVolume { setting } = self.procedure { |
| buf[2] = setting; |
| } |
| Ok(()) |
| } |
| } |
| |
| #[derive(Debug, Clone, Copy, PartialEq)] |
| pub enum VolumeSettingPersisted { |
| Reset, |
| UserSet, |
| } |
| |
| impl TryFrom<&[u8]> for VolumeSettingPersisted { |
| type Error = bt_common::packet_encoding::Error; |
| |
| fn try_from(value: &[u8]) -> std::result::Result<Self, Self::Error> { |
| if value.len() < 1 { |
| return Err(bt_common::packet_encoding::Error::UnexpectedDataLength); |
| } |
| if (value[0] & 0x01) != 0 { |
| Ok(VolumeSettingPersisted::UserSet) |
| } else { |
| Ok(VolumeSettingPersisted::Reset) |
| } |
| } |
| } |
| |
| /// Volume Flags characteristic |
| /// See VCS v1.0 Section 3.3 |
| #[derive(Debug, Clone)] |
| pub struct VolumeFlags { |
| handle: bt_gatt::types::Handle, |
| persisted: VolumeSettingPersisted, |
| } |
| |
| impl FromCharacteristic for VolumeFlags { |
| const UUID: Uuid = Uuid::from_u16(0x2B7F); |
| |
| fn from_chr( |
| characteristic: Characteristic, |
| value: &[u8], |
| ) -> core::result::Result<Self, bt_common::packet_encoding::Error> { |
| let persisted = VolumeSettingPersisted::try_from(value)?; |
| Ok(Self { handle: characteristic.handle, persisted }) |
| } |
| |
| fn update( |
| &mut self, |
| new_value: &[u8], |
| ) -> core::result::Result<&mut Self, bt_common::packet_encoding::Error> { |
| self.persisted = VolumeSettingPersisted::try_from(new_value)?; |
| Ok(self) |
| } |
| } |
| |
| pub struct VolumeControlClient<T: bt_gatt::GattTypes> { |
| service: T::PeerService, |
| state: Arc<Mutex<VolumeState>>, |
| control_point: VolumeControlPoint, |
| flags: Arc<Mutex<VolumeFlags>>, |
| } |
| |
| impl<T: bt_gatt::GattTypes> std::fmt::Display for VolumeControlClient<T> { |
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| let VolumeState { change_counter, setting, mute, .. } = self.state.lock().clone(); |
| let VolumeFlags { persisted, .. } = self.flags.lock().clone(); |
| let mute_str = mute.then_some("MUTED ").unwrap_or(""); |
| write!( |
| f, |
| "Volume Control @ {change_counter}: Current Volume: {setting} {mute_str}from {persisted:?}" |
| ) |
| } |
| } |
| |
| #[derive(Error, Debug)] |
| pub enum Error { |
| #[error("Issue encoding / decoding: {0}")] |
| Decoding(#[from] bt_common::packet_encoding::Error), |
| #[error("GATT error: {0}")] |
| Gatt(bt_gatt::types::Error), |
| #[error("Required Characteristic was not found: {0}")] |
| RequiredCharNotFound(&'static str), |
| #[error("Change Counter Mismatch")] |
| ChangeCounterMismatch, |
| #[error("Opcode Not Supported")] |
| OpcodeNotSupported, |
| } |
| |
| impl From<bt_gatt::types::Error> for Error { |
| fn from(value: bt_gatt::types::Error) -> Self { |
| use bt_gatt::types::GattError::*; |
| match value { |
| bt_gatt::types::Error::Gatt(ApplicationError80) => Self::ChangeCounterMismatch, |
| bt_gatt::types::Error::Gatt(ApplicationError81) => Self::OpcodeNotSupported, |
| other => Self::Gatt(other), |
| } |
| } |
| } |
| |
| // TODO(b/441327871): Add notification method for volume state changes. |
| impl<T: bt_gatt::GattTypes> VolumeControlClient<T> { |
| pub async fn from_service(service: T::PeerService) -> Result<Self, Error> { |
| let mut state = None; |
| let mut control_point = None; |
| let mut flags = None; |
| for chr in service.discover_characteristics(None).await? { |
| if chr.uuid == VolumeState::UUID { |
| let _ = state.insert(VolumeState::try_read::<T>(chr, &service).await?); |
| } else if chr.uuid == VolumeControlPoint::UUID { |
| let _ = control_point.insert(VolumeControlPoint { handle: chr.handle }); |
| } else if chr.uuid == VolumeFlags::UUID { |
| let _ = flags.insert(VolumeFlags::try_read::<T>(chr, &service).await?); |
| } |
| } |
| |
| if state.is_none() { |
| return Err(Error::RequiredCharNotFound("Volume State")); |
| } |
| if control_point.is_none() { |
| return Err(Error::RequiredCharNotFound("Volume Conrol Point")); |
| } |
| if flags.is_none() { |
| return Err(Error::RequiredCharNotFound("Volume Flags")); |
| } |
| |
| Ok(Self { |
| service, |
| state: Arc::new(Mutex::new(state.unwrap())), |
| control_point: control_point.unwrap(), |
| flags: Arc::new(Mutex::new(flags.unwrap())), |
| }) |
| } |
| |
| pub async fn connect(client: &T::Client) -> Result<Option<Self>, Error> { |
| let handles = client.find_service(VCS_UUID).await?; |
| let Some(handle) = handles.into_iter().next() else { |
| return Ok(None); |
| }; |
| Ok(Some(handle.connect().map_err(Into::into).and_then(Self::from_service).await?)) |
| } |
| |
| fn change_counter(&self) -> u8 { |
| self.state.lock().change_counter |
| } |
| |
| // Update the state of the volume by reading the characteristics. |
| // Returns the new volume level if successful, and an Error otherwise. |
| pub async fn update(&self) -> Result<u8, Error> { |
| let state_handle = self.state.lock().handle; |
| |
| let mut state_buf = [0; 3]; |
| let _ = self.service.read_characteristic(&state_handle, 0, &mut state_buf).await?; |
| let setting = self.state.lock().update(&state_buf)?.setting; |
| |
| let flags_handle = self.flags.lock().handle; |
| let mut flags_buf = [0; 1]; |
| let _ = self.service.read_characteristic(&flags_handle, 0, &mut flags_buf).await?; |
| let _ = self.flags.lock().update(&flags_buf)?; |
| |
| Ok(setting) |
| } |
| |
| async fn send_control_pt(&self, procedure: VcpProcedure) -> Result<(), Error> { |
| let change_counter = self.change_counter(); |
| let op = VolumeControlPointOperation { procedure, change_counter }; |
| let mut buf = [0; 2]; |
| op.encode(&mut buf)?; |
| self.service |
| .write_characteristic(&self.control_point.handle, WriteMode::None, 0, &buf) |
| .await |
| .map_err(Into::into) |
| } |
| |
| /// Relative volume up. Should increase the volume by a static step size |
| /// unless the volume is at max. |
| /// If `unmute` is true, also unmute, otherwise it does not affect the mute |
| /// value. |
| pub async fn volume_up(&self, unmute: bool) -> Result<(), Error> { |
| let procedure = if unmute { |
| VcpProcedure::UnmuteRelativeVolumeUp |
| } else { |
| VcpProcedure::RelativeVolumeUp |
| }; |
| self.send_control_pt(procedure).await |
| } |
| |
| /// Relative volume down. Should decrease the volume by a static step size |
| /// unless the volume is at zero. |
| /// If `unmute` is true, also unmute, otherwise it does not affect the mute |
| /// value. |
| pub async fn volume_down(&self, unmute: bool) -> Result<(), Error> { |
| let procedure = if unmute { |
| VcpProcedure::UnmuteRelativeVolumeDown |
| } else { |
| VcpProcedure::RelativeVolumeDown |
| }; |
| self.send_control_pt(procedure).await |
| } |
| |
| pub async fn mute(&self) -> Result<(), Error> { |
| self.send_control_pt(VcpProcedure::Mute).await |
| } |
| |
| pub async fn unmute(&self) -> Result<(), Error> { |
| self.send_control_pt(VcpProcedure::Unmute).await |
| } |
| |
| pub async fn set_absolute_volume(&self, setting: u8) -> Result<(), Error> { |
| let change_counter = self.change_counter(); |
| let op = VolumeControlPointOperation { |
| procedure: VcpProcedure::SetAbsoluteVolume { setting }, |
| change_counter, |
| }; |
| let mut buf = [0; 3]; |
| op.encode(&mut buf)?; |
| self.service |
| .write_characteristic(&self.control_point.handle, WriteMode::None, 0, &buf) |
| .await |
| .map_err(Into::into) |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| |
| use bt_gatt::test_utils::*; |
| use futures::Future; |
| |
| #[test] |
| fn volume_state_decode() { |
| let handle = bt_gatt::types::Handle(1); |
| // not long enogugh |
| assert!(VolumeState::from_value(handle, &[]).is_err()); |
| // too long is fine, but mute value is wrong. |
| assert!(VolumeState::from_value(handle, &[1, 2, 3, 4]).is_err()); |
| // muted |
| let state = VolumeState::from_value(handle, &[1, 1, 3, 4]).expect("okay"); |
| assert_eq!(state.mute, true); |
| assert_eq!(state.setting, 1); |
| assert_eq!(state.change_counter, 3); |
| // not muted |
| let state = VolumeState::from_value(handle, &[3, 0, 1]).expect("okay"); |
| assert_eq!(state.mute, false); |
| assert_eq!(state.setting, 3); |
| assert_eq!(state.change_counter, 1); |
| } |
| |
| #[test] |
| fn volume_flags_decode() { |
| use bt_gatt::types::*; |
| let chr = Characteristic { |
| handle: Handle(1), |
| uuid: VolumeFlags::UUID, |
| properties: CharacteristicProperty::Read.into(), |
| permissions: AttributePermissions { |
| read: Some(SecurityLevels::default()), |
| ..Default::default() |
| }, |
| descriptors: Vec::new(), |
| }; |
| // not long enogugh |
| assert!(VolumeFlags::from_chr(chr.clone(), &[]).is_err()); |
| // persisted |
| let flags = VolumeFlags::from_chr(chr.clone(), &[1]).expect("okay"); |
| assert_eq!(flags.persisted, VolumeSettingPersisted::UserSet); |
| // not persisted (other bits ignored) |
| let flags = VolumeFlags::from_chr(chr, &[2]).expect("okay"); |
| assert_eq!(flags.persisted, VolumeSettingPersisted::Reset); |
| } |
| |
| #[track_caller] |
| fn is_ready<T>(fut: impl Future<Output = T>) -> T { |
| use futures::FutureExt; |
| let mut noop_cx = futures::task::Context::from_waker(futures::task::noop_waker_ref()); |
| let mut fut_pinned = std::pin::pin!(fut); |
| let futures::task::Poll::Ready(x) = fut_pinned.poll_unpin(&mut noop_cx) else { |
| panic!("Future is not ready"); |
| }; |
| x |
| } |
| |
| fn try_from_service( |
| service: FakePeerService, |
| ) -> core::result::Result<VolumeControlClient<FakeTypes>, crate::Error> { |
| is_ready(VolumeControlClient::<FakeTypes>::from_service(service)) |
| } |
| |
| const STATE_HANDLE: bt_gatt::types::Handle = bt_gatt::types::Handle(1); |
| const FLAGS_HANDLE: bt_gatt::types::Handle = bt_gatt::types::Handle(2); |
| const CP_HANDLE: bt_gatt::types::Handle = bt_gatt::types::Handle(3); |
| |
| fn state_chr() -> bt_gatt::types::Characteristic { |
| use bt_gatt::types::*; |
| Characteristic { |
| handle: STATE_HANDLE, |
| uuid: VolumeState::UUID, |
| properties: CharacteristicProperty::Read | CharacteristicProperty::Notify, |
| permissions: AttributePermissions { |
| read: Some(SecurityLevels::default()), |
| update: Some(SecurityLevels::default()), |
| ..Default::default() |
| }, |
| descriptors: Vec::new(), |
| } |
| } |
| |
| fn build_fake_service() -> FakePeerService { |
| use bt_gatt::types::*; |
| |
| let mut service = FakePeerService::new(); |
| |
| // No services, error (check between adding each one, should be an error until |
| // all chars are added. |
| assert!(try_from_service(service.clone()).is_err()); |
| service.add_characteristic(state_chr(), vec![1, 0, 1]); |
| assert!(try_from_service(service.clone()).is_err()); |
| service.add_characteristic( |
| Characteristic { |
| handle: FLAGS_HANDLE, |
| uuid: VolumeFlags::UUID, |
| properties: CharacteristicProperty::Read.into(), |
| permissions: AttributePermissions { |
| read: Some(SecurityLevels::default()), |
| ..Default::default() |
| }, |
| descriptors: Vec::new(), |
| }, |
| vec![1], |
| ); |
| assert!(try_from_service(service.clone()).is_err()); |
| service.add_characteristic( |
| Characteristic { |
| handle: CP_HANDLE, |
| uuid: VolumeControlPoint::UUID, |
| properties: CharacteristicProperty::Write.into(), |
| permissions: AttributePermissions { |
| write: Some(SecurityLevels::default()), |
| ..Default::default() |
| }, |
| descriptors: Vec::new(), |
| }, |
| vec![], |
| ); |
| |
| service |
| } |
| |
| #[test] |
| fn build_from_service() { |
| use futures::{task::Poll, FutureExt}; |
| |
| let mut service = build_fake_service(); |
| let client = try_from_service(service.clone()).unwrap(); |
| assert_eq!(client.change_counter(), 1); |
| |
| service.add_characteristic(state_chr(), vec![100, 1, 3]); |
| let mut update_fut = Box::pin(client.update()); |
| let mut noop_cx = futures::task::Context::from_waker(futures::task::noop_waker_ref()); |
| match update_fut.poll_unpin(&mut noop_cx) { |
| Poll::Ready(Ok(volume)) => assert_eq!(volume, 100), |
| x => panic!("Didn't update right: {x:?}"), |
| } |
| } |
| |
| #[test] |
| fn connect() { |
| let mut service = build_fake_service(); |
| let mut client = FakeClient::new(); |
| |
| // Successfully didn't find the service |
| assert!(is_ready(VolumeControlClient::<FakeTypes>::connect(&client)).unwrap().is_none()); |
| |
| client.add_service(VCS_UUID, true, service.clone()); |
| |
| let client = is_ready(VolumeControlClient::<FakeTypes>::connect(&client)).unwrap().unwrap(); |
| assert_eq!(client.change_counter(), 1); |
| service.add_characteristic(state_chr(), vec![250, 1, 3]); |
| assert_eq!(250, is_ready(client.update()).unwrap()); |
| } |
| |
| #[test] |
| fn volume() { |
| let mut service = build_fake_service(); |
| let client = try_from_service(service.clone()).unwrap(); |
| assert_eq!(client.change_counter(), 1); |
| |
| service.expect_characteristic_value(&CP_HANDLE, vec![0x01, client.change_counter()]); |
| assert!(is_ready(client.volume_up(false)).is_ok()); |
| |
| service.expect_characteristic_value(&CP_HANDLE, vec![0x03, client.change_counter()]); |
| assert!(is_ready(client.volume_up(true)).is_ok()); |
| |
| service.expect_characteristic_value(&CP_HANDLE, vec![0x02, client.change_counter()]); |
| assert!(is_ready(client.volume_down(true)).is_ok()); |
| |
| service.add_characteristic(state_chr(), vec![250, 1, 3]); |
| assert_eq!(250, is_ready(client.update()).unwrap()); |
| |
| service.expect_characteristic_value(&CP_HANDLE, vec![0x00, 3]); |
| assert!(is_ready(client.volume_down(false)).is_ok()); |
| } |
| |
| #[test] |
| fn mutes() { |
| let mut service = build_fake_service(); |
| let client = try_from_service(service.clone()).unwrap(); |
| assert_eq!(client.change_counter(), 1); |
| |
| service.expect_characteristic_value(&CP_HANDLE, vec![0x06, 1]); |
| assert!(is_ready(client.mute()).is_ok()); |
| |
| service.expect_characteristic_value(&CP_HANDLE, vec![0x05, 1]); |
| assert!(is_ready(client.unmute()).is_ok()); |
| |
| service.expect_characteristic_value(&CP_HANDLE, vec![0x05, 1]); |
| assert!(is_ready(client.unmute()).is_ok()); |
| } |
| |
| #[test] |
| fn absolute_volume() { |
| let mut service = build_fake_service(); |
| let client = try_from_service(service.clone()).unwrap(); |
| assert_eq!(client.change_counter(), 1); |
| |
| service.expect_characteristic_value(&CP_HANDLE, vec![0x04, 1, 123]); |
| assert!(is_ready(client.set_absolute_volume(123)).is_ok()); |
| } |
| } |