[rust][bt-gatt] Add initial library bt-gatt is a library abstraction for implementing clients and servers for GATT centrals and peripherals. Change-Id: If1fc2f96f36ebce576b9d11d746cda12037ed88c Reviewed-on: https://bluetooth-review.git.corp.google.com/c/bluetooth/+/1160 Reviewed-by: Dayeong Lee <dayeonglee@google.com>
diff --git a/rust/bt-gatt/.gitignore b/rust/bt-gatt/.gitignore new file mode 100644 index 0000000..438e2b1 --- /dev/null +++ b/rust/bt-gatt/.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-gatt/Cargo.toml b/rust/bt-gatt/Cargo.toml new file mode 100644 index 0000000..b106aa4 --- /dev/null +++ b/rust/bt-gatt/Cargo.toml
@@ -0,0 +1,12 @@ +[package] +name = "bt-gatt" +version = "0.0.1" +edition = "2021" +license = "BSD-2-Clause" + +[dependencies] +thiserror = "1.0.23" +futures = "0.3.19" + +### in-tree dependencies +bt-common = { path = "../bt-common" }
diff --git a/rust/bt-gatt/LICENSE b/rust/bt-gatt/LICENSE new file mode 100644 index 0000000..e5de3c8 --- /dev/null +++ b/rust/bt-gatt/LICENSE
@@ -0,0 +1,24 @@ +Copyright 2023 Google LLC + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/rust/bt-gatt/PATENTS b/rust/bt-gatt/PATENTS new file mode 100644 index 0000000..3a21f11 --- /dev/null +++ b/rust/bt-gatt/PATENTS
@@ -0,0 +1,22 @@ +Additional IP Rights Grant (Patents) + +"This implementation" means the copyrightable works distributed by +Google as part of this crate + +Google hereby grants to you a perpetual, worldwide, non-exclusive, +no-charge, royalty-free, irrevocable (except as stated in this +section) patent license to make, have made, use, offer to sell, sell, +import, transfer, and otherwise run, modify and propagate the contents +of this implementation, where such license applies only to +those patent claims, both currently owned by Google and acquired in +the future, licensable by Google that are necessarily infringed by +this implementation. This grant does not include claims that would be +infringed only as a consequence of further modification of this +implementation. If you or your agent or exclusive licensee institute +or order or agree to the institution of patent litigation or any other +patent enforcement activity against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that this +implementation constitutes direct or contributory patent +infringement, or inducement of patent infringement, then any patent +rights granted to you under this License for this implementation +shall terminate as of the date such litigation is filed.
diff --git a/rust/bt-gatt/README.md b/rust/bt-gatt/README.md new file mode 100644 index 0000000..ecddff7 --- /dev/null +++ b/rust/bt-gatt/README.md
@@ -0,0 +1,55 @@ +# bt-gatt + +An abstraction layer enabling Bluetooth Low Energy Central and Peripheral +crates to be built in a Bluetooth host stack agnostic way. + +Crates that wish to interact with GATT Services or implement a Service should +use this crate, and accept or generalize on the `bt_gatt::Central` or +`bt_gatt::Peripheral` trait implementations, which will be provided by a +Bluetooth stack support Crate. + +An example server and client for the Battery Service is provided. + +Implementations for the following Bluetooth stacks exist in other crates, i.e.: + - bt-gatt-sapphire + - bt-gatt-fluoride + +## Examples + +### Connecting from a central to a service + +```rust +async fn print_volume_changes(central: impl bt_gatt::central::Central) -> Result<(), Error> { + let peers_with_vcs = central.scan(VolumeControlService::UUID.into()); + // Presumably we would match more than just the first peer at some point. + let Some(first_match) = matches.next().await else { + panic!("Stack shutdown before we found a peer"); + }; + let client = central.connect(first_match.id).await?; + + let handle = client.find_service(VolumeControlService::UUID.into()).await?; + let service = handle.connect().await?; + + let current_char = service.discover_characteristics(Some(0x2b7d_u16.into())).await?.pop().unwrap(); + let mut current_state: [u8; 3] = [0; 3]; + let _ = service.read_characteristic(current_char.handle, 0, &mut current_state[..]).await?; + + let updates = service.subscribe(current_char.handle); + + while let Some(Ok(notif)) = updates.next().await? { + let new_state = notif.value; + let muted = new_state[1] == 1; + let muted_str = if muted { " muted" } else { "" }; + print!("New volume: {}{}", new_state[0], muted_str); + } +} +``` + +# Stack Connector Crates + +Bluetooth stacks that wish to make use of the crates that abstract over bt-gatt to provide services or clients should provide a method to +use the core traits: + - [`central::Central`] enables scanning and connecting to peers, providing connections to [`client::Client`] + - [`server::Server`] enables publishing Services and accepting connections from peers + - [`peripheral::Peripheral`] enables advertising +
diff --git a/rust/bt-gatt/src/central/mod.rs b/rust/bt-gatt/src/central/mod.rs new file mode 100644 index 0000000..5197556 --- /dev/null +++ b/rust/bt-gatt/src/central/mod.rs
@@ -0,0 +1,93 @@ +// Copyright 2023 Google LLC +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +//! Contains traits that are used to find and connect to Low Energy Peers, i.e. the GAP Central +//! and Observer roles role defined in the Bluetooth Core Specification +//! (5.4, Volume 3 Part C Section 2.2.2) +//! +//! These traits should be implemented outside this crate, conforming to the types and structs here +//! when necessary. + +use crate::client::Client; +use crate::types::*; + +use bt_common::{PeerId, Uuid}; +use futures::{Future, Stream}; + +#[derive(Debug, Clone)] +pub enum AdvertisingDatum { + Services(Vec<Uuid>), + ServiceData(Uuid, Vec<u8>), + ManufacturerData(u16, Vec<u8>), + // TODO: Update to a more structured Appearance + Appearance(u16), + TxPowerLevel(i8), + Uri(String), +} + +/// Matches a single advertised attribute or condition from a Bluetooth Low Energy peer. +#[derive(Clone, Debug)] +pub enum Filter { + /// Advertised Service UUID + ServiceUuid(Uuid), + /// ServiceData is included which is associated with the UUID + HasServiceData(Uuid), + /// ManufacturerData is provided with the Company Identifier Code given + HasManufacturerData(u16), + /// Connectable flag is set + IsConnectable, + /// String provided is included in the peer's name + MatchesName(String), + /// Path loss from the peer (RSSI - Advertised TX Power) is below the given dB value + MaxPathLoss(i8), +} + +/// A ScanFilter must match all of its combined filters and conditions to provide a result. +/// Currently can only include zero or more Filters. +/// The Default ScanFilter will match everything and should be avoided. +#[derive(Default, Clone, Debug)] +pub struct ScanFilter { + pub filters: Vec<Filter>, +} + +impl From<Filter> for ScanFilter { + fn from(value: Filter) -> Self { + ScanFilter { filters: vec![value] } + } +} + +impl ScanFilter { + pub fn add(&mut self, filter: Filter) -> &mut Self { + self.filters.push(filter); + self + } +} + +#[derive(Debug, Clone)] +pub enum PeerName { + Unknown, + PartialName(String), + CompleteName(String), +} + +#[derive(Debug, Clone)] +pub struct ScanResult { + pub id: PeerId, + pub connectable: bool, + pub name: PeerName, + pub advertised: Vec<AdvertisingDatum>, +} + +pub trait Central { + type ScanResultStream: Stream<Item = Result<ScanResult>> + 'static; + type Client: Client; + type ClientFut: Future<Output = Result<Self::Client>>; + + /// Scan for peers. + /// If any of the filters match, the results will be returned in the Stream. + fn scan(&self, filters: &[ScanFilter]) -> Self::ScanResultStream; + + /// Connect to a specific peer. + fn connect(&self, peer_id: PeerId) -> Self::ClientFut; +}
diff --git a/rust/bt-gatt/src/client/mod.rs b/rust/bt-gatt/src/client/mod.rs new file mode 100644 index 0000000..9e31756 --- /dev/null +++ b/rust/bt-gatt/src/client/mod.rs
@@ -0,0 +1,161 @@ +// Copyright 2023 Google LLC +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +use crate::types::*; + +use bt_common::{PeerId, Uuid}; +use futures::{Future, Stream}; + +pub enum ServiceKind { + Primary, + Secondary, +} + +// A short definition of a service that is either being published or has been discovered on a +// peer. +pub struct PeerServiceDefinition { + /// Service Handle + /// When publishing services, unique among all services published with [`Server::publish`]. + pub id: u64, + /// Whether the service is marked as Primary in the GATT server. + pub kind: ServiceKind, + /// The UUID identifying the type of service that this is. + pub uuid: Uuid, +} + +/// GATT Client connected to a particular peer. +/// Holding a struct that implements this should attempt to maintain a LE connection to the peer. +pub trait Client { + type PeerServiceHandleT: PeerServiceHandle; + type ServiceResultFut: Future<Output = Result<Vec<Self::PeerServiceHandleT>>> + 'static; + + /// The ID of the peer this is connected to. + fn peer_id(&self) -> PeerId; + + /// Find services by UUID on the peer. + /// This may cause as much as a full discovery of all services on the peer if the stack deems it + /// appropriate. + /// Service information should be up to date at the time returned. + fn find_service(&self, uuid: Uuid) -> Self::ServiceResultFut; +} + +pub trait PeerServiceHandle { + type PeerServiceT: PeerService; + type ConnectFut: Future<Output = Result<Self::PeerServiceT>>; + + fn uuid(&self) -> Uuid; + fn is_primary(&self) -> bool; + fn connect(&self) -> Self::ConnectFut; +} + +pub struct CharacteristicNotification { + pub handle: Handle, + pub value: Vec<u8>, + pub maybe_truncated: bool, +} + +/// A connection to a GATT Service on a Peer. +/// All operations are done synchronously. +pub trait PeerService { + type CharacteristicsFut: Future<Output = Result<Vec<Characteristic>>>; + type NotificationStream: Stream<Item = Result<CharacteristicNotification>>; + type ReadFut<'a>: Future<Output = Result<(usize, bool)>> + 'a; + type WriteFut<'a>: Future<Output = Result<()>> + 'a; + + /// Discover characteristics on this service. + /// If `uuid` is provided, only the characteristics matching `uuid` will be returned. + /// This operation may use either the Discovery All Characteristics of a Service or + /// Discovery Characteristic by UUID procedures, regardless of `uuid`. + fn discover_characteristics(&self, uuid: Option<Uuid>) -> Self::CharacteristicsFut; + + /// Read a characteristic into a buffer, given the handle within the service. + /// On success, returns the size read and whether the value may have been truncated. + /// By default this will try to use a long read if the `buf` is larger than a normal read will + /// allow (22 bytes) or if the offset is non-zero. + fn read_characteristic<'a>( + &self, + handle: &Handle, + offset: u16, + buf: &'a mut [u8], + ) -> Self::ReadFut<'a>; + + fn write_characteristic<'a>( + &self, + handle: &Handle, + mode: WriteMode, + offset: u16, + buf: &'a [u8], + ) -> Self::WriteFut<'a>; + + fn read_descriptor<'a>( + &self, + handle: &Handle, + offset: u16, + buf: &'a mut [u8], + ) -> Self::ReadFut<'a>; + + fn write_descriptor<'a>( + &self, + handle: &Handle, + offset: u16, + buf: &'a [u8], + ) -> Self::WriteFut<'a>; + + /// Subscribe to updates on a Characteristic. + /// Either notifications or indications will be enabled depending on the properties available, + /// with indications preferred if they are supported. + /// Fails if the Characteristic doesn't support indications or notifications. + /// Errors are delivered through an Err item in the stream. + /// This will often write to the Client Characteristic Configuration descriptor for the + /// Characteristic subscribed to. + /// Updates sent from the peer wlil be delivered to the Stream returned. + fn subscribe(&self, handle: &Handle) -> Self::NotificationStream; +} + +/// Convenience class for communicating with characteristics on a remote peer. +pub struct ServiceCharacteristic<'a, PeerServiceT> { + service: &'a PeerServiceT, + characteristic: Characteristic, + uuid: Uuid, +} + +impl<'a, PeerServiceT: PeerService> ServiceCharacteristic<'a, PeerServiceT> { + pub async fn find( + service: &'a PeerServiceT, + uuid: Uuid, + ) -> Result<Vec<ServiceCharacteristic<'a, PeerServiceT>>> { + let chrs = service.discover_characteristics(Some(uuid)).await?; + Ok(chrs + .into_iter() + .map(|characteristic| Self { + service, + characteristic, + uuid, + }) + .collect()) + } +} + +impl<'a, PeerServiceT> ServiceCharacteristic<'a, PeerServiceT> { + pub fn uuid(&self) -> Uuid { + self.uuid + } + + pub fn handle(&self) -> &Handle { + &self.characteristic.handle + } + + pub fn characteristic(&self) -> &Characteristic { + &self.characteristic + } +} + +impl<'a, PeerServiceT: PeerService> ServiceCharacteristic<'a, PeerServiceT> { + pub async fn read(&self, buf: &mut [u8]) -> Result<usize> { + self.service + .read_characteristic(self.handle(), 0, buf) + .await + .map(|(bytes, _)| bytes) + } +}
diff --git a/rust/bt-gatt/src/lib.rs b/rust/bt-gatt/src/lib.rs new file mode 100644 index 0000000..0eff602 --- /dev/null +++ b/rust/bt-gatt/src/lib.rs
@@ -0,0 +1,18 @@ +// Copyright 2023 The Sapphire Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +pub mod types; +pub use types::{Characteristic, Descriptor, Result}; + +pub mod server; +pub use server::Server; + +pub mod client; +pub use client::Client; + +pub mod central; +pub use central::Central; + +#[cfg(test)] +mod tests;
diff --git a/rust/bt-gatt/src/server/mod.rs b/rust/bt-gatt/src/server/mod.rs new file mode 100644 index 0000000..c5d53d3 --- /dev/null +++ b/rust/bt-gatt/src/server/mod.rs
@@ -0,0 +1,5 @@ +// Copyright 2023 The Sapphire Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +pub struct Server;
diff --git a/rust/bt-gatt/src/tests/mod.rs b/rust/bt-gatt/src/tests/mod.rs new file mode 100644 index 0000000..1df6bd5 --- /dev/null +++ b/rust/bt-gatt/src/tests/mod.rs
@@ -0,0 +1,175 @@ +// Copyright 2023 Google LLC +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +use crate::{central::ScanResult, client::CharacteristicNotification, types::*}; + +use bt_common::{PeerId, Uuid}; +use futures::{future::Ready, stream::Empty, Future, Stream}; +use std::task::Poll; + +#[derive(Default)] +pub(crate) struct FakeCentral {} + +pub(crate) struct FakePeerService {} + +impl crate::client::PeerService for FakePeerService { + type CharacteristicsFut = Ready<Result<Vec<Characteristic>>>; + type NotificationStream = Empty<Result<CharacteristicNotification>>; + type ReadFut<'a> = Ready<Result<(usize, bool)>>; + type WriteFut<'a> = Ready<Result<()>>; + + fn discover_characteristics(&self, _uuid: Option<Uuid>) -> Self::CharacteristicsFut { + todo!() + } + + fn read_characteristic<'a>( + &self, + _handle: &Handle, + _offset: u16, + _buf: &'a mut [u8], + ) -> Self::ReadFut<'a> { + todo!() + } + + fn write_characteristic<'a>( + &self, + _handle: &Handle, + _mode: WriteMode, + _offset: u16, + _buf: &'a [u8], + ) -> Self::WriteFut<'a> { + todo!() + } + + fn read_descriptor<'a>( + &self, + _handle: &Handle, + _offset: u16, + _buf: &'a mut [u8], + ) -> Self::ReadFut<'a> { + todo!() + } + + fn write_descriptor<'a>( + &self, + _handle: &Handle, + _offset: u16, + _buf: &'a [u8], + ) -> Self::WriteFut<'a> { + todo!() + } + + fn subscribe(&self, _handle: &Handle) -> Self::NotificationStream { + todo!() + } +} + +pub(crate) struct FakeServiceHandle {} + +impl crate::client::PeerServiceHandle for FakeServiceHandle { + type PeerServiceT = FakePeerService; + type ConnectFut = Ready<Result<Self::PeerServiceT>>; + + fn uuid(&self) -> Uuid { + todo!() + } + + fn is_primary(&self) -> bool { + todo!() + } + + fn connect(&self) -> Self::ConnectFut { + todo!() + } +} + +pub(crate) struct FakeClient {} + +impl crate::Client for FakeClient { + type PeerServiceHandleT = FakeServiceHandle; + type ServiceResultFut = Ready<Result<Vec<Self::PeerServiceHandleT>>>; + + fn peer_id(&self) -> PeerId { + todo!() + } + + fn find_service(&self, _uuid: Uuid) -> Self::ServiceResultFut { + todo!() + } +} + +pub(crate) struct SingleResultStream { + result: Option<Result<crate::central::ScanResult>>, +} + +impl Stream for SingleResultStream { + type Item = Result<crate::central::ScanResult>; + + fn poll_next( + self: std::pin::Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + ) -> Poll<Option<Self::Item>> { + if self.result.is_some() { + Poll::Ready(self.get_mut().result.take()) + } else { + // Never wake up, as if we never find another result + Poll::Pending + } + } +} + +pub(crate) struct ClientConnectFut {} + +impl Future for ClientConnectFut { + type Output = Result<FakeClient>; + + fn poll( + self: std::pin::Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + ) -> Poll<Self::Output> { + todo!() + } +} + +impl crate::Central for FakeCentral { + type ScanResultStream = SingleResultStream; + + type Client = FakeClient; + + type ClientFut = ClientConnectFut; + + fn scan(&self, _filters: &[crate::central::ScanFilter]) -> Self::ScanResultStream { + SingleResultStream { + result: Some(Ok(ScanResult { + id: PeerId(1), + connectable: true, + name: crate::central::PeerName::CompleteName("Marie's Pixel 7 Pro".to_owned()), + advertised: vec![crate::central::AdvertisingDatum::Services(vec![ + Uuid::from_u16(0x1844), + ])], + })), + } + } + + fn connect(&self, _peer_id: PeerId) -> Self::ClientFut { + todo!() + } +} + +#[test] +fn central_search_works() { + use crate::Central; + use futures::StreamExt; + + let mut noop_cx = futures::task::Context::from_waker(futures::task::noop_waker_ref()); + use crate::central::Filter; + let central = FakeCentral::default(); + + let mut scan_results = central.scan(&[Filter::ServiceUuid(Uuid::from_u16(0x1844)).into()]); + + let polled = scan_results.poll_next_unpin(&mut noop_cx); + let Poll::Ready(Some(Ok(_scan_result))) = polled else { + panic!("Expected a ready scan result got {polled:?}"); + }; +}
diff --git a/rust/bt-gatt/src/types/mod.rs b/rust/bt-gatt/src/types/mod.rs new file mode 100644 index 0000000..b837f09 --- /dev/null +++ b/rust/bt-gatt/src/types/mod.rs
@@ -0,0 +1,264 @@ +// Copyright 2023 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::{PeerId, Uuid}; +use thiserror::Error; + +/// 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)] +pub enum GattError { + InvalidHandle = 1, + ReadNotPermitted = 2, + WriteNotPermitted = 3, + InvalidPdu = 4, + InsufficientAuthentication = 5, + InvalidOffset = 7, + InsufficientAuthorization = 8, + InsufficientEncryptionKeySize = 12, + InvalidAttributeValueLength = 13, + UnlikelyError = 14, + InsufficientEncryption = 15, + InsufficientResources = 17, + ValueNotAllowed = 19, + ApplicationError80 = 128, + ApplicationError81 = 129, + ApplicationError82 = 130, + ApplicationError83 = 131, + ApplicationError84 = 132, + ApplicationError85 = 133, + ApplicationError86 = 134, + ApplicationError87 = 135, + ApplicationError88 = 136, + ApplicationError89 = 137, + ApplicationError8A = 138, + ApplicationError8B = 139, + ApplicationError8C = 140, + ApplicationError8D = 141, + ApplicationError8E = 142, + ApplicationError8F = 143, + ApplicationError90 = 144, + ApplicationError91 = 145, + ApplicationError92 = 146, + ApplicationError93 = 147, + ApplicationError94 = 148, + ApplicationError95 = 149, + ApplicationError96 = 150, + ApplicationError97 = 151, + ApplicationError98 = 152, + ApplicationError99 = 153, + ApplicationError9A = 154, + ApplicationError9B = 155, + ApplicationError9C = 156, + ApplicationError9D = 157, + ApplicationError9E = 158, + ApplicationError9F = 159, + WriteRequestRejected = 252, + CccDescriptorImproperlyConfigured = 253, + ProcedureAlreadyInProgress = 254, + OutOfRange = 255, + InvalidParameters = 257, + TooManyResults = 258, +} + +impl std::fmt::Display for GattError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl std::error::Error for GattError {} + +impl TryFrom<u32> for GattError { + type Error = Error; + + fn try_from(value: u32) -> std::result::Result<Self, Self::Error> { + match value { + 1 => Ok(Self::InvalidHandle), + 2 => Ok(Self::ReadNotPermitted), + 3 => Ok(Self::WriteNotPermitted), + 4 => Ok(Self::InvalidPdu), + 5 => Ok(Self::InsufficientAuthentication), + 7 => Ok(Self::InvalidOffset), + 8 => Ok(Self::InsufficientAuthorization), + 12 => Ok(Self::InsufficientEncryptionKeySize), + 13 => Ok(Self::InvalidAttributeValueLength), + 14 => Ok(Self::UnlikelyError), + 15 => Ok(Self::InsufficientEncryption), + 17 => Ok(Self::InsufficientResources), + 19 => Ok(Self::ValueNotAllowed), + 128 => Ok(Self::ApplicationError80), + 129 => Ok(Self::ApplicationError81), + 130 => Ok(Self::ApplicationError82), + 131 => Ok(Self::ApplicationError83), + 132 => Ok(Self::ApplicationError84), + 133 => Ok(Self::ApplicationError85), + 134 => Ok(Self::ApplicationError86), + 135 => Ok(Self::ApplicationError87), + 136 => Ok(Self::ApplicationError88), + 137 => Ok(Self::ApplicationError89), + 138 => Ok(Self::ApplicationError8A), + 139 => Ok(Self::ApplicationError8B), + 140 => Ok(Self::ApplicationError8C), + 141 => Ok(Self::ApplicationError8D), + 142 => Ok(Self::ApplicationError8E), + 143 => Ok(Self::ApplicationError8F), + 144 => Ok(Self::ApplicationError90), + 145 => Ok(Self::ApplicationError91), + 146 => Ok(Self::ApplicationError92), + 147 => Ok(Self::ApplicationError93), + 148 => Ok(Self::ApplicationError94), + 149 => Ok(Self::ApplicationError95), + 150 => Ok(Self::ApplicationError96), + 151 => Ok(Self::ApplicationError97), + 152 => Ok(Self::ApplicationError98), + 153 => Ok(Self::ApplicationError99), + 154 => Ok(Self::ApplicationError9A), + 155 => Ok(Self::ApplicationError9B), + 156 => Ok(Self::ApplicationError9C), + 157 => Ok(Self::ApplicationError9D), + 158 => Ok(Self::ApplicationError9E), + 159 => Ok(Self::ApplicationError9F), + 252 => Ok(Self::WriteRequestRejected), + 253 => Ok(Self::CccDescriptorImproperlyConfigured), + 254 => Ok(Self::ProcedureAlreadyInProgress), + 255 => Ok(Self::OutOfRange), + 257 => Ok(Self::InvalidParameters), + 258 => Ok(Self::TooManyResults), + x => Err(Error::Conversion(format!("Unknown GATT Error Value: {x}"))), + } + } +} + +#[derive(Error, Debug)] +pub enum Error { + #[error("peer {0} was not recognized")] + PeerNotRecognized(PeerId), + #[error("peer {0} was disconnected")] + PeerDisconnected(PeerId), + #[error("conversion error: {0}")] + Conversion(String), + #[error("scan failed: {0}")] + ScanFailed(String), + #[error("another error: {0}")] + Other(#[from] Box<dyn std::error::Error + Send>), + #[error("GATT error: {0}")] + Gatt(#[from] GattError), +} + +pub type Result<T> = core::result::Result<T, Error>; + +/// Handles are used as opaque identifiers for Characteristics and Descriptors. +/// Their value should be treated as opaque by clients of PeerService, and are explicitly not +/// guaranteed to be equal to a peer's attribute handles. +/// Stack implementations should provide unique handles for each Characteristic and Descriptor +/// within a PeerService. +#[derive(Copy, Clone, PartialEq, Debug)] +pub struct Handle(pub u64); + +pub enum WriteMode { + None, + Reliable, + WithoutResponse, +} + +#[repr(u16)] +pub enum CharacteristicProperty { + Broadcast = 0x01, + Read = 0x02, + WriteWithoutResponse = 0x04, + Write = 0x08, + Notify = 0x10, + Indicate = 0x20, + AuthenticatedSignedWrites = 0x40, + ReliableWrite = 0x100, + WritableAuxiliaries = 0x200, +} + +pub struct CharacteristicProperties(pub Vec<CharacteristicProperty>); + +#[derive(Debug, Clone, Copy, Default)] +pub struct SecurityLevels { + /// Encryption is required / provided + pub encryption: bool, + /// Authentication is required / provided + pub authentication: bool, + /// Authorization is required / provided + pub authorization: bool, +} + +impl SecurityLevels { + pub fn satisfied(&self, provided: SecurityLevels) -> bool { + (!self.encryption || provided.encryption) + && (!self.authentication || provided.authentication) + && (!self.authorization || provided.encryption) + } +} + +#[derive(Debug, Clone, Copy, Default)] +pub struct AttributePermissions { + /// If None, then this cannot be read. Otherwise the SecurityLevels given are required to read. + pub(crate) read: Option<SecurityLevels>, + /// If None, then this cannot be written. Otherwise the SecurityLevels given are required to + /// write. + pub(crate) write: Option<SecurityLevels>, + /// If None, then this cannot be updated. Otherwise the SecurityLevels given are required to + /// update. + pub(crate) update: Option<SecurityLevels>, +} + +/// The different types of well-known Descriptors are defined here and should be automatically read +/// during service characteristic discovery by Stack Implementations and included. +/// Other Descriptor attribute values can be read using [`PeerService::read_descriptor`]. +/// +/// Characteristic Extended Properties are included in the CharacteristicProperties of the +/// Characteristic. +/// This is a subset of the descriptors defined in the Core Specification (v5.4, Vol 3 Part G Sec +/// 3.3.3). Missing DescriptorTypes are handled internally and will be omitted from descriptor +/// APIs. +pub enum DescriptorType { + UserDescription(String), + ServerConfiguration { + broadcast: bool, + }, + CharacteristicPresentation { + format: u8, + exponent: i8, + unit: u16, + name_space: u8, + description: u16, + }, + Other { + uuid: Uuid, + }, +} + +pub struct Descriptor { + pub handle: Handle, + /// Permissions required needed to interact with this descriptor. May not be accurate on + /// clients until the descriptor is interacted with. + pub permissions: AttributePermissions, + pub r#type: DescriptorType, +} + +/// A Characteristic on a Service. Each Characteristic has a declaration, value, and zero or more +/// decriptors. +pub struct Characteristic { + pub handle: Handle, + pub uuid: Uuid, + pub properties: CharacteristicProperties, + /// Attribute Permissions that apply to the value + pub permissions: AttributePermissions, + pub descriptors: Vec<Descriptor>, +} + +impl Characteristic { + pub fn properties(&self) -> &CharacteristicProperties { + &self.properties + } + + pub fn descriptors(&self) -> impl Iterator<Item = &Descriptor> { + self.descriptors.iter() + } +}