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