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());
+    }
+}