rust/bt-vcs: Add bt-vcs crate
Add types and client for Volume Control Service.
Bug: b/314698517
Test: added tests, cargo test
Change-Id: I8518dbd5e79d0226d952130976c5d1bcb69485d2
Reviewed-on: https://bluetooth-review.googlesource.com/c/bluetooth/+/1481
Reviewed-by: Dayeong Lee <dayeonglee@google.com>
diff --git a/rust/.gitignore b/rust/.gitignore
index 438e2b1..01b162a 100644
--- a/rust/.gitignore
+++ b/rust/.gitignore
@@ -14,4 +14,5 @@
*.pdb
# Vim swap files.
-*.swp
+*.sw[op]
+
diff --git a/rust/bt-gatt/src/types.rs b/rust/bt-gatt/src/types.rs
index ad182b4..5644c31 100644
--- a/rust/bt-gatt/src/types.rs
+++ b/rust/bt-gatt/src/types.rs
@@ -8,7 +8,7 @@
/// Errors that can be returned from GATT procedures. These errors are sent from
/// the peer. These are defined to match the Bluetooth Core Spec (v5.4, Vol 3,
/// Part F, Sec 3.4.1.1)
-#[derive(Debug, Copy, Clone)]
+#[derive(Debug, Copy, Clone, PartialEq)]
pub enum GattError {
InvalidHandle = 1,
ReadNotPermitted = 2,
diff --git a/rust/bt-vcs/.gitignore b/rust/bt-vcs/.gitignore
new file mode 100644
index 0000000..438e2b1
--- /dev/null
+++ b/rust/bt-vcs/.gitignore
@@ -0,0 +1,17 @@
+# Generated by Cargo
+# will have compiled files and executables
+debug/
+target/
+
+# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
+# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
+Cargo.lock
+
+# These are backup files generated by rustfmt
+**/*.rs.bk
+
+# MSVC Windows builds of rustc generate these, which store debugging information
+*.pdb
+
+# Vim swap files.
+*.swp
diff --git a/rust/bt-vcs/Cargo.toml b/rust/bt-vcs/Cargo.toml
new file mode 100644
index 0000000..e54bef0
--- /dev/null
+++ b/rust/bt-vcs/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "bt-vcs"
+description = "Client and server library for the Volume Control Service"
+version = "0.0.1"
+edition.workspace = true
+license.workspace = true
+
+[dependencies]
+### In-tree dependencies
+bt-gatt.workspace = true
+bt-common.workspace = true
+
+### Others
+futures.workspace = true
+parking_lot.workspace = true
+thiserror.workspace = true
+
+[dev-dependencies]
+pretty_assertions.workspace = true
+bt-gatt = { workspace = true, features = ["test-utils"] }
diff --git a/rust/bt-vcs/LICENSE b/rust/bt-vcs/LICENSE
new file mode 120000
index 0000000..30cff74
--- /dev/null
+++ b/rust/bt-vcs/LICENSE
@@ -0,0 +1 @@
+../../LICENSE
\ No newline at end of file
diff --git a/rust/bt-vcs/src/debug.rs b/rust/bt-vcs/src/debug.rs
new file mode 100644
index 0000000..cf50edd
--- /dev/null
+++ b/rust/bt-vcs/src/debug.rs
@@ -0,0 +1,140 @@
+// Copyright 2024 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+use bt_common::debug_command::CommandRunner;
+use bt_common::debug_command::CommandSet;
+use bt_common::gen_commandset;
+
+use parking_lot::Mutex;
+
+use crate::*;
+
+gen_commandset! {
+ VcsCmd {
+ Connect = ("connect", [], [], "Connect to VCS"),
+ Info = ("info", [], [], "Info about the current status"),
+ Up = ("up", [], [], "Volume up"),
+ Down = ("down", [], [], "Volume down"),
+ Update = ("update", [], [], "Update"),
+ Mute = ("mute", [], [], "Mute"),
+ Unmute = ("unmute", [], [], "Unmute"),
+ Set = ("set", [], ["level"], "Set the level"),
+ }
+}
+
+pub struct VcsDebug<T: bt_gatt::GattTypes> {
+ peer_client: T::Client,
+ client: Mutex<Option<Arc<VolumeControlClient<T>>>>,
+}
+
+impl<T: bt_gatt::GattTypes> VcsDebug<T> {
+ pub fn new(client: T::Client) -> Self {
+ Self { peer_client: client, client: Mutex::new(None) }
+ }
+
+ async fn connect(&self) -> Option<Arc<VolumeControlClient<T>>> {
+ let client_result = VolumeControlClient::connect(&self.peer_client).await;
+ let Ok(client) = client_result else {
+ eprintln!("Could not connect to VCS: {:?}", client_result.err());
+ return None;
+ };
+ if client.is_none() {
+ eprintln!("Found no clients or had an error connecting.");
+ return None;
+ }
+ Some(Arc::new(client.unwrap()))
+ }
+
+ fn try_client(&self) -> Option<Arc<VolumeControlClient<T>>> {
+ let lock = self.client.lock();
+ let Some(vcs_client) = lock.as_ref() else {
+ eprintln!("Client not connected, connect first");
+ return None;
+ };
+ Some(vcs_client.clone())
+ }
+}
+
+impl<T: bt_gatt::GattTypes> CommandRunner for VcsDebug<T> {
+ type Set = VcsCmd;
+
+ fn run(
+ &self,
+ cmd: Self::Set,
+ args: Vec<String>,
+ ) -> impl futures::Future<Output = Result<(), impl std::error::Error>> {
+ async move {
+ match cmd {
+ // TODO(fxbug.dev/438282674): Add a way to register for vol state changes.
+ VcsCmd::Connect => {
+ let lock = self.client.lock();
+ if lock.is_some() {
+ eprintln!("Already connected to VCS");
+ return Ok(());
+ }
+ drop(lock);
+ let Some(vcs_client) = self.connect().await else {
+ eprintln!("Could not connect to VCS");
+ return Ok(());
+ };
+ *self.client.lock() = Some(vcs_client);
+ println!("Connected to VCS");
+ }
+ VcsCmd::Info => {
+ let Some(client) = self.try_client() else {
+ return Ok(());
+ };
+ println!("{}", client);
+ }
+ VcsCmd::Up => {
+ let Some(client) = self.try_client() else {
+ return Ok(());
+ };
+ client.volume_up(false).await?;
+ }
+ VcsCmd::Down => {
+ let Some(client) = self.try_client() else {
+ return Ok(());
+ };
+ client.volume_down(false).await?;
+ }
+ VcsCmd::Update => {
+ let Some(client) = self.try_client() else {
+ return Ok(());
+ };
+ client.update().await?;
+ println!("{}", client);
+ }
+ VcsCmd::Mute => {
+ let Some(client) = self.try_client() else {
+ return Ok(());
+ };
+ client.mute().await?;
+ }
+ VcsCmd::Unmute => {
+ let Some(client) = self.try_client() else {
+ return Ok(());
+ };
+ client.unmute().await?;
+ }
+ VcsCmd::Set => {
+ let Some(client) = self.try_client() else {
+ return Ok(());
+ };
+ if args.len() != 1 {
+ eprintln!("Expecting one arg: level to set (0-255)");
+ return Ok(());
+ }
+ let Ok(level) = args[0].parse::<u8>() else {
+ eprintln!("Couldn't parse level");
+ return Ok(());
+ };
+ client.set_absolute_volume(level).await?;
+ }
+ }
+
+ Ok::<(), Error>(())
+ }
+ }
+}
diff --git a/rust/bt-vcs/src/lib.rs b/rust/bt-vcs/src/lib.rs
new file mode 100644
index 0000000..6758dec
--- /dev/null
+++ b/rust/bt-vcs/src/lib.rs
@@ -0,0 +1,570 @@
+// 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::Uuid;
+use bt_common::packet_encoding::Encodable;
+use bt_gatt::{
+ Characteristic, Client,
+ client::{FromCharacteristic, PeerService, PeerServiceHandle},
+ types::WriteMode,
+};
+
+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::{FutureExt, task::Poll};
+
+ 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());
+ }
+}