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