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/low_energy_advertiser.h"
16
17 #include "pw_bluetooth_sapphire/internal/host/hci/sequential_command_runner.h"
18
19 namespace bt::hci {
20 namespace pwemb = pw::bluetooth::emboss;
21
LowEnergyAdvertiser(hci::Transport::WeakPtr hci,uint16_t max_advertising_data_length)22 LowEnergyAdvertiser::LowEnergyAdvertiser(hci::Transport::WeakPtr hci,
23 uint16_t max_advertising_data_length)
24 : hci_(std::move(hci)),
25 hci_cmd_runner_(std::make_unique<SequentialCommandRunner>(
26 hci_->command_channel()->AsWeakPtr())),
27 max_advertising_data_length_(max_advertising_data_length) {}
28
GetSizeLimit(const AdvertisingEventProperties & properties,const AdvertisingOptions & options) const29 size_t LowEnergyAdvertiser::GetSizeLimit(
30 const AdvertisingEventProperties& properties,
31 const AdvertisingOptions& options) const {
32 if (!properties.use_legacy_pdus) {
33 return max_advertising_data_length_;
34 }
35
36 // Core Spec Version 5.4, Volume 6, Part B, Section 2.3.1.2: legacy
37 // advertising PDUs that use directed advertising (ADV_DIRECT_IND) don't
38 // have an advertising data field in their payloads.
39 if (properties.IsDirected()) {
40 return 0;
41 }
42
43 uint16_t size_limit = hci_spec::kMaxLEAdvertisingDataLength;
44
45 // Core Spec Version 5.4, Volume 6, Part B, Section 2.3, Figure 2.5: Legacy
46 // advertising PDUs headers don't have a predesignated field for tx power.
47 // Instead, we include it in the Host advertising data itself. Subtract the
48 // size it will take up from the allowable remaining data size.
49 if (options.include_tx_power_level) {
50 size_limit -= kTLVTxPowerLevelSize;
51 }
52
53 return size_limit;
54 }
55
CanStartAdvertising(const DeviceAddress & address,const AdvertisingData & data,const AdvertisingData & scan_rsp,const AdvertisingOptions & options,const ConnectionCallback & connect_callback) const56 fit::result<HostError> LowEnergyAdvertiser::CanStartAdvertising(
57 const DeviceAddress& address,
58 const AdvertisingData& data,
59 const AdvertisingData& scan_rsp,
60 const AdvertisingOptions& options,
61 const ConnectionCallback& connect_callback) const {
62 PW_CHECK(address.type() != DeviceAddress::Type::kBREDR);
63
64 if (options.anonymous) {
65 bt_log(WARN, "hci-le", "anonymous advertising not supported");
66 return fit::error(HostError::kNotSupported);
67 }
68
69 AdvertisingEventProperties properties =
70 GetAdvertisingEventProperties(data, scan_rsp, options, connect_callback);
71
72 // Core Spec Version 5.4, Volume 5, Part E, Section 7.8.53: If extended
73 // advertising PDU types are being used then the advertisement shall not be
74 // both connectable and scannable.
75 if (!properties.use_legacy_pdus && properties.connectable &&
76 properties.scannable) {
77 bt_log(
78 WARN,
79 "hci-le",
80 "extended advertising pdus cannot be both connectable and scannable");
81 return fit::error(HostError::kNotSupported);
82 }
83
84 size_t size_limit = GetSizeLimit(properties, options);
85 if (size_t size = data.CalculateBlockSize(/*include_flags=*/true);
86 size > size_limit) {
87 bt_log(WARN,
88 "hci-le",
89 "advertising data too large (actual: %zu, max: %zu)",
90 size,
91 size_limit);
92 return fit::error(HostError::kAdvertisingDataTooLong);
93 }
94
95 if (size_t size = scan_rsp.CalculateBlockSize(/*include_flags=*/false);
96 size > size_limit) {
97 bt_log(WARN,
98 "hci-le",
99 "scan response too large (actual: %zu, max: %zu)",
100 size,
101 size_limit);
102 return fit::error(HostError::kScanResponseTooLong);
103 }
104
105 return fit::ok();
106 }
107
108 static LowEnergyAdvertiser::AdvertisingEventProperties
GetExtendedAdvertisingEventProperties(const AdvertisingData &,const AdvertisingData & scan_rsp,const LowEnergyAdvertiser::AdvertisingOptions & options,const LowEnergyAdvertiser::ConnectionCallback & connect_callback)109 GetExtendedAdvertisingEventProperties(
110 const AdvertisingData&,
111 const AdvertisingData& scan_rsp,
112 const LowEnergyAdvertiser::AdvertisingOptions& options,
113 const LowEnergyAdvertiser::ConnectionCallback& connect_callback) {
114 LowEnergyAdvertiser::AdvertisingEventProperties properties;
115
116 if (connect_callback) {
117 properties.connectable = true;
118 }
119
120 if (scan_rsp.CalculateBlockSize() > 0) {
121 properties.scannable = true;
122 }
123
124 // don't set the following fields because we don't currently support sending
125 // out directed advertisements:
126 // - directed
127 // - high_duty_cycle_directed_connectable
128
129 if (!options.extended_pdu) {
130 properties.use_legacy_pdus = true;
131 }
132
133 if (options.anonymous) {
134 properties.anonymous_advertising = true;
135 }
136
137 if (options.include_tx_power_level) {
138 properties.include_tx_power = true;
139 }
140
141 return properties;
142 }
143
144 static LowEnergyAdvertiser::AdvertisingEventProperties
GetLegacyAdvertisingEventProperties(const AdvertisingData &,const AdvertisingData & scan_rsp,const LowEnergyAdvertiser::AdvertisingOptions &,const LowEnergyAdvertiser::ConnectionCallback & connect_callback)145 GetLegacyAdvertisingEventProperties(
146 const AdvertisingData&,
147 const AdvertisingData& scan_rsp,
148 const LowEnergyAdvertiser::AdvertisingOptions&,
149 const LowEnergyAdvertiser::ConnectionCallback& connect_callback) {
150 LowEnergyAdvertiser::AdvertisingEventProperties properties;
151 properties.use_legacy_pdus = true;
152
153 // ADV_IND
154 if (connect_callback) {
155 properties.connectable = true;
156 properties.scannable = true;
157 return properties;
158 }
159
160 // ADV_SCAN_IND
161 if (scan_rsp.CalculateBlockSize() > 0) {
162 properties.scannable = true;
163 return properties;
164 }
165
166 // ADV_NONCONN_IND
167 return properties;
168 }
169
170 LowEnergyAdvertiser::AdvertisingEventProperties
GetAdvertisingEventProperties(const AdvertisingData & data,const AdvertisingData & scan_rsp,const AdvertisingOptions & options,const ConnectionCallback & connect_callback)171 LowEnergyAdvertiser::GetAdvertisingEventProperties(
172 const AdvertisingData& data,
173 const AdvertisingData& scan_rsp,
174 const AdvertisingOptions& options,
175 const ConnectionCallback& connect_callback) {
176 if (options.extended_pdu) {
177 return GetExtendedAdvertisingEventProperties(
178 data, scan_rsp, options, connect_callback);
179 }
180
181 return GetLegacyAdvertisingEventProperties(
182 data, scan_rsp, options, connect_callback);
183 }
184
185 pwemb::LEAdvertisingType
AdvertisingEventPropertiesToLEAdvertisingType(const AdvertisingEventProperties & p)186 LowEnergyAdvertiser::AdvertisingEventPropertiesToLEAdvertisingType(
187 const AdvertisingEventProperties& p) {
188 // ADV_IND
189 if (!p.high_duty_cycle_directed_connectable && !p.directed && p.scannable &&
190 p.connectable) {
191 return pwemb::LEAdvertisingType::CONNECTABLE_AND_SCANNABLE_UNDIRECTED;
192 }
193
194 // ADV_DIRECT_IND
195 if (!p.high_duty_cycle_directed_connectable && p.directed && !p.scannable &&
196 p.connectable) {
197 return pwemb::LEAdvertisingType::CONNECTABLE_LOW_DUTY_CYCLE_DIRECTED;
198 }
199
200 // ADV_DIRECT_IND
201 if (p.high_duty_cycle_directed_connectable && p.directed && !p.scannable &&
202 p.connectable) {
203 return pwemb::LEAdvertisingType::CONNECTABLE_HIGH_DUTY_CYCLE_DIRECTED;
204 }
205
206 // ADV_SCAN_IND
207 if (!p.high_duty_cycle_directed_connectable && !p.directed && p.scannable &&
208 !p.connectable) {
209 return pwemb::LEAdvertisingType::SCANNABLE_UNDIRECTED;
210 }
211
212 // ADV_NONCONN_IND
213 return pwemb::LEAdvertisingType::NOT_CONNECTABLE_UNDIRECTED;
214 }
215
StartAdvertisingInternal(const DeviceAddress & address,const AdvertisingData & data,const AdvertisingData & scan_rsp,const AdvertisingOptions & options,ConnectionCallback connect_callback,hci::ResultFunction<> result_callback)216 void LowEnergyAdvertiser::StartAdvertisingInternal(
217 const DeviceAddress& address,
218 const AdvertisingData& data,
219 const AdvertisingData& scan_rsp,
220 const AdvertisingOptions& options,
221 ConnectionCallback connect_callback,
222 hci::ResultFunction<> result_callback) {
223 if (IsAdvertising(address, options.extended_pdu)) {
224 // Temporarily disable advertising so we can tweak the parameters
225 CommandPacket packet = BuildEnablePacket(
226 address, pwemb::GenericEnableParam::DISABLE, options.extended_pdu);
227 hci_cmd_runner_->QueueCommand(packet);
228 }
229
230 data.Copy(&staged_parameters_.data);
231 scan_rsp.Copy(&staged_parameters_.scan_rsp);
232
233 pwemb::LEOwnAddressType own_addr_type;
234 if (address.type() == DeviceAddress::Type::kLEPublic) {
235 own_addr_type = pwemb::LEOwnAddressType::PUBLIC;
236 } else {
237 own_addr_type = pwemb::LEOwnAddressType::RANDOM;
238 }
239
240 AdvertisingEventProperties properties =
241 GetAdvertisingEventProperties(data, scan_rsp, options, connect_callback);
242 std::optional<CommandPacket> set_adv_params_packet =
243 BuildSetAdvertisingParams(address,
244 properties,
245 own_addr_type,
246 options.interval,
247 options.extended_pdu);
248 if (!set_adv_params_packet) {
249 bt_log(
250 WARN, "hci-le", "failed to start advertising for %s", bt_str(address));
251 return;
252 }
253
254 hci_cmd_runner_->QueueCommand(
255 *set_adv_params_packet,
256 fit::bind_member<&LowEnergyAdvertiser::OnSetAdvertisingParamsComplete>(
257 this));
258
259 // In order to support use cases where advertisers use the return parameters
260 // of the SetAdvertisingParams HCI command, we place the remaining advertising
261 // setup HCI commands in the result callback here. SequentialCommandRunner
262 // doesn't allow enqueuing commands within a callback (during a run).
263 hci_cmd_runner_->RunCommands(
264 [this,
265 address,
266 options,
267 result_cb = std::move(result_callback),
268 connect_cb = std::move(connect_callback)](hci::Result<> result) mutable {
269 if (bt_is_error(result,
270 WARN,
271 "hci-le",
272 "failed to start advertising for %s",
273 bt_str(address))) {
274 result_cb(result);
275 return;
276 }
277
278 bool success = StartAdvertisingInternalStep2(
279 address, options, std::move(connect_cb), std::move(result_cb));
280 if (!success) {
281 result_cb(ToResult(HostError::kCanceled));
282 }
283 });
284 }
285
StartAdvertisingInternalStep2(const DeviceAddress & address,const AdvertisingOptions & options,ConnectionCallback connect_callback,hci::ResultFunction<> result_callback)286 bool LowEnergyAdvertiser::StartAdvertisingInternalStep2(
287 const DeviceAddress& address,
288 const AdvertisingOptions& options,
289 ConnectionCallback connect_callback,
290 hci::ResultFunction<> result_callback) {
291 std::vector<CommandPacket> set_adv_data_packets = BuildSetAdvertisingData(
292 address, staged_parameters_.data, options.flags, options.extended_pdu);
293 for (auto& packet : set_adv_data_packets) {
294 hci_cmd_runner_->QueueCommand(std::move(packet));
295 }
296
297 std::vector<CommandPacket> set_scan_rsp_packets = BuildSetScanResponse(
298 address, staged_parameters_.scan_rsp, options.extended_pdu);
299 for (auto& packet : set_scan_rsp_packets) {
300 hci_cmd_runner_->QueueCommand(std::move(packet));
301 }
302
303 CommandPacket enable_packet = BuildEnablePacket(
304 address, pwemb::GenericEnableParam::ENABLE, options.extended_pdu);
305 hci_cmd_runner_->QueueCommand(enable_packet);
306
307 staged_parameters_.reset();
308 hci_cmd_runner_->RunCommands([this,
309 address,
310 extended_pdu = options.extended_pdu,
311 result_cb = std::move(result_callback),
312 connect_cb = std::move(connect_callback)](
313 Result<> result) mutable {
314 if (!bt_is_error(result,
315 WARN,
316 "hci-le",
317 "failed to start advertising for %s",
318 bt_str(address))) {
319 bt_log(INFO, "hci-le", "advertising enabled for %s", bt_str(address));
320 connection_callbacks_[{address, extended_pdu}] = std::move(connect_cb);
321 }
322
323 result_cb(result);
324 OnCurrentOperationComplete();
325 });
326
327 return true;
328 }
329
330 // We have StopAdvertising(address) so one would naturally think to implement
331 // StopAdvertising() by iterating through all addresses and calling
332 // StopAdvertising(address) on each iteration. However, such an implementation
333 // won't work. Each call to StopAdvertising(address) checks if the command
334 // runner is running, cancels any pending commands if it is, and then issues new
335 // ones. Called in quick succession, StopAdvertising(address) won't have a
336 // chance to finish its previous HCI commands before being cancelled. Instead,
337 // we must enqueue them all at once and then run them together.
StopAdvertising()338 void LowEnergyAdvertiser::StopAdvertising() {
339 if (!hci_cmd_runner_->IsReady()) {
340 hci_cmd_runner_->Cancel();
341 }
342
343 for (auto itr = connection_callbacks_.begin();
344 itr != connection_callbacks_.end();) {
345 const auto& [address, extended_pdu] = itr->first;
346
347 bool success = EnqueueStopAdvertisingCommands(address, extended_pdu);
348 if (success) {
349 itr = connection_callbacks_.erase(itr);
350 } else {
351 bt_log(WARN, "hci-le", "cannot stop advertising for %s", bt_str(address));
352 itr++;
353 }
354 }
355
356 if (hci_cmd_runner_->HasQueuedCommands()) {
357 hci_cmd_runner_->RunCommands([this](hci::Result<> result) {
358 bt_log(INFO, "hci-le", "advertising stopped: %s", bt_str(result));
359 OnCurrentOperationComplete();
360 });
361 }
362 }
363
StopAdvertisingInternal(const DeviceAddress & address,bool extended_pdu)364 void LowEnergyAdvertiser::StopAdvertisingInternal(const DeviceAddress& address,
365 bool extended_pdu) {
366 if (!IsAdvertising(address, extended_pdu)) {
367 return;
368 }
369
370 bool success = EnqueueStopAdvertisingCommands(address, extended_pdu);
371 if (!success) {
372 bt_log(WARN, "hci-le", "cannot stop advertising for %s", bt_str(address));
373 return;
374 }
375
376 hci_cmd_runner_->RunCommands([this, address](Result<> result) {
377 bt_log(INFO,
378 "hci-le",
379 "advertising stopped for %s: %s",
380 bt_str(address),
381 bt_str(result));
382 OnCurrentOperationComplete();
383 });
384
385 connection_callbacks_.erase({address, extended_pdu});
386 }
387
EnqueueStopAdvertisingCommands(const DeviceAddress & address,bool extended_pdu)388 bool LowEnergyAdvertiser::EnqueueStopAdvertisingCommands(
389 const DeviceAddress& address, bool extended_pdu) {
390 CommandPacket disable_packet = BuildEnablePacket(
391 address, pwemb::GenericEnableParam::DISABLE, extended_pdu);
392 CommandPacket unset_scan_rsp_packet =
393 BuildUnsetScanResponse(address, extended_pdu);
394 CommandPacket unset_adv_data_packet =
395 BuildUnsetAdvertisingData(address, extended_pdu);
396 CommandPacket remove_packet =
397 BuildRemoveAdvertisingSet(address, extended_pdu);
398
399 hci_cmd_runner_->QueueCommand(disable_packet);
400 hci_cmd_runner_->QueueCommand(unset_scan_rsp_packet);
401 hci_cmd_runner_->QueueCommand(unset_adv_data_packet);
402 hci_cmd_runner_->QueueCommand(remove_packet);
403
404 return true;
405 }
406
CompleteIncomingConnection(hci_spec::ConnectionHandle handle,pwemb::ConnectionRole role,const DeviceAddress & local_address,const DeviceAddress & peer_address,const hci_spec::LEConnectionParameters & conn_params,bool extended_pdu)407 void LowEnergyAdvertiser::CompleteIncomingConnection(
408 hci_spec::ConnectionHandle handle,
409 pwemb::ConnectionRole role,
410 const DeviceAddress& local_address,
411 const DeviceAddress& peer_address,
412 const hci_spec::LEConnectionParameters& conn_params,
413 bool extended_pdu) {
414 // Immediately construct a Connection object. If this object goes out of scope
415 // following the error checks below, it will send the a command to disconnect
416 // the link.
417 std::unique_ptr<LowEnergyConnection> link =
418 std::make_unique<LowEnergyConnection>(
419 handle, local_address, peer_address, conn_params, role, hci());
420
421 if (!IsAdvertising(local_address, extended_pdu)) {
422 bt_log(DEBUG,
423 "hci-le",
424 "connection received without advertising address (role: %d, local "
425 "address: %s, peer "
426 "address: %s, connection parameters: %s)",
427 static_cast<uint8_t>(role),
428 bt_str(local_address),
429 bt_str(peer_address),
430 bt_str(conn_params));
431 return;
432 }
433
434 ConnectionCallback connect_callback =
435 std::move(connection_callbacks_[{local_address, extended_pdu}]);
436 if (!connect_callback) {
437 bt_log(DEBUG,
438 "hci-le",
439 "connection received when not connectable (role: %d, "
440 "local address: %s, "
441 "peer address: %s, "
442 "connection parameters: %s)",
443 static_cast<uint8_t>(role),
444 bt_str(local_address),
445 bt_str(peer_address),
446 bt_str(conn_params));
447 return;
448 }
449
450 StopAdvertising(local_address, extended_pdu);
451 connect_callback(std::move(link));
452 connection_callbacks_.erase({local_address, extended_pdu});
453 }
454
455 } // namespace bt::hci
456