• 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 #pragma once
16 
17 #include "pw_bluetooth_sapphire/internal/host/hci-spec/constants.h"
18 #include "pw_bluetooth_sapphire/internal/host/hci-spec/defaults.h"
19 #include "pw_bluetooth_sapphire/internal/host/hci/local_address_delegate.h"
20 #include "pw_bluetooth_sapphire/internal/host/hci/sequential_command_runner.h"
21 #include "pw_bluetooth_sapphire/internal/host/transport/transport.h"
22 
23 namespace bt::hci {
24 
25 class SequentialCommandRunner;
26 
27 // Represents a discovered Bluetooth Low Energy peer.
28 struct LowEnergyScanResult {
29   // The device address of the remote peer.
30   DeviceAddress address;
31 
32   // True if |address| is a static or random identity address resolved by the
33   // controller.
34   bool resolved = false;
35 
36   // True if this peer accepts connections. This is the case if this peer sent a
37   // connectable advertising PDU.
38   bool connectable = false;
39 
40   // The received signal strength of the advertisement packet corresponding to
41   // this peer.
42   int8_t rssi = hci_spec::kRSSIInvalid;
43 };
44 
45 // LowEnergyScanner manages Low Energy scan procedures that are used during
46 // general and limited discovery and connection establishment procedures. This
47 // is an abstract class that provides a common interface over 5.0 Extended
48 // Advertising and Legacy Advertising features.
49 //
50 // Instances of this class are expected to each as a singleton on a
51 // per-transport basis as multiple instances cannot accurately reflect the state
52 // of the controller while allowing simultaneous scan operations.
53 class LowEnergyScanner : public LocalAddressClient {
54  public:
55   // Value that can be passed to StartScan() to scan indefinitely.
56   static constexpr pw::chrono::SystemClock::duration kPeriodInfinite =
57       pw::chrono::SystemClock::duration::zero();
58 
59   enum class State {
60     // No scan is currently being performed.
61     kIdle,
62 
63     // A previously running scan is being stopped.
64     kStopping,
65 
66     // A scan is being initiated.
67     kInitiating,
68 
69     // An active scan is currently being performed.
70     kActiveScanning,
71 
72     // A passive scan is currently being performed.
73     kPassiveScanning,
74   };
75 
76   enum class ScanStatus {
77     // Reported when the scan could not be started.
78     kFailed,
79 
80     // Reported when an active scan was started and is currently in progress.
81     kActive,
82 
83     // Reported when a passive scan was started and is currently in progress.
84     kPassive,
85 
86     // Called when the scan was terminated naturally at the end of the scan
87     // period.
88     kComplete,
89 
90     // Called when the scan was terminated due to a call to StopScan().
91     kStopped,
92   };
93 
94   struct ScanOptions {
95     // Perform an active scan if true. During an active scan, scannable
96     // advertisements are reported alongside their corresponding scan response.
97     bool active = false;
98 
99     // When enabled, the controller will filter out duplicate advertising
100     // reports. This means that Delegate::OnPeerFound will be called only once
101     // per device address during the scan period.
102     //
103     // When disabled, Delegate::OnPeerFound will get called once for every
104     // observed advertisement (depending on |filter_policy|).
105     bool filter_duplicates = false;
106 
107     // Determines the type of filtering the controller should perform to limit
108     // the number of advertising reports.
109     pw::bluetooth::emboss::LEScanFilterPolicy filter_policy =
110         pw::bluetooth::emboss::LEScanFilterPolicy::BASIC_UNFILTERED;
111 
112     // Determines the length of the software defined scan period. If the value
113     // is kPeriodInfinite, then the scan will remain enabled until StopScan()
114     // gets called. For all other values, the scan will be disabled after the
115     // duration expires.
116     pw::chrono::SystemClock::duration period = kPeriodInfinite;
117 
118     // Maximum time duration during an active scan for which a scannable
119     // advertisement will be stored and not reported to clients until a
120     // corresponding scan response is received.
121     pw::chrono::SystemClock::duration scan_response_timeout =
122         std::chrono::seconds(2);
123 
124     // Scan parameters.
125     uint16_t interval = hci_spec::defaults::kLEScanInterval;
126     uint16_t window = hci_spec::defaults::kLEScanWindow;
127   };
128 
129   // This represents the data obtained for a scannable advertisement for which a
130   // scan response has not yet been received. Clients are notified for scannable
131   // advertisement either when the corresponding scan response is received or,
132   // otherwise, a timeout expires.
133   class PendingScanResult {
134    public:
135     // |adv|: Initial advertising data payload.
136     PendingScanResult(LowEnergyScanResult result,
137                       pw::chrono::SystemClock::duration timeout,
138                       pw::async::Dispatcher& dispatcher,
139                       fit::closure timeout_handler);
~PendingScanResult()140     ~PendingScanResult() { timeout_task_.Cancel(); }
141 
142     // Return the contents of the data.
data()143     BufferView data() const { return buffer_.view(0, data_size_); }
144 
result()145     const LowEnergyScanResult& result() const { return result_; }
146 
set_rssi(int8_t rssi)147     void set_rssi(int8_t rssi) { result_.rssi = rssi; }
set_resolved(bool resolved)148     void set_resolved(bool resolved) { result_.resolved = resolved; }
149 
150     // Appends |data| to the end of the current contents.
151     void AppendData(const ByteBuffer& data);
152 
153    private:
154     LowEnergyScanResult result_;
155 
156     // The size of the data so far accumulated in |buffer_|.
157     size_t data_size_ = 0u;
158 
159     // Buffer large enough to store both advertising and scan response payloads.
160     // LowEnergyScanner is subclassed by both LegacyLowEnergyScanner and
161     // ExtendedLowEnergyScanner. We use the maximum extended advertising data
162     // length here to support either version.
163     StaticByteBuffer<hci_spec::kMaxLEExtendedAdvertisingDataLength * 2> buffer_;
164 
165     // The duration which we will wait for a pending scan result to receive more
166     // data before reporting the pending result to the delegate.
167     pw::chrono::SystemClock::duration timeout_;
168 
169     // Since not all scannable advertisements are always followed by a scan
170     // response, we report a pending result if a scan response is not received
171     // within a timeout.
172     SmartTask timeout_task_;
173   };
174 
175   // Interface for receiving events related to Low Energy scan.
176   class Delegate {
177    public:
178     virtual ~Delegate() = default;
179 
180     // Called when a peer is found. During a passive scan |data| contains the
181     // advertising data. During an active scan |data| contains the combined
182     // advertising and scan response data (if the peer is scannable).
OnPeerFound(const LowEnergyScanResult & result,const ByteBuffer & data)183     virtual void OnPeerFound(const LowEnergyScanResult& result,
184                              const ByteBuffer& data) {}
185 
186     // Called when a directed advertising report is received from the peer with
187     // the given address.
OnDirectedAdvertisement(const LowEnergyScanResult & result)188     virtual void OnDirectedAdvertisement(const LowEnergyScanResult& result) {}
189   };
190 
191   LowEnergyScanner(LocalAddressDelegate* local_addr_delegate,
192                    Transport::WeakPtr hci,
193                    pw::async::Dispatcher& pw_dispatcher);
194   ~LowEnergyScanner() override = default;
195 
196   // Returns the current Scan state.
state()197   State state() const { return state_; }
198 
IsActiveScanning()199   bool IsActiveScanning() const { return state() == State::kActiveScanning; }
IsPassiveScanning()200   bool IsPassiveScanning() const { return state() == State::kPassiveScanning; }
IsScanning()201   bool IsScanning() const { return IsActiveScanning() || IsPassiveScanning(); }
IsInitiating()202   bool IsInitiating() const { return state() == State::kInitiating; }
203 
204   // LocalAddressClient override:
AllowsRandomAddressChange()205   bool AllowsRandomAddressChange() const override {
206     return !IsScanning() && hci_cmd_runner_->IsReady();
207   }
208 
209   // True if no scan procedure is currently enabled.
IsIdle()210   bool IsIdle() const { return state() == State::kIdle; }
211 
212   // Initiates a scan. This is an asynchronous operation that abides by the
213   // following rules:
214   //
215   //   - This method synchronously returns false if the procedure could not be
216   //   started, e.g. because discovery is already in progress, or it is in the
217   //   process of being stopped, or the controller does not support discovery,
218   //   etc.
219   //
220   //   - Synchronously returns true if the procedure was initiated but the it is
221   //   unknown whether or not the procedure has succeeded.
222   //
223   //   - |callback| is invoked asynchronously to report the status of the
224   //   procedure. In the case of failure, |callback| will be invoked once to
225   //   report the end of the procedure. In the case of success, |callback| will
226   //   be invoked twice: the first time to report that the procedure has
227   //   started, and a second time to report when the procedure ends, either due
228   //   to a timeout or cancellation.
229   //
230   //   - |period| specifies (in milliseconds) the duration of the scan. If the
231   //   special value of kPeriodInfinite is passed then scanning will continue
232   //   indefinitely and must be explicitly stopped by calling StopScan().
233   //   Otherwise, the value must be non-zero.
234   //
235   // Once started, a scan can be terminated at any time by calling the
236   // StopScan() method. Otherwise, an ongoing scan will terminate at the end of
237   // the scan period if a finite value for |period| was provided.
238   //
239   // During an active scan, scannable advertisements are reported alongside
240   // their corresponding scan response. Every scannable advertisement is stored
241   // and not reported until either
242   //
243   //   a) a scan response is received
244   //   b) an implementation determined timeout period expires
245   //   c) for periodic scans, when the scan period expires
246   //
247   // Since a passive scan involves no scan request/response, all advertisements
248   // are reported immediately without waiting for a scan response.
249   //
250   // (For more information about passive and active scanning, see Core Spec.
251   // v5.2, Vol 6, Part B, 4.4.3.1 and 4.4.3.2).
252   using ScanStatusCallback = fit::function<void(ScanStatus)>;
253   virtual bool StartScan(const ScanOptions& options,
254                          ScanStatusCallback callback);
255 
256   // Stops a previously started scan. Returns false if a scan is not in
257   // progress. Otherwise, cancels any in progress scan procedure and returns
258   // true.
259   virtual bool StopScan();
260 
261   // Assigns the delegate for scan events.
set_delegate(Delegate * delegate)262   void set_delegate(Delegate* delegate) { delegate_ = delegate; }
263 
264  protected:
265   // Build the HCI command packet to set the scan parameters for the flavor of
266   // low energy scanning being implemented.
267   virtual EmbossCommandPacket BuildSetScanParametersPacket(
268       const DeviceAddress& local_address, const ScanOptions& options) = 0;
269 
270   // Build the HCI command packet to enable scanning for the flavor of low
271   // energy scanning being implemented.
272   virtual EmbossCommandPacket BuildEnablePacket(
273       const ScanOptions& options,
274       pw::bluetooth::emboss::GenericEnableParam enable) = 0;
275 
276   // Called when a Scan Response is received during an active scan or when we
277   // time out waiting
278   void HandleScanResponse(const DeviceAddress& address,
279                           bool resolved,
280                           int8_t rssi);
281 
AddPendingResult(const DeviceAddress & address,const LowEnergyScanResult & scan_result,fit::closure timeout_handler)282   void AddPendingResult(const DeviceAddress& address,
283                         const LowEnergyScanResult& scan_result,
284                         fit::closure timeout_handler) {
285     std::unique_ptr<PendingScanResult> pending =
286         std::make_unique<PendingScanResult>(scan_result,
287                                             scan_response_timeout_,
288                                             pw_dispatcher_,
289                                             std::move(timeout_handler));
290     pending_results_[address] = std::move(pending);
291   }
292 
HasPendingResult(const DeviceAddress & address)293   bool HasPendingResult(const DeviceAddress& address) const {
294     return pending_results_.count(address);
295   }
296 
GetPendingResult(const DeviceAddress & address)297   std::unique_ptr<PendingScanResult>& GetPendingResult(
298       const DeviceAddress& address) {
299     return pending_results_[address];
300   }
301 
RemovePendingResult(const DeviceAddress & address)302   void RemovePendingResult(const DeviceAddress& address) {
303     pending_results_.erase(address);
304   }
305 
hci()306   Transport::WeakPtr hci() const { return hci_; }
delegate()307   Delegate* delegate() const { return delegate_; }
308 
309  private:
310   // Called by StartScan() after the local peer address has been obtained.
311   void StartScanInternal(const DeviceAddress& local_address,
312                          const ScanOptions& options,
313                          ScanStatusCallback callback);
314 
315   // Called by StopScan() and by the scan timeout handler set up by StartScan().
316   void StopScanInternal(bool stopped);
317 
318   State state_ = State::kIdle;
319   pw::async::Dispatcher& pw_dispatcher_;
320   Delegate* delegate_ = nullptr;  // weak
321 
322   // Callback passed in to the most recently accepted call to StartScan();
323   ScanStatusCallback scan_cb_;
324 
325   // The scan period timeout handler for the currently active scan session.
326   SmartTask scan_timeout_task_;
327 
328   // Maximum time duration for which a scannable advertisement will be stored
329   // and not reported to clients until a corresponding scan response is
330   // received.
331   pw::chrono::SystemClock::duration scan_response_timeout_;
332 
333   // Scannable advertising events for which a Scan Response PDU has not been
334   // received. This is accumulated during a discovery procedure and always
335   // cleared at the end of the scan period.
336   std::unordered_map<DeviceAddress, std::unique_ptr<PendingScanResult>>
337       pending_results_;
338 
339   // Used to obtain the local peer address type to use during scanning.
340   LocalAddressDelegate* local_addr_delegate_;  // weak
341 
342   // The HCI transport.
343   Transport::WeakPtr hci_;
344 
345   // Command runner for all HCI commands sent out by implementations.
346   std::unique_ptr<SequentialCommandRunner> hci_cmd_runner_;
347 
348   BT_DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(LowEnergyScanner);
349 };
350 
351 }  // namespace bt::hci
352