1 // Copyright 2024 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/fuchsia/host/fidl/gatt2_remote_service_server.h"
16
17 #include <utility>
18
19 #include "pw_bluetooth_sapphire/fuchsia/host/fidl/helpers.h"
20 #include "pw_bluetooth_sapphire/fuchsia/host/fidl/measure_tape/hlcpp_measure_tape_for_read_by_type_result.h"
21 #include "pw_bluetooth_sapphire/internal/host/att/att.h"
22 #include "pw_bluetooth_sapphire/internal/host/common/identifier.h"
23
24 namespace fbg = fuchsia::bluetooth::gatt2;
25 namespace measure_fbg = measure_tape::fuchsia::bluetooth::gatt2;
26
27 namespace bthost {
28 namespace {
29
MakeStatusCallback(bt::PeerId peer_id,const char * request_name,fbg::Handle fidl_handle,fit::function<void (fpromise::result<void,fbg::Error>)> callback)30 bt::att::ResultFunction<> MakeStatusCallback(
31 bt::PeerId peer_id,
32 const char* request_name,
33 fbg::Handle fidl_handle,
34 fit::function<void(fpromise::result<void, fbg::Error>)> callback) {
35 return [peer_id, fidl_handle, callback = std::move(callback), request_name](
36 bt::att::Result<> status) {
37 if (bt_is_error(status,
38 INFO,
39 "fidl",
40 "%s: error (peer: %s, handle: 0x%lX)",
41 request_name,
42 bt_str(peer_id),
43 fidl_handle.value)) {
44 callback(fpromise::error(
45 fidl_helpers::AttErrorToGattFidlError(status.error_value())));
46 return;
47 }
48
49 callback(fpromise::ok());
50 };
51 }
52
CharacteristicToFidl(const bt::gatt::CharacteristicData & characteristic,const std::map<bt::gatt::DescriptorHandle,bt::gatt::DescriptorData> & descriptors)53 fbg::Characteristic CharacteristicToFidl(
54 const bt::gatt::CharacteristicData& characteristic,
55 const std::map<bt::gatt::DescriptorHandle, bt::gatt::DescriptorData>&
56 descriptors) {
57 fbg::Characteristic fidl_char;
58 fidl_char.set_handle(fbg::Handle{characteristic.value_handle});
59 fidl_char.set_type(fuchsia::bluetooth::Uuid{characteristic.type.value()});
60
61 // The FIDL property bitfield combines the properties and extended properties
62 // bits. We mask away the kExtendedProperties property.
63 constexpr uint8_t kRemoveExtendedPropertiesMask = 0x7F;
64 fbg::CharacteristicPropertyBits fidl_properties =
65 static_cast<fbg::CharacteristicPropertyBits>(
66 characteristic.properties & kRemoveExtendedPropertiesMask);
67 if (characteristic.extended_properties) {
68 if (*characteristic.extended_properties &
69 bt::gatt::ExtendedProperty::kReliableWrite) {
70 fidl_properties |= fbg::CharacteristicPropertyBits::RELIABLE_WRITE;
71 }
72 if (*characteristic.extended_properties &
73 bt::gatt::ExtendedProperty::kWritableAuxiliaries) {
74 fidl_properties |= fbg::CharacteristicPropertyBits::WRITABLE_AUXILIARIES;
75 }
76 }
77 fidl_char.set_properties(fidl_properties);
78
79 if (!descriptors.empty()) {
80 std::vector<fbg::Descriptor> fidl_descriptors;
81 for (const auto& [handle, data] : descriptors) {
82 fbg::Descriptor fidl_descriptor;
83 fidl_descriptor.set_handle(fbg::Handle{handle.value});
84 fidl_descriptor.set_type(fuchsia::bluetooth::Uuid{data.type.value()});
85 fidl_descriptors.push_back(std::move(fidl_descriptor));
86 }
87 fidl_char.set_descriptors(std::move(fidl_descriptors));
88 }
89
90 return fidl_char;
91 }
92
93 // Returned result is supposed to match Read{Characteristic, Descriptor}Callback
94 // (result type is converted by FIDL move constructor).
95 [[nodiscard]] fpromise::result<::fuchsia::bluetooth::gatt2::ReadValue,
96 ::fuchsia::bluetooth::gatt2::Error>
ReadResultToFidl(bt::PeerId peer_id,fbg::Handle handle,bt::att::Result<> status,const bt::ByteBuffer & value,bool maybe_truncated,const char * request)97 ReadResultToFidl(bt::PeerId peer_id,
98 fbg::Handle handle,
99 bt::att::Result<> status,
100 const bt::ByteBuffer& value,
101 bool maybe_truncated,
102 const char* request) {
103 if (bt_is_error(status,
104 INFO,
105 "fidl",
106 "%s: error (peer: %s, handle: 0x%lX)",
107 request,
108 bt_str(peer_id),
109 handle.value)) {
110 return fpromise::error(
111 fidl_helpers::AttErrorToGattFidlError(status.error_value()));
112 }
113
114 fbg::ReadValue fidl_value;
115 fidl_value.set_handle(handle);
116 fidl_value.set_value(value.ToVector());
117 fidl_value.set_maybe_truncated(maybe_truncated);
118 return fpromise::ok(std::move(fidl_value));
119 }
120
FillInReadOptionsDefaults(fbg::ReadOptions & options)121 void FillInReadOptionsDefaults(fbg::ReadOptions& options) {
122 if (options.is_short_read()) {
123 return;
124 }
125 if (!options.long_read().has_offset()) {
126 options.long_read().set_offset(0);
127 }
128 if (!options.long_read().has_max_bytes()) {
129 options.long_read().set_max_bytes(fbg::MAX_VALUE_LENGTH);
130 }
131 }
132
FillInDefaultWriteOptions(fbg::WriteOptions & options)133 void FillInDefaultWriteOptions(fbg::WriteOptions& options) {
134 if (!options.has_write_mode()) {
135 *options.mutable_write_mode() = fbg::WriteMode::DEFAULT;
136 }
137 if (!options.has_offset()) {
138 *options.mutable_offset() = 0;
139 }
140 }
141
ReliableModeFromFidl(const fbg::WriteMode & mode)142 bt::gatt::ReliableMode ReliableModeFromFidl(const fbg::WriteMode& mode) {
143 return mode == fbg::WriteMode::RELIABLE ? bt::gatt::ReliableMode::kEnabled
144 : bt::gatt::ReliableMode::kDisabled;
145 }
146
147 } // namespace
148
Gatt2RemoteServiceServer(bt::gatt::RemoteService::WeakPtr service,bt::gatt::GATT::WeakPtr gatt,bt::PeerId peer_id,fidl::InterfaceRequest<fuchsia::bluetooth::gatt2::RemoteService> request)149 Gatt2RemoteServiceServer::Gatt2RemoteServiceServer(
150 bt::gatt::RemoteService::WeakPtr service,
151 bt::gatt::GATT::WeakPtr gatt,
152 bt::PeerId peer_id,
153 fidl::InterfaceRequest<fuchsia::bluetooth::gatt2::RemoteService> request)
154 : GattServerBase(std::move(gatt), this, std::move(request)),
155 service_(std::move(service)),
156 peer_id_(peer_id),
157 weak_self_(this) {}
158
~Gatt2RemoteServiceServer()159 Gatt2RemoteServiceServer::~Gatt2RemoteServiceServer() {
160 // Disable all notifications to prevent leaks.
161 for (auto& [_, notifier] : characteristic_notifiers_) {
162 service_->DisableNotifications(notifier.characteristic_handle,
163 notifier.handler_id,
164 /*status_callback=*/[](auto /*status*/) {});
165 }
166 characteristic_notifiers_.clear();
167 }
168
Close(zx_status_t status)169 void Gatt2RemoteServiceServer::Close(zx_status_t status) {
170 binding()->Close(status);
171 }
172
DiscoverCharacteristics(DiscoverCharacteristicsCallback callback)173 void Gatt2RemoteServiceServer::DiscoverCharacteristics(
174 DiscoverCharacteristicsCallback callback) {
175 auto res_cb = [callback = std::move(callback)](
176 bt::att::Result<> status,
177 const bt::gatt::CharacteristicMap& characteristics) {
178 if (status.is_error()) {
179 callback({});
180 return;
181 }
182
183 std::vector<fbg::Characteristic> fidl_characteristics;
184 for (const auto& [_, characteristic] : characteristics) {
185 const auto& [data, descriptors] = characteristic;
186 fidl_characteristics.push_back(CharacteristicToFidl(data, descriptors));
187 }
188 callback(std::move(fidl_characteristics));
189 };
190
191 service_->DiscoverCharacteristics(std::move(res_cb));
192 }
193
ReadByType(::fuchsia::bluetooth::Uuid uuid,ReadByTypeCallback callback)194 void Gatt2RemoteServiceServer::ReadByType(::fuchsia::bluetooth::Uuid uuid,
195 ReadByTypeCallback callback) {
196 service_->ReadByType(
197 fidl_helpers::UuidFromFidl(uuid),
198 [self = weak_self_.GetWeakPtr(),
199 cb = std::move(callback),
200 func = __FUNCTION__](
201 bt::att::Result<> status,
202 std::vector<bt::gatt::RemoteService::ReadByTypeResult> results) {
203 if (!self.is_alive()) {
204 return;
205 }
206
207 if (status == ToResult(bt::HostError::kInvalidParameters)) {
208 bt_log(WARN,
209 "fidl",
210 "%s: called with invalid parameters (peer: %s)",
211 func,
212 bt_str(self->peer_id_));
213 cb(fpromise::error(fbg::Error::INVALID_PARAMETERS));
214 return;
215 } else if (status.is_error()) {
216 cb(fpromise::error(fbg::Error::UNLIKELY_ERROR));
217 return;
218 }
219
220 const size_t kVectorOverhead =
221 sizeof(fidl_message_header_t) + sizeof(fidl_vector_t);
222 const size_t kMaxBytes = ZX_CHANNEL_MAX_MSG_BYTES - kVectorOverhead;
223 size_t bytes_used = 0;
224
225 std::vector<fuchsia::bluetooth::gatt2::ReadByTypeResult> fidl_results;
226 fidl_results.reserve(results.size());
227
228 for (const bt::gatt::RemoteService::ReadByTypeResult& result :
229 results) {
230 fuchsia::bluetooth::gatt2::ReadByTypeResult fidl_result;
231 fidl_result.set_handle(fbg::Handle{result.handle.value});
232 if (result.result.is_ok()) {
233 fbg::ReadValue read_value;
234 read_value.set_handle(fbg::Handle{result.handle.value});
235 read_value.set_value(result.result.value()->ToVector());
236 read_value.set_maybe_truncated(result.maybe_truncated);
237 fidl_result.set_value(std::move(read_value));
238 } else {
239 fidl_result.set_error(fidl_helpers::AttErrorToGattFidlError(
240 bt::att::Error(result.result.error_value())));
241 }
242
243 measure_fbg::Size result_size = measure_fbg::Measure(fidl_result);
244 PW_CHECK(result_size.num_handles == 0);
245 bytes_used += result_size.num_bytes;
246
247 if (bytes_used > kMaxBytes) {
248 cb(fpromise::error(
249 fuchsia::bluetooth::gatt2::Error::TOO_MANY_RESULTS));
250 return;
251 }
252
253 fidl_results.push_back(std::move(fidl_result));
254 }
255
256 cb(fpromise::ok(std::move(fidl_results)));
257 });
258 }
259
ReadCharacteristic(fbg::Handle fidl_handle,fbg::ReadOptions options,ReadCharacteristicCallback callback)260 void Gatt2RemoteServiceServer::ReadCharacteristic(
261 fbg::Handle fidl_handle,
262 fbg::ReadOptions options,
263 ReadCharacteristicCallback callback) {
264 if (!fidl_helpers::IsFidlGattHandleValid(fidl_handle)) {
265 callback(fpromise::error(fbg::Error::INVALID_HANDLE));
266 return;
267 }
268 bt::gatt::CharacteristicHandle handle(
269 static_cast<bt::att::Handle>(fidl_handle.value));
270
271 FillInReadOptionsDefaults(options);
272
273 const char* kRequestName = __FUNCTION__;
274 bt::gatt::RemoteService::ReadValueCallback read_cb =
275 [peer_id = peer_id_,
276 fidl_handle,
277 kRequestName,
278 callback = std::move(callback)](bt::att::Result<> status,
279 const bt::ByteBuffer& value,
280 bool maybe_truncated) {
281 callback(ReadResultToFidl(peer_id,
282 fidl_handle,
283 status,
284 value,
285 maybe_truncated,
286 kRequestName));
287 };
288
289 if (options.is_short_read()) {
290 service_->ReadCharacteristic(handle, std::move(read_cb));
291 return;
292 }
293
294 service_->ReadLongCharacteristic(handle,
295 options.long_read().offset(),
296 options.long_read().max_bytes(),
297 std::move(read_cb));
298 }
299
WriteCharacteristic(fbg::Handle fidl_handle,std::vector<uint8_t> value,fbg::WriteOptions options,WriteCharacteristicCallback callback)300 void Gatt2RemoteServiceServer::WriteCharacteristic(
301 fbg::Handle fidl_handle,
302 std::vector<uint8_t> value,
303 fbg::WriteOptions options,
304 WriteCharacteristicCallback callback) {
305 if (!fidl_helpers::IsFidlGattHandleValid(fidl_handle)) {
306 callback(fpromise::error(fbg::Error::INVALID_HANDLE));
307 return;
308 }
309 bt::gatt::CharacteristicHandle handle(
310 static_cast<bt::att::Handle>(fidl_handle.value));
311
312 FillInDefaultWriteOptions(options);
313
314 bt::att::ResultFunction<> write_cb = MakeStatusCallback(
315 peer_id_, __FUNCTION__, fidl_handle, std::move(callback));
316
317 if (options.write_mode() == fbg::WriteMode::WITHOUT_RESPONSE) {
318 if (options.offset() != 0) {
319 write_cb(bt::ToResult(bt::HostError::kInvalidParameters));
320 return;
321 }
322 service_->WriteCharacteristicWithoutResponse(
323 handle, std::move(value), std::move(write_cb));
324 return;
325 }
326
327 const uint16_t kMaxShortWriteValueLength =
328 service_->att_mtu() - sizeof(bt::att::OpCode) -
329 sizeof(bt::att::WriteRequestParams);
330 if (options.offset() == 0 &&
331 options.write_mode() == fbg::WriteMode::DEFAULT &&
332 value.size() <= kMaxShortWriteValueLength) {
333 service_->WriteCharacteristic(
334 handle, std::move(value), std::move(write_cb));
335 return;
336 }
337
338 service_->WriteLongCharacteristic(handle,
339 options.offset(),
340 std::move(value),
341 ReliableModeFromFidl(options.write_mode()),
342 std::move(write_cb));
343 }
344
ReadDescriptor(::fuchsia::bluetooth::gatt2::Handle fidl_handle,::fuchsia::bluetooth::gatt2::ReadOptions options,ReadDescriptorCallback callback)345 void Gatt2RemoteServiceServer::ReadDescriptor(
346 ::fuchsia::bluetooth::gatt2::Handle fidl_handle,
347 ::fuchsia::bluetooth::gatt2::ReadOptions options,
348 ReadDescriptorCallback callback) {
349 if (!fidl_helpers::IsFidlGattHandleValid(fidl_handle)) {
350 callback(fpromise::error(fbg::Error::INVALID_HANDLE));
351 return;
352 }
353 bt::gatt::DescriptorHandle handle(
354 static_cast<bt::att::Handle>(fidl_handle.value));
355
356 FillInReadOptionsDefaults(options);
357
358 const char* kRequestName = __FUNCTION__;
359 bt::gatt::RemoteService::ReadValueCallback read_cb =
360 [peer_id = peer_id_,
361 fidl_handle,
362 kRequestName,
363 callback = std::move(callback)](bt::att::Result<> status,
364 const bt::ByteBuffer& value,
365 bool maybe_truncated) {
366 callback(ReadResultToFidl(peer_id,
367 fidl_handle,
368 status,
369 value,
370 maybe_truncated,
371 kRequestName));
372 };
373
374 if (options.is_short_read()) {
375 service_->ReadDescriptor(handle, std::move(read_cb));
376 return;
377 }
378
379 service_->ReadLongDescriptor(handle,
380 options.long_read().offset(),
381 options.long_read().max_bytes(),
382 std::move(read_cb));
383 }
384
WriteDescriptor(fbg::Handle fidl_handle,std::vector<uint8_t> value,fbg::WriteOptions options,WriteDescriptorCallback callback)385 void Gatt2RemoteServiceServer::WriteDescriptor(
386 fbg::Handle fidl_handle,
387 std::vector<uint8_t> value,
388 fbg::WriteOptions options,
389 WriteDescriptorCallback callback) {
390 if (!fidl_helpers::IsFidlGattHandleValid(fidl_handle)) {
391 callback(fpromise::error(fbg::Error::INVALID_HANDLE));
392 return;
393 }
394 bt::gatt::DescriptorHandle handle(
395 static_cast<bt::att::Handle>(fidl_handle.value));
396
397 FillInDefaultWriteOptions(options);
398
399 bt::att::ResultFunction<> write_cb = MakeStatusCallback(
400 peer_id_, __FUNCTION__, fidl_handle, std::move(callback));
401
402 // WITHOUT_RESPONSE and RELIABLE write modes are not supported for
403 // descriptors.
404 if (options.write_mode() == fbg::WriteMode::WITHOUT_RESPONSE ||
405 options.write_mode() == fbg::WriteMode::RELIABLE) {
406 write_cb(bt::ToResult(bt::HostError::kInvalidParameters));
407 return;
408 }
409
410 const uint16_t kMaxShortWriteValueLength =
411 service_->att_mtu() - sizeof(bt::att::OpCode) -
412 sizeof(bt::att::WriteRequestParams);
413 if (options.offset() == 0 && value.size() <= kMaxShortWriteValueLength) {
414 service_->WriteDescriptor(handle, std::move(value), std::move(write_cb));
415 return;
416 }
417
418 service_->WriteLongDescriptor(
419 handle, options.offset(), std::move(value), std::move(write_cb));
420 }
421
RegisterCharacteristicNotifier(fbg::Handle fidl_handle,fidl::InterfaceHandle<fbg::CharacteristicNotifier> notifier_handle,RegisterCharacteristicNotifierCallback callback)422 void Gatt2RemoteServiceServer::RegisterCharacteristicNotifier(
423 fbg::Handle fidl_handle,
424 fidl::InterfaceHandle<fbg::CharacteristicNotifier> notifier_handle,
425 RegisterCharacteristicNotifierCallback callback) {
426 bt::gatt::CharacteristicHandle char_handle(
427 static_cast<bt::att::Handle>(fidl_handle.value));
428 NotifierId notifier_id = next_notifier_id_++;
429 auto self = weak_self_.GetWeakPtr();
430
431 auto value_cb = [self, notifier_id, fidl_handle](const bt::ByteBuffer& value,
432 bool maybe_truncated) {
433 if (!self.is_alive()) {
434 return;
435 }
436
437 auto notifier_iter = self->characteristic_notifiers_.find(notifier_id);
438 // The lower layers guarantee that the status callback is always invoked
439 // before sending notifications. Notifiers are only removed during
440 // destruction (addressed by previous `self` check) and in the
441 // `DisableNotifications` completion callback in
442 // `OnCharacteristicNotifierError`, so no notifications should be received
443 // after removing a notifier.
444 PW_CHECK(
445 notifier_iter != self->characteristic_notifiers_.end(),
446 "characteristic notification value received after notifier unregistered"
447 "(peer: %s, characteristic: 0x%lX) ",
448 bt_str(self->peer_id_),
449 fidl_handle.value);
450 CharacteristicNotifier& notifier = notifier_iter->second;
451
452 // The `- 1` is needed because there is one unacked notification that we've
453 // already sent to the client aside from the values in the queue.
454 if (notifier.queued_values.size() == kMaxPendingNotifierValues - 1) {
455 bt_log(WARN,
456 "fidl",
457 "GATT CharacteristicNotifier pending values limit reached, "
458 "closing protocol (peer: "
459 "%s, characteristic: %#.2x)",
460 bt_str(self->peer_id_),
461 notifier.characteristic_handle.value);
462 self->OnCharacteristicNotifierError(
463 notifier_id, notifier.characteristic_handle, notifier.handler_id);
464 return;
465 }
466
467 fbg::ReadValue fidl_value;
468 fidl_value.set_handle(fidl_handle);
469 fidl_value.set_value(value.ToVector());
470 fidl_value.set_maybe_truncated(maybe_truncated);
471
472 bt_log(TRACE,
473 "fidl",
474 "Queueing GATT notification value (characteristic: %#.2x)",
475 notifier.characteristic_handle.value);
476 notifier.queued_values.push(std::move(fidl_value));
477
478 self->MaybeNotifyNextValue(notifier_id);
479 };
480
481 auto status_cb = [self,
482 service = service_,
483 char_handle,
484 notifier_id,
485 notifier_handle = std::move(notifier_handle),
486 callback = std::move(callback)](
487 bt::att::Result<> status,
488 bt::gatt::IdType handler_id) mutable {
489 if (!self.is_alive()) {
490 if (status.is_ok()) {
491 // Disable this handler so it doesn't leak.
492 service->DisableNotifications(
493 char_handle, handler_id, [](auto /*status*/) {
494 // There is no notifier to clean up because the server has been
495 // destroyed.
496 });
497 }
498 return;
499 }
500
501 if (status.is_error()) {
502 callback(fpromise::error(
503 fidl_helpers::AttErrorToGattFidlError(status.error_value())));
504 return;
505 }
506
507 CharacteristicNotifier notifier{.handler_id = handler_id,
508 .characteristic_handle = char_handle,
509 .notifier = notifier_handle.Bind()};
510 auto [notifier_iter, emplaced] = self->characteristic_notifiers_.emplace(
511 notifier_id, std::move(notifier));
512 PW_CHECK(emplaced);
513
514 // When the client closes the protocol, unregister the notifier.
515 notifier_iter->second.notifier.set_error_handler(
516 [self, char_handle, handler_id, notifier_id](auto /*status*/) {
517 self->OnCharacteristicNotifierError(
518 notifier_id, char_handle, handler_id);
519 });
520
521 callback(fpromise::ok());
522 };
523
524 service_->EnableNotifications(
525 char_handle, std::move(value_cb), std::move(status_cb));
526 }
527
MaybeNotifyNextValue(NotifierId notifier_id)528 void Gatt2RemoteServiceServer::MaybeNotifyNextValue(NotifierId notifier_id) {
529 auto notifier_iter = characteristic_notifiers_.find(notifier_id);
530 if (notifier_iter == characteristic_notifiers_.end()) {
531 return;
532 }
533 CharacteristicNotifier& notifier = notifier_iter->second;
534
535 if (notifier.queued_values.empty()) {
536 return;
537 }
538
539 if (!notifier.last_value_ack) {
540 return;
541 }
542 notifier.last_value_ack = false;
543
544 fbg::ReadValue value = std::move(notifier.queued_values.front());
545 notifier.queued_values.pop();
546
547 bt_log(DEBUG,
548 "fidl",
549 "Sending GATT notification value (handle: 0x%lX)",
550 value.handle().value);
551 auto self = weak_self_.GetWeakPtr();
552 notifier.notifier->OnNotification(std::move(value), [self, notifier_id]() {
553 if (!self.is_alive()) {
554 return;
555 }
556
557 auto notifier_iter = self->characteristic_notifiers_.find(notifier_id);
558 if (notifier_iter == self->characteristic_notifiers_.end()) {
559 return;
560 }
561 notifier_iter->second.last_value_ack = true;
562 self->MaybeNotifyNextValue(notifier_id);
563 });
564 }
565
OnCharacteristicNotifierError(NotifierId notifier_id,bt::gatt::CharacteristicHandle char_handle,bt::gatt::IdType handler_id)566 void Gatt2RemoteServiceServer::OnCharacteristicNotifierError(
567 NotifierId notifier_id,
568 bt::gatt::CharacteristicHandle char_handle,
569 bt::gatt::IdType handler_id) {
570 auto self = weak_self_.GetWeakPtr();
571 service_->DisableNotifications(
572 char_handle, handler_id, [self, notifier_id](auto /*status*/) {
573 if (!self.is_alive()) {
574 return;
575 }
576 // Clear the notifier regardless of status. Wait until this callback is
577 // called in order to prevent the value callback from being called for
578 // an erased notifier.
579 self->characteristic_notifiers_.erase(notifier_id);
580 });
581 }
582
583 } // namespace bthost
584