• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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/hci/extended_low_energy_advertiser.h"
16 
17 #include "pw_bluetooth_sapphire/internal/host/hci-spec/util.h"
18 #include "pw_bluetooth_sapphire/internal/host/transport/transport.h"
19 
20 namespace bt::hci {
21 
ExtendedLowEnergyAdvertiser(hci::Transport::WeakPtr hci_ptr)22 ExtendedLowEnergyAdvertiser::ExtendedLowEnergyAdvertiser(
23     hci::Transport::WeakPtr hci_ptr)
24     : LowEnergyAdvertiser(std::move(hci_ptr)), weak_self_(this) {
25   auto self = weak_self_.GetWeakPtr();
26   set_terminated_event_handler_id_ =
27       hci()->command_channel()->AddLEMetaEventHandler(
28           hci_spec::kLEAdvertisingSetTerminatedSubeventCode,
29           [self](const EventPacket& event_packet) {
30             if (self.is_alive()) {
31               return self->OnAdvertisingSetTerminatedEvent(event_packet);
32             }
33 
34             return CommandChannel::EventCallbackResult::kRemove;
35           });
36 }
37 
~ExtendedLowEnergyAdvertiser()38 ExtendedLowEnergyAdvertiser::~ExtendedLowEnergyAdvertiser() {
39   // This object is probably being destroyed because the stack is shutting down,
40   // in which case the HCI layer may have already been destroyed.
41   if (!hci().is_alive() || !hci()->command_channel()) {
42     return;
43   }
44   hci()->command_channel()->RemoveEventHandler(
45       set_terminated_event_handler_id_);
46   // TODO(fxbug.dev/42063496): This will only cancel one advertisement, after
47   // which the SequentialCommandRunner will have been destroyed and no further
48   // commands will be sent.
49   StopAdvertising();
50 }
51 
BuildEnablePacket(const DeviceAddress & address,pw::bluetooth::emboss::GenericEnableParam enable)52 EmbossCommandPacket ExtendedLowEnergyAdvertiser::BuildEnablePacket(
53     const DeviceAddress& address,
54     pw::bluetooth::emboss::GenericEnableParam enable) {
55   // We only enable or disable a single address at a time. The multiply by 1 is
56   // set explicitly to show that data[] within
57   // LESetExtendedAdvertisingEnableData is of size 1.
58   constexpr size_t kPacketSize =
59       pw::bluetooth::emboss::LESetExtendedAdvertisingEnableCommand::
60           MinSizeInBytes() +
61       (1 * pw::bluetooth::emboss::LESetExtendedAdvertisingEnableData::
62                IntrinsicSizeInBytes());
63   auto packet = hci::EmbossCommandPacket::New<
64       pw::bluetooth::emboss::LESetExtendedAdvertisingEnableCommandWriter>(
65       hci_spec::kLESetExtendedAdvertisingEnable, kPacketSize);
66   auto packet_view = packet.view_t();
67   packet_view.enable().Write(enable);
68   packet_view.num_sets().Write(1);
69 
70   std::optional<hci_spec::AdvertisingHandle> handle =
71       advertising_handle_map_.GetHandle(address);
72   BT_ASSERT(handle);
73 
74   packet_view.data()[0].advertising_handle().Write(handle.value());
75   packet_view.data()[0].duration().Write(hci_spec::kNoAdvertisingDuration);
76   packet_view.data()[0].max_extended_advertising_events().Write(
77       hci_spec::kNoMaxExtendedAdvertisingEvents);
78 
79   return packet;
80 }
81 
82 CommandChannel::CommandPacketVariant
BuildSetAdvertisingParams(const DeviceAddress & address,pw::bluetooth::emboss::LEAdvertisingType type,pw::bluetooth::emboss::LEOwnAddressType own_address_type,AdvertisingIntervalRange interval)83 ExtendedLowEnergyAdvertiser::BuildSetAdvertisingParams(
84     const DeviceAddress& address,
85     pw::bluetooth::emboss::LEAdvertisingType type,
86     pw::bluetooth::emboss::LEOwnAddressType own_address_type,
87     AdvertisingIntervalRange interval) {
88   constexpr size_t kPacketSize = pw::bluetooth::emboss::
89       LESetExtendedAdvertisingParametersV1CommandView::SizeInBytes();
90   auto packet = hci::EmbossCommandPacket::New<
91       pw::bluetooth::emboss::LESetExtendedAdvertisingParametersV1CommandWriter>(
92       hci_spec::kLESetExtendedAdvertisingParameters, kPacketSize);
93   auto packet_view = packet.view_t();
94 
95   // advertising handle
96   std::optional<hci_spec::AdvertisingHandle> handle =
97       advertising_handle_map_.MapHandle(address);
98   if (!handle) {
99     bt_log(WARN,
100            "hci-le",
101            "could not allocate a new advertising handle for address: %s (all "
102            "in use)",
103            bt_str(address));
104     return std::unique_ptr<CommandPacket>();
105   }
106   packet_view.advertising_handle().Write(handle.value());
107 
108   // advertising event properties
109   std::optional<hci_spec::AdvertisingEventBits> bits =
110       hci_spec::AdvertisingTypeToEventBits(type);
111   if (!bits) {
112     bt_log(WARN,
113            "hci-le",
114            "could not generate event bits for type: %hhu",
115            static_cast<unsigned char>(type));
116     return std::unique_ptr<CommandPacket>();
117   }
118   uint16_t properties = bits.value();
119   packet_view.advertising_event_properties().BackingStorage().WriteUInt(
120       properties);
121 
122   // advertising interval, NOTE: LE advertising parameters allow for up to 3
123   // octets (10 ms to 10428 s) to configure an advertising interval. However, we
124   // expose only the recommended advertising interval configurations to users,
125   // as specified in the Bluetooth Spec Volume 3, Part C, Appendix A. These
126   // values are expressed as uint16_t so we simply copy them (taking care of
127   // endianness) into the 3 octets as is.
128   packet_view.primary_advertising_interval_min().Write(interval.min());
129   packet_view.primary_advertising_interval_max().Write(interval.max());
130 
131   // advertise on all channels
132   packet_view.primary_advertising_channel_map().channel_37().Write(true);
133   packet_view.primary_advertising_channel_map().channel_38().Write(true);
134   packet_view.primary_advertising_channel_map().channel_39().Write(true);
135 
136   packet_view.own_address_type().Write(own_address_type);
137   packet_view.advertising_filter_policy().Write(
138       pw::bluetooth::emboss::LEAdvertisingFilterPolicy::ALLOW_ALL);
139   packet_view.advertising_tx_power().Write(
140       hci_spec::kLEExtendedAdvertisingTxPowerNoPreference);
141   packet_view.scan_request_notification_enable().Write(
142       pw::bluetooth::emboss::GenericEnableParam::DISABLE);
143 
144   // TODO(fxbug.dev/42161929): using legacy PDUs requires advertisements on the
145   // LE 1M PHY.
146   packet_view.primary_advertising_phy().Write(
147       pw::bluetooth::emboss::LEPrimaryAdvertisingPHY::LE_1M);
148   packet_view.secondary_advertising_phy().Write(
149       pw::bluetooth::emboss::LESecondaryAdvertisingPHY::LE_1M);
150 
151   // Payload values were initialized to zero above. By not setting the values
152   // for the following fields, we are purposely ignoring them:
153   //
154   // advertising_sid: We use only legacy PDUs, the controller ignores this field
155   // in that case peer_address: We don't support directed advertising yet
156   // peer_address_type: We don't support directed advertising yet
157   // secondary_adv_max_skip: We use only legacy PDUs, the controller ignores
158   // this field in that case
159 
160   return packet;
161 }
162 
163 CommandChannel::CommandPacketVariant
BuildSetAdvertisingData(const DeviceAddress & address,const AdvertisingData & data,AdvFlags flags)164 ExtendedLowEnergyAdvertiser::BuildSetAdvertisingData(
165     const DeviceAddress& address, const AdvertisingData& data, AdvFlags flags) {
166   AdvertisingData adv_data;
167   data.Copy(&adv_data);
168   if (staged_advertising_parameters_.include_tx_power_level) {
169     adv_data.SetTxPower(staged_advertising_parameters_.selected_tx_power_level);
170   }
171   size_t block_size = adv_data.CalculateBlockSize(/*include_flags=*/true);
172 
173   size_t kPayloadSize =
174       pw::bluetooth::emboss::LESetExtendedAdvertisingDataCommandView::
175           MinSizeInBytes()
176               .Read() +
177       block_size;
178   auto packet = EmbossCommandPacket::New<
179       pw::bluetooth::emboss::LESetExtendedAdvertisingDataCommandWriter>(
180       hci_spec::kLESetExtendedAdvertisingData, kPayloadSize);
181   auto params = packet.view_t();
182 
183   // advertising handle
184   std::optional<hci_spec::AdvertisingHandle> handle =
185       advertising_handle_map_.GetHandle(address);
186   BT_ASSERT(handle);
187   params.advertising_handle().Write(handle.value());
188 
189   // TODO(fxbug.dev/42161929): We support only legacy PDUs and do not support
190   // fragmented extended advertising data at this time.
191   params.operation().Write(
192       pw::bluetooth::emboss::LESetExtendedAdvDataOp::COMPLETE);
193   params.fragment_preference().Write(
194       pw::bluetooth::emboss::LEExtendedAdvFragmentPreference::
195           SHOULD_NOT_FRAGMENT);
196 
197   // advertising data
198   params.advertising_data_length().Write(static_cast<uint8_t>(block_size));
199   MutableBufferView data_view(params.advertising_data().BackingStorage().data(),
200                               params.advertising_data_length().Read());
201   adv_data.WriteBlock(&data_view, flags);
202 
203   return packet;
204 }
205 
206 CommandChannel::CommandPacketVariant
BuildUnsetAdvertisingData(const DeviceAddress & address)207 ExtendedLowEnergyAdvertiser::BuildUnsetAdvertisingData(
208     const DeviceAddress& address) {
209   constexpr size_t kPacketSize =
210       pw::bluetooth::emboss::LESetExtendedAdvertisingDataCommandView::
211           MinSizeInBytes()
212               .Read();
213   auto packet = EmbossCommandPacket::New<
214       pw::bluetooth::emboss::LESetExtendedAdvertisingDataCommandWriter>(
215       hci_spec::kLESetExtendedAdvertisingData, kPacketSize);
216   auto payload = packet.view_t();
217 
218   // advertising handle
219   std::optional<hci_spec::AdvertisingHandle> handle =
220       advertising_handle_map_.GetHandle(address);
221   BT_ASSERT(handle);
222   payload.advertising_handle().Write(handle.value());
223 
224   // TODO(fxbug.dev/42161929): We support only legacy PDUs and do not support
225   // fragmented extended advertising data at this time.
226   payload.operation().Write(
227       pw::bluetooth::emboss::LESetExtendedAdvDataOp::COMPLETE);
228   payload.fragment_preference().Write(
229       pw::bluetooth::emboss::LEExtendedAdvFragmentPreference::
230           SHOULD_NOT_FRAGMENT);
231   payload.advertising_data_length().Write(0);
232 
233   return packet;
234 }
235 
236 CommandChannel::CommandPacketVariant
BuildSetScanResponse(const DeviceAddress & address,const AdvertisingData & data)237 ExtendedLowEnergyAdvertiser::BuildSetScanResponse(const DeviceAddress& address,
238                                                   const AdvertisingData& data) {
239   AdvertisingData scan_rsp;
240   data.Copy(&scan_rsp);
241   if (staged_advertising_parameters_.include_tx_power_level) {
242     scan_rsp.SetTxPower(staged_advertising_parameters_.selected_tx_power_level);
243   }
244   size_t block_size = scan_rsp.CalculateBlockSize();
245 
246   size_t kPayloadSize =
247       pw::bluetooth::emboss::LESetExtendedScanResponseDataCommandView::
248           MinSizeInBytes()
249               .Read() +
250       block_size;
251   auto packet = EmbossCommandPacket::New<
252       pw::bluetooth::emboss::LESetExtendedScanResponseDataCommandWriter>(
253       hci_spec::kLESetExtendedScanResponseData, kPayloadSize);
254   auto params = packet.view_t();
255 
256   // advertising handle
257   std::optional<hci_spec::AdvertisingHandle> handle =
258       advertising_handle_map_.GetHandle(address);
259   BT_ASSERT(handle);
260   params.advertising_handle().Write(handle.value());
261 
262   // TODO(fxbug.dev/42161929): We support only legacy PDUs and do not support
263   // fragmented extended advertising data at this time.
264   params.operation().Write(
265       pw::bluetooth::emboss::LESetExtendedAdvDataOp::COMPLETE);
266   params.fragment_preference().Write(
267       pw::bluetooth::emboss::LEExtendedAdvFragmentPreference::
268           SHOULD_NOT_FRAGMENT);
269 
270   // scan response data
271   params.scan_response_data_length().Write(static_cast<uint8_t>(block_size));
272   MutableBufferView scan_rsp_view(
273       params.scan_response_data().BackingStorage().data(),
274       params.scan_response_data_length().Read());
275   scan_rsp.WriteBlock(&scan_rsp_view, std::nullopt);
276 
277   return packet;
278 }
279 
280 CommandChannel::CommandPacketVariant
BuildUnsetScanResponse(const DeviceAddress & address)281 ExtendedLowEnergyAdvertiser::BuildUnsetScanResponse(
282     const DeviceAddress& address) {
283   constexpr size_t kPacketSize =
284       pw::bluetooth::emboss::LESetExtendedScanResponseDataCommandView::
285           MinSizeInBytes()
286               .Read();
287   auto packet = EmbossCommandPacket::New<
288       pw::bluetooth::emboss::LESetExtendedScanResponseDataCommandWriter>(
289       hci_spec::kLESetExtendedScanResponseData, kPacketSize);
290   auto payload = packet.view_t();
291 
292   // advertising handle
293   std::optional<hci_spec::AdvertisingHandle> handle =
294       advertising_handle_map_.GetHandle(address);
295   BT_ASSERT(handle);
296   payload.advertising_handle().Write(handle.value());
297 
298   // TODO(fxbug.dev/42161929): We support only legacy PDUs and do not support
299   // fragmented extended advertising data at this time.
300   payload.operation().Write(
301       pw::bluetooth::emboss::LESetExtendedAdvDataOp::COMPLETE);
302   payload.fragment_preference().Write(
303       pw::bluetooth::emboss::LEExtendedAdvFragmentPreference::
304           SHOULD_NOT_FRAGMENT);
305   payload.scan_response_data_length().Write(0);
306 
307   return packet;
308 }
309 
BuildRemoveAdvertisingSet(const DeviceAddress & address)310 EmbossCommandPacket ExtendedLowEnergyAdvertiser::BuildRemoveAdvertisingSet(
311     const DeviceAddress& address) {
312   std::optional<hci_spec::AdvertisingHandle> handle =
313       advertising_handle_map_.GetHandle(address);
314   BT_ASSERT(handle);
315   auto packet = hci::EmbossCommandPacket::New<
316       pw::bluetooth::emboss::LERemoveAdvertisingSetCommandWriter>(
317       hci_spec::kLERemoveAdvertisingSet);
318   auto packet_view = packet.view_t();
319   packet_view.advertising_handle().Write(handle.value());
320 
321   return packet;
322 }
323 
OnSetAdvertisingParamsComplete(const EventPacket & event)324 void ExtendedLowEnergyAdvertiser::OnSetAdvertisingParamsComplete(
325     const EventPacket& event) {
326   BT_ASSERT(event.event_code() == hci_spec::kCommandCompleteEventCode);
327   BT_ASSERT(
328       event.params<hci_spec::CommandCompleteEventParams>().command_opcode ==
329       hci_spec::kLESetExtendedAdvertisingParameters);
330 
331   Result<> result = event.ToResult();
332   if (bt_is_error(result,
333                   WARN,
334                   "hci-le",
335                   "set advertising parameters, error received: %s",
336                   bt_str(result))) {
337     return;  // full error handling done in super class, can just return here
338   }
339 
340   auto params = event.return_params<
341       hci_spec::LESetExtendedAdvertisingParametersReturnParams>();
342   BT_ASSERT(params);
343 
344   if (staged_advertising_parameters_.include_tx_power_level) {
345     staged_advertising_parameters_.selected_tx_power_level =
346         params->selected_tx_power;
347   }
348 }
349 
StartAdvertising(const DeviceAddress & address,const AdvertisingData & data,const AdvertisingData & scan_rsp,AdvertisingOptions options,ConnectionCallback connect_callback,ResultFunction<> result_callback)350 void ExtendedLowEnergyAdvertiser::StartAdvertising(
351     const DeviceAddress& address,
352     const AdvertisingData& data,
353     const AdvertisingData& scan_rsp,
354     AdvertisingOptions options,
355     ConnectionCallback connect_callback,
356     ResultFunction<> result_callback) {
357   // if there is an operation currently in progress, enqueue this operation and
358   // we will get to it the next time we have a chance
359   if (!hci_cmd_runner().IsReady()) {
360     bt_log(INFO,
361            "hci-le",
362            "hci cmd runner not ready, queuing advertisement commands for now");
363 
364     AdvertisingData copied_data;
365     data.Copy(&copied_data);
366 
367     AdvertisingData copied_scan_rsp;
368     scan_rsp.Copy(&copied_scan_rsp);
369 
370     op_queue_.push([this,
371                     address,
372                     data = std::move(copied_data),
373                     scan_rsp = std::move(copied_scan_rsp),
374                     options,
375                     conn_cb = std::move(connect_callback),
376                     result_cb = std::move(result_callback)]() mutable {
377       StartAdvertising(address,
378                        data,
379                        scan_rsp,
380                        options,
381                        std::move(conn_cb),
382                        std::move(result_cb));
383     });
384 
385     return;
386   }
387 
388   fit::result<HostError> result =
389       CanStartAdvertising(address, data, scan_rsp, options);
390   if (result.is_error()) {
391     result_callback(ToResult(result.error_value()));
392     return;
393   }
394 
395   if (IsAdvertising(address)) {
396     bt_log(DEBUG,
397            "hci-le",
398            "updating existing advertisement for %s",
399            bt_str(address));
400   }
401 
402   std::memset(&staged_advertising_parameters_,
403               0,
404               sizeof(staged_advertising_parameters_));
405   staged_advertising_parameters_.include_tx_power_level =
406       options.include_tx_power_level;
407 
408   // Core Spec, Volume 4, Part E, Section 7.8.58: "the number of advertising
409   // sets that can be supported is not fixed and the Controller can change it at
410   // any time. The memory used to store advertising sets can also be used for
411   // other purposes."
412   //
413   // Depending on the memory profile of the controller, a new advertising set
414   // may or may not be accepted. We could use
415   // HCI_LE_Read_Number_of_Supported_Advertising_Sets to check if the controller
416   // has space for another advertising set. However, the value may change after
417   // the read and before the addition of the advertising set. Furthermore,
418   // sending an extra HCI command increases the latency of our stack. Instead,
419   // we simply attempt to add. If the controller is unable to support another
420   // advertising set, it will respond with a memory capacity exceeded error.
421   StartAdvertisingInternal(address,
422                            data,
423                            scan_rsp,
424                            options.interval,
425                            options.flags,
426                            std::move(connect_callback),
427                            std::move(result_callback));
428 }
429 
StopAdvertising()430 void ExtendedLowEnergyAdvertiser::StopAdvertising() {
431   LowEnergyAdvertiser::StopAdvertising();
432   advertising_handle_map_.Clear();
433 
434   // std::queue doesn't have a clear method so we have to resort to this
435   // tomfoolery :(
436   decltype(op_queue_) empty;
437   std::swap(op_queue_, empty);
438 }
439 
StopAdvertising(const DeviceAddress & address)440 void ExtendedLowEnergyAdvertiser::StopAdvertising(
441     const DeviceAddress& address) {
442   // if there is an operation currently in progress, enqueue this operation and
443   // we will get to it the next time we have a chance
444   if (!hci_cmd_runner().IsReady()) {
445     bt_log(
446         INFO,
447         "hci-le",
448         "hci cmd runner not ready, queueing stop advertising command for now");
449     op_queue_.push([this, address]() { StopAdvertising(address); });
450     return;
451   }
452 
453   LowEnergyAdvertiser::StopAdvertisingInternal(address);
454   advertising_handle_map_.RemoveAddress(address);
455 }
456 
OnIncomingConnection(hci_spec::ConnectionHandle handle,pw::bluetooth::emboss::ConnectionRole role,const DeviceAddress & peer_address,const hci_spec::LEConnectionParameters & conn_params)457 void ExtendedLowEnergyAdvertiser::OnIncomingConnection(
458     hci_spec::ConnectionHandle handle,
459     pw::bluetooth::emboss::ConnectionRole role,
460     const DeviceAddress& peer_address,
461     const hci_spec::LEConnectionParameters& conn_params) {
462   // Core Spec Volume 4, Part E, Section 7.8.56: Incoming connections to LE
463   // Extended Advertising occur through two events: HCI_LE_Connection_Complete
464   // and HCI_LE_Advertising_Set_Terminated. This method is called as a result of
465   // the HCI_LE_Connection_Complete event. At this point, we only have a
466   // connection handle but don't know the locally advertised address that the
467   // connection is for. Until we receive the HCI_LE_Advertising_Set_Terminated
468   // event, we stage these parameters.
469   staged_connections_map_[handle] = {role, peer_address, conn_params};
470 }
471 
472 // The HCI_LE_Advertising_Set_Terminated event contains the mapping between
473 // connection handle and advertising handle. After the
474 // HCI_LE_Advertising_Set_Terminated event, we have all the information
475 // necessary to create a connection object within the Host layer.
476 CommandChannel::EventCallbackResult
OnAdvertisingSetTerminatedEvent(const EventPacket & event)477 ExtendedLowEnergyAdvertiser::OnAdvertisingSetTerminatedEvent(
478     const EventPacket& event) {
479   BT_ASSERT(event.event_code() == hci_spec::kLEMetaEventCode);
480   BT_ASSERT(event.params<hci_spec::LEMetaEventParams>().subevent_code ==
481             hci_spec::kLEAdvertisingSetTerminatedSubeventCode);
482 
483   Result<> result = event.ToResult();
484   if (bt_is_error(result,
485                   ERROR,
486                   "hci-le",
487                   "advertising set terminated event, error received %s",
488                   bt_str(result))) {
489     return CommandChannel::EventCallbackResult::kContinue;
490   }
491 
492   auto params = event.subevent_params<
493       hci_spec::LEAdvertisingSetTerminatedSubeventParams>();
494   BT_ASSERT(params);
495 
496   hci_spec::ConnectionHandle connection_handle = params->connection_handle;
497   auto staged_parameters_node =
498       staged_connections_map_.extract(connection_handle);
499 
500   if (staged_parameters_node.empty()) {
501     bt_log(ERROR,
502            "hci-le",
503            "advertising set terminated event, staged params not available "
504            "(handle: %d)",
505            params->adv_handle);
506     return CommandChannel::EventCallbackResult::kContinue;
507   }
508 
509   hci_spec::AdvertisingHandle adv_handle = params->adv_handle;
510   std::optional<DeviceAddress> opt_local_address =
511       advertising_handle_map_.GetAddress(adv_handle);
512 
513   // We use the identity address as the local address if we aren't advertising
514   // or otherwise don't know about this advertising set. This is obviously
515   // wrong. However, the link will be disconnected in that case before it can
516   // propagate to higher layers.
517   static DeviceAddress identity_address =
518       DeviceAddress(DeviceAddress::Type::kLEPublic, {0});
519   DeviceAddress local_address = identity_address;
520   if (opt_local_address) {
521     local_address = opt_local_address.value();
522   }
523 
524   StagedConnectionParameters staged = staged_parameters_node.mapped();
525 
526   CompleteIncomingConnection(connection_handle,
527                              staged.role,
528                              local_address,
529                              staged.peer_address,
530                              staged.conn_params);
531 
532   std::memset(&staged_advertising_parameters_,
533               0,
534               sizeof(staged_advertising_parameters_));
535   return CommandChannel::EventCallbackResult::kContinue;
536 }
537 
OnCurrentOperationComplete()538 void ExtendedLowEnergyAdvertiser::OnCurrentOperationComplete() {
539   if (op_queue_.empty()) {
540     return;  // no more queued operations so nothing to do
541   }
542 
543   fit::closure closure = std::move(op_queue_.front());
544   op_queue_.pop();
545   closure();
546 }
547 
548 }  // namespace bt::hci
549