1 // Copyright 2023 The Pigweed Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License"); you may not
4 // use this file except in compliance with the License. You may obtain a copy of
5 // the License at
6 //
7 // https://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 // License for the specific language governing permissions and limitations under
13 // the License.
14
15 #include "pw_bluetooth_sapphire/internal/host/gatt/local_service_manager.h"
16
17 #include <endian.h>
18
19 #include <algorithm>
20
21 #include "pw_bluetooth_sapphire/internal/host/common/assert.h"
22 #include "pw_bluetooth_sapphire/internal/host/common/log.h"
23 #include "pw_bluetooth_sapphire/internal/host/gatt/gatt_defs.h"
24
25 namespace bt::gatt {
26 namespace {
27
28 // Adds characteristic definition attributes to |grouping| for |chrc|. Returns
29 // the characteristic value handle.
InsertCharacteristicAttributes(att::AttributeGrouping * grouping,const Characteristic & chrc,att::Attribute::ReadHandler read_handler,att::Attribute::WriteHandler write_handler)30 att::Handle InsertCharacteristicAttributes(
31 att::AttributeGrouping* grouping,
32 const Characteristic& chrc,
33 att::Attribute::ReadHandler read_handler,
34 att::Attribute::WriteHandler write_handler) {
35 BT_DEBUG_ASSERT(grouping);
36 BT_DEBUG_ASSERT(!grouping->complete());
37 BT_DEBUG_ASSERT(read_handler);
38 BT_DEBUG_ASSERT(write_handler);
39
40 // Characteristic Declaration (Vol 3, Part G, 3.3.1).
41 auto* decl_attr = grouping->AddAttribute(
42 types::kCharacteristicDeclaration,
43 att::AccessRequirements(/*encryption=*/false,
44 /*authentication=*/false,
45 /*authorization=*/false), // read (no security)
46 att::AccessRequirements()); // write (not allowed)
47 BT_DEBUG_ASSERT(decl_attr);
48
49 // Characteristic Value Declaration (Vol 3, Part G, 3.3.2)
50 auto* value_attr = grouping->AddAttribute(
51 chrc.type(), chrc.read_permissions(), chrc.write_permissions());
52 BT_DEBUG_ASSERT(value_attr);
53
54 value_attr->set_read_handler(std::move(read_handler));
55 value_attr->set_write_handler(std::move(write_handler));
56
57 size_t uuid_size = chrc.type().CompactSize(/*allow_32bit=*/false);
58 BT_DEBUG_ASSERT(uuid_size == 2 || uuid_size == 16);
59
60 // The characteristic declaration value contains:
61 // 1 octet: properties
62 // 2 octets: value handle
63 // 2 or 16 octets: UUID
64 DynamicByteBuffer decl_value(3 + uuid_size);
65 decl_value[0] = chrc.properties();
66 decl_value[1] = static_cast<uint8_t>(value_attr->handle());
67 decl_value[2] = static_cast<uint8_t>(value_attr->handle() >> 8);
68
69 auto uuid_view = decl_value.mutable_view(3);
70 chrc.type().ToBytes(&uuid_view, /*allow_32bit=*/false);
71 decl_attr->SetValue(decl_value);
72
73 return value_attr->handle();
74 }
75
76 // Adds a characteristic descriptor declaration to |grouping| for |desc|.
InsertDescriptorAttribute(att::AttributeGrouping * grouping,const UUID & type,const att::AccessRequirements & read_reqs,const att::AccessRequirements & write_reqs,att::Attribute::ReadHandler read_handler,att::Attribute::WriteHandler write_handler)77 void InsertDescriptorAttribute(att::AttributeGrouping* grouping,
78 const UUID& type,
79 const att::AccessRequirements& read_reqs,
80 const att::AccessRequirements& write_reqs,
81 att::Attribute::ReadHandler read_handler,
82 att::Attribute::WriteHandler write_handler) {
83 BT_DEBUG_ASSERT(grouping);
84 BT_DEBUG_ASSERT(!grouping->complete());
85 BT_DEBUG_ASSERT(read_handler);
86 BT_DEBUG_ASSERT(write_handler);
87
88 // There is no special declaration attribute type for descriptors.
89 auto* attr = grouping->AddAttribute(type, read_reqs, write_reqs);
90 BT_DEBUG_ASSERT(attr);
91
92 attr->set_read_handler(std::move(read_handler));
93 attr->set_write_handler(std::move(write_handler));
94 }
95
96 // Returns false if the given service hierarchy contains repeating identifiers.
97 // Returns the number of attributes that will be in the service attribute group
98 // (exluding the service declaration) in |out_attrs|.
ValidateService(const Service & service,size_t * out_attr_count)99 bool ValidateService(const Service& service, size_t* out_attr_count) {
100 BT_DEBUG_ASSERT(out_attr_count);
101
102 size_t attr_count = 0u;
103 std::unordered_set<IdType> ids;
104 for (const auto& chrc_ptr : service.characteristics()) {
105 if (ids.count(chrc_ptr->id()) != 0u) {
106 bt_log(TRACE, "gatt", "server: repeated ID: %lu", chrc_ptr->id());
107 return false;
108 }
109
110 ids.insert(chrc_ptr->id());
111
112 // +1: Characteristic Declaration (Vol 3, Part G, 3.3.1)
113 // +1: Characteristic Value Declaration (Vol 3, Part G, 3.3.2)
114 attr_count += 2;
115
116 // Increment the count for the CCC descriptor if the characteristic supports
117 // notifications or indications.
118 if ((chrc_ptr->properties() & Property::kNotify) ||
119 (chrc_ptr->properties() & Property::kIndicate)) {
120 attr_count++;
121 }
122
123 for (const auto& desc_ptr : chrc_ptr->descriptors()) {
124 if (ids.count(desc_ptr->id()) != 0u) {
125 bt_log(TRACE, "gatt", "server: repeated ID: %lu", desc_ptr->id());
126 return false;
127 }
128
129 // Reject descriptors with types that are internally managed by us.
130 if (desc_ptr->type() == types::kClientCharacteristicConfig ||
131 desc_ptr->type() == types::kCharacteristicExtProperties ||
132 desc_ptr->type() == types::kServerCharacteristicConfig) {
133 bt_log(TRACE,
134 "gatt",
135 "server: disallowed descriptor type: %s",
136 desc_ptr->type().ToString().c_str());
137 return false;
138 }
139
140 ids.insert(desc_ptr->id());
141
142 // +1: Characteristic Descriptor Declaration (Vol 3, Part G, 3.3.3)
143 attr_count++;
144 }
145 if (chrc_ptr->extended_properties()) {
146 attr_count++;
147 }
148 }
149
150 *out_attr_count = attr_count;
151
152 return true;
153 }
154
155 } // namespace
156
157 class LocalServiceManager::ServiceData final {
158 public:
ServiceData(IdType id,att::AttributeGrouping * grouping,Service * service,ReadHandler && read_handler,WriteHandler && write_handler,ClientConfigCallback && ccc_callback)159 ServiceData(IdType id,
160 att::AttributeGrouping* grouping,
161 Service* service,
162 ReadHandler&& read_handler,
163 WriteHandler&& write_handler,
164 ClientConfigCallback&& ccc_callback)
165 : id_(id),
166 read_handler_(std::forward<ReadHandler>(read_handler)),
167 write_handler_(std::forward<WriteHandler>(write_handler)),
168 ccc_callback_(std::forward<ClientConfigCallback>(ccc_callback)),
169 weak_self_(this) {
170 BT_DEBUG_ASSERT(read_handler_);
171 BT_DEBUG_ASSERT(write_handler_);
172 BT_DEBUG_ASSERT(ccc_callback_);
173 BT_DEBUG_ASSERT(grouping);
174
175 start_handle_ = grouping->start_handle();
176 end_handle_ = grouping->end_handle();
177
178 // Sort characteristics by UUID size (see Vol 3, Part G, 3.3.1).
179 auto chrcs = service->ReleaseCharacteristics();
180 std::sort(chrcs.begin(),
181 chrcs.end(),
182 [](const auto& chrc_ptr1, const auto& chrc_ptr2) {
183 return chrc_ptr1->type().CompactSize(/*allow_32bit=*/false) <
184 chrc_ptr2->type().CompactSize(/*allow_32bit=*/false);
185 });
186 for (auto& chrc : chrcs) {
187 AddCharacteristic(grouping, std::move(chrc));
188 }
189 }
190
id() const191 inline IdType id() const { return id_; }
start_handle() const192 inline att::Handle start_handle() const { return start_handle_; }
end_handle() const193 inline att::Handle end_handle() const { return end_handle_; }
194
GetCharacteristicConfig(IdType chrc_id,PeerId peer_id,ClientCharacteristicConfig * out_config)195 bool GetCharacteristicConfig(IdType chrc_id,
196 PeerId peer_id,
197 ClientCharacteristicConfig* out_config) {
198 BT_DEBUG_ASSERT(out_config);
199
200 auto iter = chrc_configs_.find(chrc_id);
201 if (iter == chrc_configs_.end())
202 return false;
203
204 uint16_t value = iter->second.Get(peer_id);
205 out_config->handle = iter->second.handle();
206 out_config->notify = value & kCCCNotificationBit;
207 out_config->indicate = value & kCCCIndicationBit;
208
209 return true;
210 }
211
212 // Clean up our knoweledge of the diconnecting peer.
DisconnectClient(PeerId peer_id)213 void DisconnectClient(PeerId peer_id) {
214 for (auto& id_config_pair : chrc_configs_) {
215 id_config_pair.second.Erase(peer_id);
216 }
217 }
218
219 private:
220 class CharacteristicConfig {
221 public:
CharacteristicConfig(att::Handle handle)222 explicit CharacteristicConfig(att::Handle handle) : handle_(handle) {}
223 CharacteristicConfig(CharacteristicConfig&&) = default;
224 CharacteristicConfig& operator=(CharacteristicConfig&&) = default;
225
226 // The characteristic handle.
handle() const227 att::Handle handle() const { return handle_; }
228
Get(PeerId peer_id)229 uint16_t Get(PeerId peer_id) {
230 auto iter = client_states_.find(peer_id);
231
232 // If a configuration doesn't exist for |peer_id| then return the default
233 // value.
234 if (iter == client_states_.end())
235 return 0;
236
237 return iter->second;
238 }
239
Set(PeerId peer_id,uint16_t value)240 void Set(PeerId peer_id, uint16_t value) {
241 client_states_[peer_id] = value;
242 }
243
Erase(PeerId peer_id)244 void Erase(PeerId peer_id) { client_states_.erase(peer_id); }
245
246 private:
247 att::Handle handle_;
248 std::unordered_map<PeerId, uint16_t> client_states_;
249
250 BT_DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(CharacteristicConfig);
251 };
252
253 // Called when a read request is performed on a CCC descriptor belonging to
254 // the characteristic identified by |chrc_id|.
OnReadCCC(IdType chrc_id,PeerId peer_id,att::Handle handle,uint16_t offset,att::Attribute::ReadResultCallback result_cb)255 void OnReadCCC(IdType chrc_id,
256 PeerId peer_id,
257 att::Handle handle,
258 uint16_t offset,
259 att::Attribute::ReadResultCallback result_cb) {
260 uint16_t value = 0;
261 auto iter = chrc_configs_.find(chrc_id);
262 if (iter != chrc_configs_.end()) {
263 value = iter->second.Get(peer_id);
264 }
265
266 value = htole16(value);
267 result_cb(
268 fit::ok(),
269 BufferView(reinterpret_cast<const uint8_t*>(&value), sizeof(value)));
270 }
271
272 // Called when a write request is performed on a CCC descriptor belonging to
273 // the characteristic identified by |chrc_id|.
OnWriteCCC(IdType chrc_id,uint8_t chrc_props,PeerId peer_id,att::Handle handle,uint16_t offset,const ByteBuffer & value,att::Attribute::WriteResultCallback result_cb)274 void OnWriteCCC(IdType chrc_id,
275 uint8_t chrc_props,
276 PeerId peer_id,
277 att::Handle handle,
278 uint16_t offset,
279 const ByteBuffer& value,
280 att::Attribute::WriteResultCallback result_cb) {
281 if (offset != 0u) {
282 result_cb(fit::error(att::ErrorCode::kInvalidOffset));
283 return;
284 }
285
286 if (value.size() != sizeof(uint16_t)) {
287 result_cb(fit::error(att::ErrorCode::kInvalidAttributeValueLength));
288 return;
289 }
290
291 uint16_t ccc_value = le16toh(value.To<uint16_t>());
292 if (ccc_value > (kCCCNotificationBit | kCCCIndicationBit)) {
293 result_cb(fit::error(att::ErrorCode::kInvalidPDU));
294 return;
295 }
296
297 bool notify = ccc_value & kCCCNotificationBit;
298 bool indicate = ccc_value & kCCCIndicationBit;
299
300 if ((notify && !(chrc_props & Property::kNotify)) ||
301 (indicate && !(chrc_props & Property::kIndicate))) {
302 result_cb(fit::error(att::ErrorCode::kWriteNotPermitted));
303 return;
304 }
305
306 auto iter = chrc_configs_.find(chrc_id);
307 if (iter == chrc_configs_.end()) {
308 auto result_pair =
309 chrc_configs_.emplace(chrc_id, CharacteristicConfig(handle));
310 iter = result_pair.first;
311 }
312
313 // Send a reply back.
314 result_cb(fit::ok());
315
316 uint16_t current_value = iter->second.Get(peer_id);
317 iter->second.Set(peer_id, ccc_value);
318
319 if (current_value != ccc_value) {
320 ccc_callback_(id_, chrc_id, peer_id, notify, indicate);
321 }
322 }
323
AddCharacteristic(att::AttributeGrouping * grouping,CharacteristicPtr chrc)324 void AddCharacteristic(att::AttributeGrouping* grouping,
325 CharacteristicPtr chrc) {
326 // Set up the characteristic callbacks.
327 // TODO(armansito): Consider tracking a transaction timeout here
328 // (fxbug.dev/42142121).
329 IdType id = chrc->id();
330 uint8_t props = chrc->properties();
331 uint16_t ext_props = chrc->extended_properties();
332 auto self = weak_self_.GetWeakPtr();
333
334 auto read_handler = [self, id, props](PeerId peer_id,
335 att::Handle handle,
336 uint16_t offset,
337 auto result_cb) {
338 if (!self.is_alive()) {
339 result_cb(fit::error(att::ErrorCode::kUnlikelyError), BufferView());
340 return;
341 }
342
343 // ATT permissions checks passed if we got here; also check the
344 // characteristic property.
345 if (!(props & Property::kRead)) {
346 // TODO(armansito): Return kRequestNotSupported?
347 result_cb(fit::error(att::ErrorCode::kReadNotPermitted), BufferView());
348 return;
349 }
350
351 self->read_handler_(peer_id, self->id_, id, offset, std::move(result_cb));
352 };
353
354 auto write_handler = [self, id, props](PeerId peer_id,
355 att::Handle handle,
356 uint16_t offset,
357 const auto& value,
358 auto result_cb) {
359 if (!self.is_alive()) {
360 if (result_cb)
361 result_cb(fit::error(att::ErrorCode::kUnlikelyError));
362 return;
363 }
364
365 // If |result_cb| was provided, then this is a write request and the
366 // characteristic must support the "write" procedure.
367 if (result_cb && !(props & Property::kWrite)) {
368 // TODO(armansito): Return kRequestNotSupported?
369 result_cb(fit::error(att::ErrorCode::kWriteNotPermitted));
370 return;
371 }
372
373 if (!result_cb && !(props & Property::kWriteWithoutResponse))
374 return;
375
376 self->write_handler_(
377 peer_id, self->id_, id, offset, value, std::move(result_cb));
378 };
379
380 att::Handle chrc_handle = InsertCharacteristicAttributes(
381 grouping, *chrc, std::move(read_handler), std::move(write_handler));
382
383 if (props & Property::kNotify || props & Property::kIndicate) {
384 AddCCCDescriptor(grouping, *chrc, chrc_handle);
385 }
386
387 if (ext_props) {
388 auto* decl_attr = grouping->AddAttribute(
389 types::kCharacteristicExtProperties,
390 att::AccessRequirements(
391 /*encryption=*/false,
392 /*authentication=*/false,
393 /*authorization=*/false), // read (no security)
394 att::AccessRequirements()); // write (not allowed)
395 BT_DEBUG_ASSERT(decl_attr);
396 decl_attr->SetValue(StaticByteBuffer(
397 (uint8_t)(ext_props & 0x00FF), (uint8_t)((ext_props & 0xFF00) >> 8)));
398 }
399
400 // TODO(armansito): Inject a SCC descriptor if the characteristic has the
401 // broadcast property and if we ever support configured broadcasts.
402
403 // Sort descriptors by UUID size. This is not required by the specification
404 // but we do this to return as many descriptors as possible in a ATT Find
405 // Information response.
406 auto descs = chrc->ReleaseDescriptors();
407 std::sort(descs.begin(),
408 descs.end(),
409 [](const auto& desc_ptr1, const auto& desc_ptr2) {
410 return desc_ptr1->type().CompactSize(/*allow_32bit=*/false) <
411 desc_ptr2->type().CompactSize(/*allow_32bit=*/false);
412 });
413 for (auto& desc : descs) {
414 AddDescriptor(grouping, std::move(desc));
415 }
416 }
417
AddDescriptor(att::AttributeGrouping * grouping,DescriptorPtr desc)418 void AddDescriptor(att::AttributeGrouping* grouping, DescriptorPtr desc) {
419 auto self = weak_self_.GetWeakPtr();
420 auto read_handler = [self, id = desc->id()](PeerId peer_id,
421 att::Handle handle,
422 uint16_t offset,
423 auto result_cb) {
424 if (!self.is_alive()) {
425 result_cb(fit::error(att::ErrorCode::kUnlikelyError), BufferView());
426 return;
427 }
428
429 self->read_handler_(peer_id, self->id_, id, offset, std::move(result_cb));
430 };
431
432 auto write_handler = [self, id = desc->id()](PeerId peer_id,
433 att::Handle handle,
434 uint16_t offset,
435 const auto& value,
436 auto result_cb) {
437 // Descriptors cannot be written using the "write without response"
438 // procedure.
439 if (!result_cb)
440 return;
441
442 if (!self.is_alive()) {
443 result_cb(fit::error(att::ErrorCode::kUnlikelyError));
444 return;
445 }
446
447 self->write_handler_(
448 peer_id, self->id_, id, offset, value, std::move(result_cb));
449 };
450
451 InsertDescriptorAttribute(grouping,
452 desc->type(),
453 desc->read_permissions(),
454 desc->write_permissions(),
455 std::move(read_handler),
456 std::move(write_handler));
457 }
458
AddCCCDescriptor(att::AttributeGrouping * grouping,const Characteristic & chrc,att::Handle chrc_handle)459 void AddCCCDescriptor(att::AttributeGrouping* grouping,
460 const Characteristic& chrc,
461 att::Handle chrc_handle) {
462 BT_DEBUG_ASSERT(chrc.update_permissions().allowed());
463
464 // Readable with no authentication or authorization (Vol 3, Part G,
465 // 3.3.3.3). We let the service determine the encryption permission.
466 att::AccessRequirements read_reqs(
467 chrc.update_permissions().encryption_required(),
468 /*authentication=*/false,
469 /*authorization=*/false);
470
471 IdType id = chrc.id();
472 auto self = weak_self_.GetWeakPtr();
473
474 auto read_handler = [self, id, chrc_handle](const auto& peer_id,
475 att::Handle handle,
476 uint16_t offset,
477 auto result_cb) {
478 if (!self.is_alive()) {
479 result_cb(fit::error(att::ErrorCode::kUnlikelyError), BufferView());
480 return;
481 }
482
483 self->OnReadCCC(id, peer_id, chrc_handle, offset, std::move(result_cb));
484 };
485
486 auto write_handler = [self, id, chrc_handle, props = chrc.properties()](
487 const auto& peer_id,
488 att::Handle handle,
489 uint16_t offset,
490 const auto& value,
491 auto result_cb) {
492 if (!self.is_alive()) {
493 result_cb(fit::error(att::ErrorCode::kUnlikelyError));
494 return;
495 }
496
497 self->OnWriteCCC(
498 id, props, peer_id, chrc_handle, offset, value, std::move(result_cb));
499 };
500
501 // The write permission is determined by the service.
502 InsertDescriptorAttribute(grouping,
503 types::kClientCharacteristicConfig,
504 read_reqs,
505 chrc.update_permissions(),
506 std::move(read_handler),
507 std::move(write_handler));
508 }
509
510 IdType id_;
511 att::Handle start_handle_;
512 att::Handle end_handle_;
513 ReadHandler read_handler_;
514 WriteHandler write_handler_;
515 ClientConfigCallback ccc_callback_;
516
517 // Characteristic configuration states.
518 // TODO(armansito): Add a mechanism to persist client configuration for bonded
519 // devices.
520 std::unordered_map<IdType, CharacteristicConfig> chrc_configs_;
521
522 WeakSelf<ServiceData> weak_self_;
523
524 BT_DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(ServiceData);
525 };
526
LocalServiceManager()527 LocalServiceManager::LocalServiceManager()
528 : WeakSelf(this),
529 db_(std::make_unique<att::Database>()),
530 next_service_id_(1ull) {
531 BT_DEBUG_ASSERT(db_);
532 }
533
534 LocalServiceManager::~LocalServiceManager() = default;
535
RegisterService(ServicePtr service,ReadHandler read_handler,WriteHandler write_handler,ClientConfigCallback ccc_callback)536 IdType LocalServiceManager::RegisterService(ServicePtr service,
537 ReadHandler read_handler,
538 WriteHandler write_handler,
539 ClientConfigCallback ccc_callback) {
540 BT_DEBUG_ASSERT(service);
541 BT_DEBUG_ASSERT(read_handler);
542 BT_DEBUG_ASSERT(write_handler);
543 BT_DEBUG_ASSERT(ccc_callback);
544
545 if (services_.find(next_service_id_) != services_.end()) {
546 bt_log(TRACE, "gatt", "server: Ran out of service IDs");
547 return kInvalidId;
548 }
549
550 size_t attr_count;
551 if (!ValidateService(*service, &attr_count))
552 return kInvalidId;
553
554 // GATT does not support 32-bit UUIDs.
555 const BufferView service_decl_value =
556 service->type().CompactView(/*allow_32bit=*/false);
557
558 // TODO(armansito): Cluster services with 16-bit and 128-bit together inside
559 // |db_| (Vol 3, Part G, 3.1).
560
561 att::AttributeGrouping* grouping = db_->NewGrouping(
562 service->primary() ? types::kPrimaryService : types::kSecondaryService,
563 attr_count,
564 service_decl_value);
565 if (!grouping) {
566 bt_log(DEBUG,
567 "gatt",
568 "server: Failed to allocate attribute grouping for service");
569 return kInvalidId;
570 }
571
572 // Creating a ServiceData will populate the attribute grouping.
573 auto service_data = std::make_unique<ServiceData>(next_service_id_,
574 grouping,
575 service.get(),
576 std::move(read_handler),
577 std::move(write_handler),
578 std::move(ccc_callback));
579 BT_DEBUG_ASSERT(grouping->complete());
580 grouping->set_active(true);
581
582 // TODO(armansito): Handle potential 64-bit unsigned overflow?
583 IdType id = next_service_id_++;
584
585 services_[id] = std::move(service_data);
586 if (service_changed_callback_) {
587 service_changed_callback_(
588 id, grouping->start_handle(), grouping->end_handle());
589 }
590
591 return id;
592 }
593
UnregisterService(IdType service_id)594 bool LocalServiceManager::UnregisterService(IdType service_id) {
595 auto iter = services_.find(service_id);
596 if (iter == services_.end())
597 return false;
598
599 const att::Handle start_handle = iter->second->start_handle();
600 const att::Handle end_handle = iter->second->end_handle();
601 db_->RemoveGrouping(start_handle);
602 services_.erase(iter);
603
604 if (service_changed_callback_) {
605 service_changed_callback_(service_id, start_handle, end_handle);
606 }
607 return true;
608 }
609
GetCharacteristicConfig(IdType service_id,IdType chrc_id,PeerId peer_id,ClientCharacteristicConfig * out_config)610 bool LocalServiceManager::GetCharacteristicConfig(
611 IdType service_id,
612 IdType chrc_id,
613 PeerId peer_id,
614 ClientCharacteristicConfig* out_config) {
615 BT_DEBUG_ASSERT(out_config);
616
617 auto iter = services_.find(service_id);
618 if (iter == services_.end())
619 return false;
620
621 return iter->second->GetCharacteristicConfig(chrc_id, peer_id, out_config);
622 }
623
DisconnectClient(PeerId peer_id)624 void LocalServiceManager::DisconnectClient(PeerId peer_id) {
625 for (auto& id_service_pair : services_) {
626 id_service_pair.second->DisconnectClient(peer_id);
627 }
628 }
629
630 } // namespace bt::gatt
631