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