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