rust/bt-gatt: Add server interface
Add API to define services and then:
- publish them
- receive connections from peers
- answer read requests
- receive write requests
- send notifications
Test: added fakes and unit tests
Test: cargo test
Change-Id: I4b4dcd22750e8dc83ffae357a22cde838df5ba04
Reviewed-on: https://bluetooth-review.git.corp.google.com/c/bluetooth/+/1602
Reviewed-by: Dayeong Lee <dayeonglee@google.com>
Reviewed-by: Ani Ramakrishnan <aniramakri@google.com>
diff --git a/rust/bt-gatt/src/client.rs b/rust/bt-gatt/src/client.rs
index 0a573b0..d27a448 100644
--- a/rust/bt-gatt/src/client.rs
+++ b/rust/bt-gatt/src/client.rs
@@ -6,24 +6,6 @@
use bt_common::{PeerId, Uuid};
-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.
@@ -117,6 +99,8 @@
/// Configuration descriptor for the Characteristic subscribed to.
/// Updates sent from the peer wlil be delivered to the Stream returned.
fn subscribe(&self, handle: &Handle) -> T::NotificationStream;
+
+ // TODO: Find included services
}
/// Convenience class for communicating with characteristics on a remote peer.
diff --git a/rust/bt-gatt/src/lib.rs b/rust/bt-gatt/src/lib.rs
index c441821..f5adefd 100644
--- a/rust/bt-gatt/src/lib.rs
+++ b/rust/bt-gatt/src/lib.rs
@@ -23,6 +23,9 @@
use futures::{Future, Stream};
/// Implementors implement traits with respect to GattTypes.
+/// Implementation crates provide an object which relates a constellation of
+/// types to each other, used to provide concrete types for library crates to
+/// paramaterize on.
pub trait GattTypes: Sized {
// Types related to finding and connecting to peers
type Central: Central<Self>;
@@ -52,3 +55,19 @@
/// Returns an error if the value could not be written.
type WriteFut<'a>: Future<Output = Result<()>> + 'a;
}
+
+/// Servers and services are defined with respect to ServerTypes.
+/// Implementation crates provide an object which relates this constellation of
+/// types to each other, used to provide concrete types for library crates to
+/// paramaterize on.
+pub trait ServerTypes: Sized {
+ type Server: Server<Self>;
+ type LocalService: server::LocalService<Self>;
+ type LocalServiceFut: Future<Output = Result<Self::LocalService>>;
+ type ServiceEventStream: Stream<Item = Result<server::ServiceEvent<Self>>>;
+ type ServiceWriteType: ToOwned<Owned = Vec<u8>>;
+ type ReadResponder: server::ReadResponder;
+ type WriteResponder: server::WriteResponder;
+
+ type IndicateConfirmationStream: Stream<Item = Result<server::ConfirmationEvent>>;
+}
diff --git a/rust/bt-gatt/src/server.rs b/rust/bt-gatt/src/server.rs
index c5d53d3..8a6f3b3 100644
--- a/rust/bt-gatt/src/server.rs
+++ b/rust/bt-gatt/src/server.rs
@@ -1,5 +1,234 @@
-// Copyright 2023 The Sapphire Authors. All rights reserved.
+// Copyright 2023 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.
-pub struct Server;
+use std::collections::HashSet;
+
+use bt_common::{PeerId, Uuid};
+
+use crate::types::*;
+
+/// ServiceId is used to identify a local service when publishing.
+#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
+pub struct ServiceId(u64);
+
+impl ServiceId {
+ pub fn new(id: u64) -> Self {
+ Self(id)
+ }
+}
+
+impl From<ServiceId> for u64 {
+ fn from(value: ServiceId) -> Self {
+ value.0
+ }
+}
+
+/// Defines a service to be added to a Server
+#[derive(Debug, Clone)]
+pub struct ServiceDefinition {
+ /// Local identifier used to include services within each other.
+ id: ServiceId,
+ /// UUID identifying the type of service
+ uuid: Uuid,
+ /// Whether the service is marked as Primary in the GATT server.
+ kind: ServiceKind,
+ /// Characteristics in the service. Add with
+ /// [ServiceDefinition::add_characteristic]
+ characteristics: Vec<Characteristic>,
+ included_services: HashSet<ServiceId>,
+ /// Set of handles (characteristic or descriptor) used, to verify uniquness
+ /// of new handles added.
+ handles: HashSet<Handle>,
+}
+
+impl ServiceDefinition {
+ /// Make a new, empty service with a given id.
+ pub fn new(id: ServiceId, uuid: Uuid, kind: ServiceKind) -> Self {
+ Self {
+ id,
+ uuid,
+ kind,
+ characteristics: Default::default(),
+ included_services: Default::default(),
+ handles: HashSet::new(),
+ }
+ }
+
+ /// Add a characteristic to the definition. If any duplicate handles or an
+ /// invalid configuration of descriptors are detected, an error is
+ /// returned.
+ pub fn add_characteristic(&mut self, characteristic: Characteristic) -> Result<()> {
+ let new_handles = characteristic.handles().collect();
+ if !self.handles.is_disjoint(&new_handles) {
+ return Err(Error::DuplicateHandle(
+ self.handles.intersection(&new_handles).copied().collect(),
+ ));
+ }
+ self.handles.extend(new_handles);
+ // TODO: check for more errors
+ self.characteristics.push(characteristic);
+ Ok(())
+ }
+
+ pub fn id(&self) -> ServiceId {
+ self.id
+ }
+
+ pub fn uuid(&self) -> Uuid {
+ self.uuid
+ }
+
+ pub fn kind(&self) -> ServiceKind {
+ self.kind
+ }
+
+ pub fn characteristics(&self) -> impl Iterator<Item = &Characteristic> {
+ self.characteristics.iter()
+ }
+
+ pub fn included(&self) -> impl Iterator<Item = ServiceId> + '_ {
+ self.included_services.iter().cloned()
+ }
+
+ /// Add a service to the definition.
+ pub fn add_service(&mut self, id: ServiceId) {
+ self.included_services.insert(id);
+ }
+}
+
+/// Services can be included in other services, and are included in the database
+/// when they are published. All included services should be prepared before
+/// the service including them. Publishing a service that includes other
+/// services will publish the included services, although the events associated
+/// with the included service will not be returned until the
+/// [LocalService::publish] is called.
+pub trait Server<T: crate::ServerTypes> {
+ /// Prepare to publish a service.
+ /// This service is not immediately visible in the local GATT server.
+ /// It will be published when the LocalService::publish is called.
+ /// If the returned LocalService is dropped, the service will be removed
+ /// from the Server.
+ fn prepare(&self, service: ServiceDefinition) -> T::LocalServiceFut;
+}
+
+pub trait LocalService<T: crate::ServerTypes> {
+ /// Publish the service.
+ /// Returns an EventStream providing Events to be processed by the local
+ /// service implementation.
+ /// Events will only be delivered to one ServiceEventStream at a time.
+ /// Calling publish while a previous ServiceEventStream is still active
+ /// will return a stream with only Err(AlreadyPublished).
+ fn publish(&self) -> T::ServiceEventStream;
+
+ /// Notify a characteristic.
+ /// Leave `peers` empty to notify all peers who have configured
+ /// notifications. Peers that have not configured for notifications will
+ /// not be notified.
+ fn notify(&self, characteristic: &Handle, data: &[u8], peers: &[PeerId]);
+
+ /// Indicate on a characteristic.
+ /// Leave `peers` empty to notify all peers who have configured
+ /// indications. Peers that have not configured for indications will
+ /// be skipped. Returns a stream which has items for each peer that
+ /// confirms the notification, and terminates when all peers have either
+ /// timed out or confirmed.
+ fn indicate(
+ &self,
+ characteristic: &Handle,
+ data: &[u8],
+ peers: &[PeerId],
+ ) -> T::IndicateConfirmationStream;
+}
+
+pub struct ConfirmationEvent {
+ peer_id: PeerId,
+ result: Result<()>,
+}
+
+impl ConfirmationEvent {
+ pub fn create_ack(peer_id: PeerId) -> Self {
+ Self { peer_id, result: Ok(()) }
+ }
+
+ pub fn create_error(peer_id: PeerId, error: Error) -> Self {
+ Self { peer_id, result: Err(error) }
+ }
+ pub fn peer_id(&self) -> PeerId {
+ self.peer_id
+ }
+
+ pub fn error(&self) -> Option<&Error> {
+ self.result.as_ref().err()
+ }
+
+ pub fn is_ok(&self) -> bool {
+ self.result.is_ok()
+ }
+
+ pub fn is_err(&self) -> bool {
+ self.result.is_err()
+ }
+}
+
+/// Responder that can send data that has been read from a characteristic.
+pub trait ReadResponder {
+ /// Respond with the data requested. `value` may be shorter than requested.
+ fn respond(self, value: &[u8]);
+ /// Respond with an error.
+ fn error(self, error: GattError);
+}
+
+/// Responder that can acknowledge a write to a characteristic.
+pub trait WriteResponder {
+ /// Acknowledge the write. Will only send an acknowledgement if allowed by
+ /// the GATT protocol.
+ fn acknowledge(self);
+ /// Respond with an error.
+ fn error(self, error: GattError);
+}
+
+#[derive(Debug)]
+pub enum NotificationType {
+ Disable,
+ Notify,
+ Indicate,
+}
+
+#[non_exhaustive]
+pub enum ServiceEvent<T: crate::ServerTypes> {
+ /// Peer requests to read from a handle (characteritic or descriptor) at the
+ /// given offset.
+ Read { peer_id: PeerId, handle: Handle, offset: u32, responder: T::ReadResponder },
+ /// Peer has written a value to a handle (characteristic or descriptor) at
+ /// the given offset.
+ Write {
+ peer_id: PeerId,
+ handle: Handle,
+ offset: u32,
+ value: T::ServiceWriteType,
+ responder: T::WriteResponder,
+ },
+ /// Notification that a peer has configured a characteristic for indication
+ /// or notification.
+ ClientConfiguration { peer_id: PeerId, handle: Handle, notification_type: NotificationType },
+ /// Extra information about a peer is provided. This event may not be sent
+ /// by all implementations.
+ #[non_exhaustive]
+ PeerInfo { peer_id: PeerId, mtu: Option<u16>, connected: Option<bool> },
+}
+
+impl<T: crate::ServerTypes> ServiceEvent<T> {
+ pub fn peer_id(&self) -> PeerId {
+ match self {
+ Self::Read { peer_id, .. } => *peer_id,
+ Self::Write { peer_id, .. } => *peer_id,
+ Self::ClientConfiguration { peer_id, .. } => *peer_id,
+ Self::PeerInfo { peer_id, .. } => *peer_id,
+ }
+ }
+
+ pub fn peer_info(peer_id: PeerId, mtu: Option<u16>, connected: Option<bool>) -> Self {
+ Self::PeerInfo { peer_id, mtu, connected }
+ }
+}
diff --git a/rust/bt-gatt/src/test_utils.rs b/rust/bt-gatt/src/test_utils.rs
index 7a3578f..e155f8f 100644
--- a/rust/bt-gatt/src/test_utils.rs
+++ b/rust/bt-gatt/src/test_utils.rs
@@ -14,7 +14,8 @@
use crate::central::ScanResult;
use crate::client::CharacteristicNotification;
-use crate::{types::*, GattTypes};
+use crate::server::{self, LocalService, ReadResponder, ServiceDefinition, WriteResponder};
+use crate::{types::*, GattTypes, ServerTypes};
#[derive(Default)]
struct FakePeerServiceInner {
@@ -257,6 +258,17 @@
type WriteFut<'a> = Ready<Result<()>>;
}
+impl ServerTypes for FakeTypes {
+ type Server = FakeServer;
+ type LocalService = FakeLocalService;
+ type LocalServiceFut = Ready<Result<FakeLocalService>>;
+ type ServiceEventStream = UnboundedReceiver<Result<server::ServiceEvent<FakeTypes>>>;
+ type ServiceWriteType = Vec<u8>;
+ type ReadResponder = FakeResponder;
+ type WriteResponder = FakeResponder;
+ type IndicateConfirmationStream = UnboundedReceiver<Result<server::ConfirmationEvent>>;
+}
+
#[derive(Default)]
pub struct FakeCentralInner {
clients: HashMap<PeerId, FakeClient>,
@@ -291,3 +303,243 @@
futures::future::ready(res)
}
}
+
+pub enum FakeServerEvent {
+ ReadResponded {
+ service_id: server::ServiceId,
+ handle: Handle,
+ value: Result<Vec<u8>>,
+ },
+ WriteResponded {
+ service_id: server::ServiceId,
+ handle: Handle,
+ value: Result<()>,
+ },
+ Notified {
+ service_id: server::ServiceId,
+ handle: Handle,
+ value: Vec<u8>,
+ peers: Vec<PeerId>,
+ },
+ Indicated {
+ service_id: server::ServiceId,
+ handle: Handle,
+ value: Vec<u8>,
+ peers: Vec<PeerId>,
+ confirmations: UnboundedSender<Result<server::ConfirmationEvent>>,
+ },
+ Unpublished {
+ id: server::ServiceId,
+ },
+ Published {
+ id: server::ServiceId,
+ definition: ServiceDefinition,
+ },
+}
+
+#[derive(Debug)]
+struct FakeServerInner {
+ services: HashMap<server::ServiceId, ServiceDefinition>,
+ service_senders:
+ HashMap<server::ServiceId, UnboundedSender<Result<server::ServiceEvent<FakeTypes>>>>,
+ sender: UnboundedSender<FakeServerEvent>,
+}
+
+#[derive(Debug)]
+pub struct FakeServer {
+ inner: Arc<Mutex<FakeServerInner>>,
+}
+
+impl server::Server<FakeTypes> for FakeServer {
+ fn prepare(
+ &self,
+ service: server::ServiceDefinition,
+ ) -> <FakeTypes as ServerTypes>::LocalServiceFut {
+ let id = service.id();
+ self.inner.lock().services.insert(id, service);
+ futures::future::ready(Ok(FakeLocalService::new(id, self.inner.clone())))
+ }
+}
+
+impl FakeServer {
+ pub fn new() -> (Self, UnboundedReceiver<FakeServerEvent>) {
+ let (sender, receiver) = futures::channel::mpsc::unbounded();
+ (
+ Self {
+ inner: Arc::new(Mutex::new(FakeServerInner {
+ services: Default::default(),
+ service_senders: Default::default(),
+ sender,
+ })),
+ },
+ receiver,
+ )
+ }
+
+ pub fn service(&self, id: server::ServiceId) -> Option<ServiceDefinition> {
+ self.inner.lock().services.get(&id).cloned()
+ }
+
+ pub fn incoming_write(
+ &self,
+ peer_id: PeerId,
+ id: server::ServiceId,
+ handle: Handle,
+ offset: u32,
+ value: Vec<u8>,
+ ) {
+ // TODO: check that the write is allowed
+ let sender = self.inner.lock().sender.clone();
+ self.inner
+ .lock()
+ .service_senders
+ .get(&id)
+ .unwrap()
+ .unbounded_send(Ok(server::ServiceEvent::Write {
+ peer_id,
+ handle,
+ offset,
+ value,
+ responder: FakeResponder { sender, service_id: id, handle },
+ }))
+ .unwrap();
+ }
+
+ pub fn incoming_read(
+ &self,
+ peer_id: PeerId,
+ id: server::ServiceId,
+ handle: Handle,
+ offset: u32,
+ ) {
+ // TODO: check that the read is allowed
+ let sender = self.inner.lock().sender.clone();
+ self.inner
+ .lock()
+ .service_senders
+ .get(&id)
+ .unwrap()
+ .unbounded_send(Ok(server::ServiceEvent::Read {
+ peer_id,
+ handle,
+ offset,
+ responder: FakeResponder { sender, service_id: id, handle },
+ }))
+ .unwrap();
+ }
+}
+
+pub struct FakeLocalService {
+ id: server::ServiceId,
+ inner: Arc<Mutex<FakeServerInner>>,
+}
+
+impl FakeLocalService {
+ fn new(id: server::ServiceId, inner: Arc<Mutex<FakeServerInner>>) -> Self {
+ Self { id, inner }
+ }
+}
+
+impl Drop for FakeLocalService {
+ fn drop(&mut self) {
+ self.inner.lock().services.remove(&self.id);
+ }
+}
+
+impl LocalService<FakeTypes> for FakeLocalService {
+ fn publish(&self) -> <FakeTypes as ServerTypes>::ServiceEventStream {
+ let (sender, receiver) = futures::channel::mpsc::unbounded();
+ let _ = self.inner.lock().service_senders.insert(self.id, sender);
+ let definition = self.inner.lock().services.get(&self.id).unwrap().clone();
+ self.inner
+ .lock()
+ .sender
+ .unbounded_send(FakeServerEvent::Published { id: self.id, definition })
+ .unwrap();
+ receiver
+ }
+
+ fn notify(&self, characteristic: &Handle, data: &[u8], peers: &[PeerId]) {
+ self.inner
+ .lock()
+ .sender
+ .unbounded_send(FakeServerEvent::Notified {
+ service_id: self.id,
+ handle: *characteristic,
+ value: data.into(),
+ peers: peers.into(),
+ })
+ .unwrap();
+ }
+
+ fn indicate(
+ &self,
+ characteristic: &Handle,
+ data: &[u8],
+ peers: &[PeerId],
+ ) -> <FakeTypes as ServerTypes>::IndicateConfirmationStream {
+ let (sender, receiver) = futures::channel::mpsc::unbounded();
+ self.inner
+ .lock()
+ .sender
+ .unbounded_send(FakeServerEvent::Indicated {
+ service_id: self.id,
+ handle: *characteristic,
+ value: data.into(),
+ peers: peers.into(),
+ confirmations: sender,
+ })
+ .unwrap();
+ receiver
+ }
+}
+
+pub struct FakeResponder {
+ sender: UnboundedSender<FakeServerEvent>,
+ service_id: server::ServiceId,
+ handle: Handle,
+}
+
+impl ReadResponder for FakeResponder {
+ fn respond(self, value: &[u8]) {
+ self.sender
+ .unbounded_send(FakeServerEvent::ReadResponded {
+ service_id: self.service_id,
+ handle: self.handle,
+ value: Ok(value.into()),
+ })
+ .unwrap();
+ }
+
+ fn error(self, error: GattError) {
+ self.sender
+ .unbounded_send(FakeServerEvent::ReadResponded {
+ service_id: self.service_id,
+ handle: self.handle,
+ value: Err(Error::Gatt(error)),
+ })
+ .unwrap();
+ }
+}
+
+impl WriteResponder for FakeResponder {
+ fn acknowledge(self) {
+ self.sender
+ .unbounded_send(FakeServerEvent::WriteResponded {
+ service_id: self.service_id,
+ handle: self.handle,
+ value: Ok(()),
+ })
+ .unwrap();
+ }
+
+ fn error(self, error: GattError) {
+ self.sender
+ .unbounded_send(FakeServerEvent::WriteResponded {
+ service_id: self.service_id,
+ handle: self.handle,
+ value: Err(Error::Gatt(error)),
+ })
+ .unwrap();
+ }
+}
diff --git a/rust/bt-gatt/src/tests.rs b/rust/bt-gatt/src/tests.rs
index e753706..896f2cc 100644
--- a/rust/bt-gatt/src/tests.rs
+++ b/rust/bt-gatt/src/tests.rs
@@ -10,9 +10,10 @@
use bt_common::{PeerId, Uuid};
use crate::central::{AdvertisingDatum, PeerName, ScanResult};
+use crate::server::{ServiceDefinition, ServiceEvent};
use crate::test_utils::*;
use crate::types::*;
-use crate::{central::Filter, client::PeerService, Central};
+use crate::{central::Filter, client::PeerService, server, Central};
const TEST_UUID_1: Uuid = Uuid::from_u16(0x1234);
const TEST_UUID_2: Uuid = Uuid::from_u16(0x2345);
@@ -192,7 +193,7 @@
}
#[test]
-fn peer_service_subsribe() {
+fn peer_service_subscribe() {
let mut noop_cx = futures::task::Context::from_waker(futures::task::noop_waker_ref());
let mut fake_peer_service = setup_peer_service();
@@ -293,3 +294,113 @@
boxed_generic_usage(boxed);
}
+
+/// Generate an example service definition with a variety of things in it.
+fn example_service_definition() -> ServiceDefinition {
+ let mut def =
+ ServiceDefinition::new(server::ServiceId::new(1), TEST_UUID_1, ServiceKind::Primary);
+ // Only readable
+ def.add_characteristic(Characteristic {
+ handle: Handle(1),
+ uuid: TEST_UUID_2,
+ properties: CharacteristicProperty::Read.into(),
+ permissions: AttributePermissions {
+ read: Some(SecurityLevels::default()),
+ write: None,
+ update: None,
+ },
+ descriptors: vec![],
+ })
+ .unwrap();
+ // Only writable
+ def.add_characteristic(Characteristic {
+ handle: Handle(2),
+ uuid: TEST_UUID_3,
+ properties: CharacteristicProperty::Write.into(),
+ permissions: AttributePermissions {
+ read: None,
+ write: Some(SecurityLevels::default()),
+ update: None,
+ },
+ descriptors: vec![],
+ })
+ .unwrap();
+ // Readable, writable, and notifiable
+ def.add_characteristic(Characteristic {
+ handle: Handle(3),
+ uuid: TEST_UUID_3,
+ properties: CharacteristicProperty::Read
+ | CharacteristicProperty::Write
+ | CharacteristicProperty::Notify,
+ permissions: AttributePermissions {
+ read: Some(SecurityLevels::default()),
+ write: Some(SecurityLevels::default()),
+ update: Some(SecurityLevels::default()),
+ },
+ descriptors: vec![],
+ })
+ .unwrap();
+ // Only Readable and Indicatable
+ def.add_characteristic(Characteristic {
+ handle: Handle(4),
+ uuid: TEST_UUID_3,
+ properties: CharacteristicProperty::Read | CharacteristicProperty::Indicate,
+ permissions: AttributePermissions {
+ read: Some(SecurityLevels::default()),
+ write: None,
+ update: Some(SecurityLevels::default()),
+ },
+ descriptors: vec![],
+ })
+ .unwrap();
+ def
+}
+
+#[test]
+fn fake_server_usable() {
+ use server::{LocalService, Server, ServiceId};
+ let mut noop_cx = futures::task::Context::from_waker(futures::task::noop_waker_ref());
+
+ let (server, mut events) = FakeServer::new();
+ let mut local_service_fut = server.prepare(example_service_definition());
+
+ let Poll::Ready(Ok(local_service)) = local_service_fut.poll_unpin(&mut noop_cx) else {
+ panic!("Expected local service to prepare fine");
+ };
+
+ let mut service_event_stream = local_service.publish();
+
+ // Published shouldn't be emitted until the service is present on the server.
+ let Poll::Ready(Some(FakeServerEvent::Published { id, definition })) =
+ events.poll_next_unpin(&mut noop_cx)
+ else {
+ panic!("Expected to generate an event from publishing");
+ };
+
+ assert_eq!(server::ServiceId::new(1), id);
+ assert_eq!(definition.characteristics().count(), 4);
+
+ server.incoming_read(PeerId(1), ServiceId::new(1), Handle(1), 0);
+
+ let Poll::Ready(Some(Ok(ServiceEvent::Read { peer_id, handle, offset: _, responder }))) =
+ service_event_stream.poll_next_unpin(&mut noop_cx)
+ else {
+ panic!("Expected read on the service stream");
+ };
+
+ assert_eq!(PeerId(1), peer_id);
+ assert_eq!(Handle(1), handle);
+
+ use server::ReadResponder;
+ responder.respond(&[0, 1, 2, 3, 4, 5]);
+
+ let Poll::Ready(Some(FakeServerEvent::ReadResponded { service_id, handle, value })) =
+ events.poll_next_unpin(&mut noop_cx)
+ else {
+ panic!("Expected a read response");
+ };
+
+ assert_eq!(ServiceId::new(1), service_id);
+ assert_eq!(Handle(1), handle);
+ assert_eq!(&[0, 1, 2, 3, 4, 5], value.expect("should be ok").as_slice());
+}
diff --git a/rust/bt-gatt/src/types.rs b/rust/bt-gatt/src/types.rs
index cd052f2..0e7ada4 100644
--- a/rust/bt-gatt/src/types.rs
+++ b/rust/bt-gatt/src/types.rs
@@ -147,6 +147,10 @@
Other(#[from] Box<dyn std::error::Error + Send + Sync>),
#[error("GATT error: {0}")]
Gatt(#[from] GattError),
+ #[error("definition error: duplicate handle(s) {0:?}")]
+ DuplicateHandle(Vec<Handle>),
+ #[error("service for {0:?} already published")]
+ AlreadyPublished(crate::server::ServiceId),
}
impl From<String> for Error {
@@ -163,14 +167,34 @@
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.
+/// Handles are used as local-to-your-code 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.
+/// and Descriptor within a PeerService for the client. It is not necessary or
+/// expected for different clients of the same service to have the same handles.
+///
+/// For LocalService publishing, Service Definitions are required to have no
+/// duplicate Handles across their defined Characteristics and Descriptors.
+/// Characteristic and ServiceDefinition will check and return
+/// Error::DuplicateHandle. Handles in ServiceDefinitions should not match the
+/// attribute handles in the local GATT server. They are local to your code for
+/// use in correlating with ServiceEvents from peers. Stacks must translate
+/// the actual GATT handles to the handles provided in the ServiceDefinition
+/// for these events.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct Handle(pub u64);
+/// Whether a service is marked as Primary or Secondary on the server.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum ServiceKind {
+ Primary,
+ Secondary,
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WriteMode {
None,
Reliable,
@@ -219,13 +243,13 @@
pub struct AttributePermissions {
/// If None, then this cannot be read. Otherwise the SecurityLevels given
/// are required to read.
- pub(crate) read: Option<SecurityLevels>,
+ pub read: Option<SecurityLevels>,
/// If None, then this cannot be written. Otherwise the SecurityLevels given
/// are required to write.
- pub(crate) write: Option<SecurityLevels>,
+ pub write: Option<SecurityLevels>,
/// If None, then this cannot be updated. Otherwise the SecurityLevels given
/// are required to update.
- pub(crate) update: Option<SecurityLevels>,
+ pub update: Option<SecurityLevels>,
}
/// The different types of well-known Descriptors are defined here and should be
@@ -236,8 +260,8 @@
/// 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.
+/// Vol 3 Part G Sec 3.3.3). Missing DescriptorTypes are handled internally
+/// and are added automatically, and are omitted from descriptor APIs.
#[derive(Clone, Debug)]
pub enum DescriptorType {
UserDescription(String),
@@ -256,6 +280,18 @@
},
}
+impl From<&DescriptorType> for Uuid {
+ fn from(value: &DescriptorType) -> Self {
+ // Values from https://bitbucket.org/bluetooth-SIG/public/src/main/assigned_numbers/uuids/descriptors.yaml
+ match value {
+ DescriptorType::UserDescription(_) => Uuid::from_u16(0x2901),
+ DescriptorType::ServerConfiguration { .. } => Uuid::from_u16(0x2903),
+ DescriptorType::CharacteristicPresentation { .. } => Uuid::from_u16(0x2904),
+ DescriptorType::Other { uuid } => uuid.clone(),
+ }
+ }
+}
+
#[derive(Clone, Debug)]
pub struct Descriptor {
pub handle: Handle,
@@ -285,4 +321,8 @@
pub fn descriptors(&self) -> impl Iterator<Item = &Descriptor> {
self.descriptors.iter()
}
+
+ pub fn handles(&self) -> impl Iterator<Item = Handle> + '_ {
+ std::iter::once(self.handle).chain(self.descriptors.iter().map(|d| d.handle))
+ }
}