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