ESPHome 2026.8.0b4
Loading...
Searching...
No Matches
rp2_ble_tracker.cpp
Go to the documentation of this file.
1#ifdef USE_RP2
2
3#include "rp2_ble_tracker.h"
4
5#include <cinttypes>
6
8#include "esphome/core/log.h"
9
11
12static const char *const TAG = "rp2_ble_tracker";
13
14// Minimum interval between scan start attempts on an active stack. The
15// controller start has no failure mode once HCI is WORKING, so this fires at
16// most once per enable cycle today; the floor is insurance against a future
17// scan_start() failure being retried every main-loop iteration.
18static constexpr uint32_t SCAN_START_RETRY_MS = 1000;
19
20// One BLE scan unit in milliseconds; the controller programs interval/window in these units.
21static constexpr float BLE_SCAN_UNIT_MS = 0.625f;
22
24 // Receive the controller's scan reports; the controller queues them from the
25 // BTstack packet handler (IRQ) and delivers here on the main loop.
27 // Merged (and unmerged) frames go to the shared dispatcher; scan_continuous_
28 // is read at each delivery to decide unclaimed-device logging.
29 this->merger_.bind(&this->dispatcher_, &this->scan_continuous_, TAG);
30#ifdef USE_OTA_STATE_LISTENER
31 // Pause scanning while an OTA update is in flight — the BLE scan competes with
32 // the OTA download on the shared CYW43 radio. Mirrors esp32_ble_tracker.
34#endif
35 if (!this->scan_continuous_) {
36 // Nothing to do until an external start_scan(); the loop is re-enabled there.
37 this->disable_loop();
38 }
39}
40
41#ifdef USE_OTA_STATE_LISTENER
42void RP2BLETracker::on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) {
43 if (state == ota::OTA_STARTED) {
45 // A one-shot scan counts as pending when it is running or still retrying
46 // its start (loop enabled); captured before stop_scan() disables the loop.
48 this->stop_scan();
49 } else if (state == ota::OTA_ERROR || state == ota::OTA_ABORT) {
50 // On success the device reboots, so restore only on a failed/aborted update;
51 // loop()'s retry branch restarts the scan on its next iteration.
53 this->scan_continuous_before_ota_ = false;
54 this->scan_continuous_ = true;
55 this->enable_loop();
56 }
57 // A one-shot scan interrupted by the OTA resumes for a fresh duration
58 // rather than silently staying idle — an OTA failure does not reboot, so
59 // nothing external would restart it.
60 if (this->scan_pending_before_ota_) {
61 this->scan_pending_before_ota_ = false;
62 this->enable_loop();
63 }
64 }
65}
66#endif // USE_OTA_STATE_LISTENER
67
70 // Deliver held scannable advertisements whose scan response never arrived —
71 // unmerged after the merger's timeout.
72 if (!this->merger_.empty())
73 this->merger_.sweep(now);
74 if (this->scan_running_ && !this->parent_->is_active()) {
75 // The controller was disabled underneath us (e.g. a lambda calling
76 // rp2040_ble's disable()); the scan died with the stack. Reconcile so the
77 // retry branch below takes over once the user re-enables the stack.
78 this->scan_running_ = false;
79 this->fire_scan_end_();
80 }
81 if (!this->scan_running_) {
82 // A scan should be running but is not: continuous mode is always in this
83 // state until the start succeeds, and non-continuous mode only reaches
84 // here between start_scan() and a successful controller start, because
85 // stop_scan_() disables the loop otherwise.
86 if (!this->parent_->is_active()) {
87 // Stack not up (still booting, or the user called disable()) —
88 // scan_start() cannot succeed, so there is nothing to attempt; scanning
89 // starts on the first iteration after HCI reaches WORKING.
90 return;
91 }
92 if (now - this->last_scan_start_attempt_ >= SCAN_START_RETRY_MS) {
93 this->start_scan_();
94 }
95 return;
96 }
97
98 if (this->scan_continuous_) {
99 // Period timer: fire on_scan_end() once per scan_duration_ window, mirroring
100 // esp32_ble_tracker::cleanup_scan_state_().
101 if (now - this->scan_period_start_ >= this->scan_duration_) {
102 this->fire_scan_end_();
103 this->scan_period_start_ = now;
104 }
105 return;
106 }
107
108 // Non-continuous mode: run for scan_duration_ ms, then stop and fire on_scan_end.
109 // Restart is driven externally (e.g. api: on_client_connected:).
110 if (now - this->scan_period_start_ >= this->scan_duration_) {
111 this->stop_scan_();
112 }
113}
114
116 ESP_LOGCONFIG(TAG,
117 "RP2 BLE Tracker:\n"
118 " Scan Duration: %" PRIu32 " s\n"
119 " Scan Interval: %.0f ms (%" PRIu32 " BLE units)\n"
120 " Scan Window: %.0f ms (%" PRIu32 " BLE units)\n"
121 " Scan Type: %s\n"
122 " Continuous Scanning: %s",
123 this->scan_duration_ / 1000, this->scan_interval_ * BLE_SCAN_UNIT_MS, this->scan_interval_,
124 this->scan_window_ * BLE_SCAN_UNIT_MS, this->scan_window_,
125 this->scan_active_ ? LOG_STR_LITERAL("ACTIVE") : LOG_STR_LITERAL("PASSIVE"),
126 YESNO(this->scan_continuous_));
127}
128
129// GAP advertising event types as BTstack reports them (Core spec advertising
130// report event types; the tracker deliberately does not include BTstack
131// headers). ADV_IND and ADV_SCAN_IND are the scannable types.
132static constexpr uint8_t ADV_EVENT_TYPE_ADV_IND = 0;
133static constexpr uint8_t ADV_EVENT_TYPE_ADV_SCAN_IND = 2;
134static constexpr uint8_t ADV_EVENT_TYPE_SCAN_RSP = 4;
135
136// Demux advertisements vs scan responses into the shared merger: BTstack
137// delivers the pair as separate reports; a scannable advertisement is held
138// until its scan response arrives and delivered as one merged frame.
140 if (report.adv_event_type == ADV_EVENT_TYPE_SCAN_RSP) {
141 this->merger_.submit_scan_rsp(report.mac, report.rssi, report.addr_type, report.data, report.data_len);
142 return;
143 }
144 // Stash only while an active scan runs: a passive scan never gets a
145 // response, and after a stop nothing would sweep the merger, so a late
146 // report would surface minutes later as a fresh advertisement.
147 if (this->scan_running_ && this->scan_active_ &&
148 (report.adv_event_type == ADV_EVENT_TYPE_ADV_IND || report.adv_event_type == ADV_EVENT_TYPE_ADV_SCAN_IND)) {
149 this->merger_.stash_adv(report.mac, report.rssi, report.addr_type, report.data, report.data_len,
151 return;
152 }
153 this->dispatcher_.dispatch(report.mac, report.rssi, report.addr_type, report.data, report.data_len,
154 /*raw_only=*/false, this->scan_continuous_ ? nullptr : TAG);
155}
156
158 // Mirrors esp32_ble_tracker::start_scan(): caller sets scan_continuous_ via
159 // set_scan_continuous() first, then calls start_scan() to begin scanning.
160 this->enable_loop();
161 this->start_scan_();
162}
163
165 if (this->scan_active_ == active)
166 return true;
167 this->scan_active_ = active;
168 // V: the proxy's "Setting scanner mode" line already narrates this at D.
169 ESP_LOGV(TAG, "Scan mode %s", active ? "active" : "passive");
170 // Apply to a running scan by restarting the CONTROLLER scan with the new
171 // mode, bypassing the tracker's stop/start bookkeeping: no on_scan_end (the
172 // scan logically continues, only the request mode changes), no period reset.
173 // An idle scanner picks the mode up on its next start.
174 if (this->scan_running_) {
175 this->parent_->scan_stop();
176 if (!this->controller_scan_start_()) {
177 // The controller really stopped: behave exactly like loop()'s
178 // reconciliation branch - notify listeners and let its retry recover.
179 this->scan_running_ = false;
180 this->fire_scan_end_();
181 }
182 }
183 return true;
184}
185
187 this->scan_continuous_ = false;
188 this->stop_scan_();
189 // stop_scan_() early-returns when no scan is running, so disable the loop
190 // here too: a scan that never came up (stack still powering on at OTA start)
191 // must not keep attempting scan_start() from the loop's retry branch.
192 this->disable_loop();
193}
194
195// Stamp-and-start for every controller scan attempt: the stamp keeps the
196// SCAN_START_RETRY_MS floor covering all callers, not only loop()'s retry.
199 return this->parent_->scan_start(static_cast<uint16_t>(this->scan_interval_),
200 static_cast<uint16_t>(this->scan_window_), this->scan_active_);
201}
202
204 if (this->scan_running_)
205 return;
206
207 if (!this->controller_scan_start_())
208 return;
209
210 this->scan_running_ = true;
211 // Log every explicit start at DEBUG — stop_scan_() logs every stop at DEBUG, and
212 // in non-continuous mode each period is an explicit start, so asymmetric logging
213 // would read as the scanner failing to come back up.
214 ESP_LOGD(TAG, "Scan started (%s, window=%.0fms, interval=%.0fms)",
215 this->scan_active_ ? LOG_STR_LITERAL("active") : LOG_STR_LITERAL("passive"),
216 this->scan_window_ * BLE_SCAN_UNIT_MS, this->scan_interval_ * BLE_SCAN_UNIT_MS);
217 // Re-anchor the scan period to every successful start — first start (so the
218 // period counts from the scan, not from boot) and every restart after a stop (so
219 // resuming after longer than scan_duration, e.g. a failed OTA restoring continuous
220 // mode 10 minutes later, does not fire on_scan_end before an advertisement can
221 // arrive). Same clock as loop()'s `now`: a fresh millis() here would be ahead of
222 // the cached loop time and make the same-iteration period check underflow.
224}
225
227 if (!this->scan_running_)
228 return;
229 this->parent_->scan_stop();
230 this->scan_running_ = false;
231 ESP_LOGD(TAG, "Scan stopped");
232 this->fire_scan_end_();
233 // Reset the period clock so on_scan_end does not double-fire; same clock as loop().
235 if (!this->scan_continuous_) {
236 // Nothing left to time; start_scan() re-enables the loop.
237 this->disable_loop();
238 }
239}
240
242 // Deliver held advertisements whose scan response never came (unmerged)
243 // BEFORE on_scan_end fires.
244 this->merger_.flush();
245 this->dispatcher_.on_scan_end();
246}
247
248} // namespace esphome::rp2_ble_tracker
249
250#endif // USE_RP2
uint32_t IRAM_ATTR HOT get_loop_component_start_time() const
Get the cached time in milliseconds from when the current component started its loop execution.
bool is_in_loop_state() const
Check if this component has completed setup and is in the loop state.
Definition component.h:205
void enable_loop()
Enable this component's loop.
Definition component.h:246
void disable_loop()
Disable this component's loop.
void on_scan_end()
Fire listeners' on_scan_end and reset the per-scan discovered-log dedup.
void dispatch(const uint8_t *mac, int8_t rssi, uint8_t addr_type, const uint8_t *data, uint8_t data_len, bool raw_only, const char *log_unclaimed_tag)
Dispatch one (possibly merged) advertisement: the raw callback, and — unless raw_only — parsing for l...
void stash_adv(const uint8_t *mac, int8_t rssi, uint8_t addr_type, const uint8_t *data, uint8_t data_len, uint32_t now)
Hold a scannable advertisement, waiting for its scan response.
void sweep(uint32_t now)
Timeout flush (call from loop() with the stash_adv() clock): deliver held advertisements whose scan r...
void flush()
Deliver every held advertisement now (scan period/scan is ending, before on_scan_end fires): unmerged...
void submit_scan_rsp(const uint8_t *mac, int8_t rssi, uint8_t addr_type, const uint8_t *data, uint8_t data_len)
A scan response arrived: append it to the held advertisement from the same device and deliver the pai...
bool empty() const
Lets loop() skip the cross-TU sweep() call in the common case (empty: passive scan,...
void bind(AdvDispatcher *dispatcher, const bool *scan_continuous, const char *log_tag)
Wire the merger's output; call once in the tracker's setup().
void add_global_state_listener(OTAGlobalStateListener *listener)
void scan_stop()
Stop the controller scan (no-op when not scanning).
bool scan_start(uint16_t interval, uint16_t window, bool active)
Start a controller scan; active sends scan requests and receives scan responses as separate reports.
void register_scan_listener(BLEScanListener *listener)
Register a consumer for scan reports (delivered on the main loop via loop()).
Definition rp2040_ble.h:86
ble_device_base::AdvDispatcher dispatcher_
void on_scan_report(const rp2040_ble::BLEScanReport &report) override
ble_device_base::ScanResponseMerger merger_
void on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) override
bool state
Definition fan.h:2
OTAGlobalCallback * get_global_ota_callback()
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t
One advertisement report from the controller.
Definition rp2040_ble.h:27
uint8_t mac[MAC_ADDRESS_SIZE]
Definition rp2040_ble.h:28