ESPHome 2026.9.0-dev
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// Floor between controller start attempts; insurance against a failing
15// scan_start() being retried every loop.
16static constexpr uint32_t SCAN_START_RETRY_MS = 1000;
17
18// One BLE scan unit in milliseconds; the controller programs interval/window in these units.
19static constexpr float BLE_SCAN_UNIT_MS = 0.625f;
20
22 // Receive the controller's scan reports; the controller queues them from the
23 // BTstack packet handler (IRQ) and delivers here on the main loop.
25 // Merged (and unmerged) frames go to the shared dispatcher; scan_continuous_
26 // is read at each delivery to decide unclaimed-device logging.
27 this->merger_.bind(&this->dispatcher_, &this->scan_continuous_, TAG);
28#ifdef USE_OTA_STATE_LISTENER
29 // Pause scanning while an OTA update is in flight — the BLE scan competes with
30 // the OTA download on the shared CYW43 radio. Mirrors esp32_ble_tracker.
32#endif
33 // An on_boot start_scan runs before setup(); parking here would strand it.
34 if (!this->scan_continuous_ && !this->scan_running_ && !this->pending_start_) {
35 // Nothing to do until an external start_scan(); the loop is re-enabled there.
36 this->disable_loop();
37 }
38}
39
40#ifdef USE_OTA_STATE_LISTENER
41void RP2BLETracker::on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) {
42 if (state == ota::OTA_STARTED) {
43 // Set before stop_scan(): its on_scan_end automations run synchronously and
44 // may call start_scan(), which must defer instead of resuming the radio.
45 this->ota_in_progress_ = true;
47 // A one-shot scan counts as pending when it is running, latched, or still
48 // retrying its start (loop enabled); captured before stop_scan() parks it.
50 !this->scan_continuous_ && (this->scan_running_ || this->pending_start_ || this->is_in_loop_state());
51 // The pause's own stop is not a user stop, so it must not clear the latches
52 // captured just above.
53 this->ota_pausing_ = true;
54 this->stop_scan();
55 this->ota_pausing_ = false;
56 } else if (state == ota::OTA_ERROR || state == ota::OTA_ABORT) {
57 this->ota_in_progress_ = false;
58 // On success the device reboots, so restore only on a failed/aborted update;
59 // loop()'s retry branch restarts the scan on its next iteration.
61 this->scan_continuous_before_ota_ = false;
62 this->scan_continuous_ = true;
63 this->enable_loop();
64 }
65 // A failed OTA does not reboot, so nothing else would restart a one-shot.
66 if (this->scan_pending_before_ota_) {
67 this->scan_pending_before_ota_ = false;
68 this->enable_loop();
69 }
70 }
71}
72#endif // USE_OTA_STATE_LISTENER
73
75#ifdef USE_OTA_STATE_LISTENER
76 // Keeps "no radio during an OTA" local instead of emergent from the
77 // parking sites.
78 if (this->ota_in_progress_)
79 return;
80#endif
82 if (this->pending_start_ && this->parent_->is_active()) {
83 // Latched start, applied once the stack is ACTIVE; earlier attempts would
84 // fail and arm the retry floor for nothing.
85 this->pending_start_ = false;
86 if (!this->scan_running_)
87 this->start_scan_();
88 }
89 // Deliver held scannable advertisements whose scan response never arrived —
90 // unmerged after the merger's timeout.
91 if (!this->merger_.empty())
92 this->merger_.sweep(now);
93 if (this->scan_running_ && !this->parent_->is_active()) {
94 // Stack disabled underneath us; reconcile so the retry branch takes over.
95 this->scan_running_ = false;
96 this->fire_scan_end_();
97 }
98 if (!this->scan_running_) {
99 // Should be scanning but is not: continuous until the start succeeds,
100 // one-shot only between start_scan() and a successful controller start.
101 if (!this->parent_->is_active()) {
102 // Stack not up: scan_start() cannot succeed yet.
103 return;
104 }
105 if (now - this->last_scan_start_attempt_ >= SCAN_START_RETRY_MS) {
106 this->start_scan_();
107 }
108 return;
109 }
110
111 if (this->scan_continuous_) {
112 // Period timer: fire on_scan_end() once per scan_duration_ window, mirroring
113 // esp32_ble_tracker::cleanup_scan_state_().
114 if (now - this->scan_period_start_ >= this->scan_duration_) {
115 this->fire_scan_end_();
116 this->scan_period_start_ = now;
117 }
118 return;
119 }
120
121 // Non-continuous mode: run for scan_duration_ ms, then stop and fire on_scan_end.
122 // Restart is driven externally (e.g. api: on_client_connected:).
123 if (now - this->scan_start_time_ >= this->scan_duration_) {
124 this->stop_scan_();
125 }
126}
127
129 ESP_LOGCONFIG(TAG,
130 "RP2 BLE Tracker:\n"
131 " Scan Duration: %" PRIu32 " s\n"
132 " Scan Interval: %.0f ms (%" PRIu32 " BLE units)\n"
133 " Scan Window: %.0f ms (%" PRIu32 " BLE units)\n"
134 " Scan Type: %s\n"
135 " Continuous Scanning: %s",
136 this->scan_duration_ / 1000, this->scan_interval_ * BLE_SCAN_UNIT_MS, this->scan_interval_,
137 this->scan_window_ * BLE_SCAN_UNIT_MS, this->scan_window_,
138 this->scan_active_ ? LOG_STR_LITERAL("ACTIVE") : LOG_STR_LITERAL("PASSIVE"),
139 YESNO(this->scan_continuous_));
140}
141
142// Core spec advertising report event types (BTstack headers stay out of this
143// TU). ADV_IND and ADV_SCAN_IND are the scannable ones.
144static constexpr uint8_t ADV_EVENT_TYPE_ADV_IND = 0;
145static constexpr uint8_t ADV_EVENT_TYPE_ADV_SCAN_IND = 2;
146static constexpr uint8_t ADV_EVENT_TYPE_SCAN_RSP = 4;
147
148// BTstack delivers the pair as separate reports; the merger holds a scannable
149// advertisement until its response arrives.
151 if (report.adv_event_type == ADV_EVENT_TYPE_SCAN_RSP) {
152 this->merger_.submit_scan_rsp(report.mac, report.rssi, report.addr_type, report.data, report.data_len);
153 return;
154 }
155 // Only while an active scan runs: nothing sweeps the merger after a stop.
156 if (this->scan_running_ && this->scan_active_ &&
157 (report.adv_event_type == ADV_EVENT_TYPE_ADV_IND || report.adv_event_type == ADV_EVENT_TYPE_ADV_SCAN_IND)) {
158 this->merger_.stash_adv(report.mac, report.rssi, report.addr_type, report.data, report.data_len,
160 return;
161 }
162 this->dispatcher_.dispatch(report.mac, report.rssi, report.addr_type, report.data, report.data_len,
163 /*raw_only=*/false, this->scan_continuous_ ? nullptr : TAG);
164}
165
167 // Mirrors esp32_ble_tracker::start_scan(): caller sets scan_continuous_ via
168 // set_scan_continuous() first, then calls start_scan() to begin scanning.
169#ifdef USE_OTA_STATE_LISTENER
170 if (this->ota_in_progress_) {
171 // Defer to the post-OTA resume path, carrying the requested mode. Not
172 // while ota_pausing_: scan_continuous_ is an artefact of the pause's own
173 // stop there, not intent.
174 if (!this->ota_pausing_) {
177 }
178 return;
179 }
180#endif
181 this->enable_loop();
182 if (!this->is_ready() || !this->parent_->is_active()) {
183 // Pre-setup or stack not ACTIVE: latch, loop() applies it.
184 this->pending_start_ = true;
185 return;
186 }
187 // bk72xx force semantics: a user start jumps the floor only while the
188 // controller is healthy. loop()'s retry branch picks the request up.
189 if (this->last_start_failed_ &&
190 App.get_loop_component_start_time() - this->last_scan_start_attempt_ < SCAN_START_RETRY_MS) {
191 return;
192 }
193 this->start_scan_();
194}
195
197 if (!this->scan_running_)
198 return; // start_scan_() anchors the clock itself on the next real start
199 // One-shot clock only (bk72xx parity); re-anchoring the period would let
200 // repeated actions starve on_scan_end. Same clock as loop()'s now.
202}
203
205 if (this->scan_active_ == active)
206 return true;
207 this->scan_active_ = active;
208 // V: the proxy's "Setting scanner mode" line already narrates this at D.
209 ESP_LOGV(TAG, "Scan mode %s", active ? "active" : "passive");
210 // Restart the controller scan only: the scan logically continues, so no
211 // on_scan_end and no period reset. An idle scanner applies it on next start.
212 if (this->scan_running_) {
213 this->parent_->scan_stop();
214 if (!this->controller_scan_start_()) {
215 // The controller really stopped: behave exactly like loop()'s
216 // reconciliation branch - notify listeners and let its retry recover.
217 this->scan_running_ = false;
218 this->fire_scan_end_();
219 }
220 }
221 return true;
222}
223
225 // Cancel a start latched before setup(); without this an on_boot
226 // start_scan/stop_scan pair would still start at the first loop().
227 this->pending_start_ = false;
228 this->scan_continuous_ = false;
229#ifdef USE_OTA_STATE_LISTENER
230 // A user stop during the OTA is the latest intent; the pause's own stop
231 // (ota_pausing_) is exempt - it armed that state.
232 if (this->ota_in_progress_ && !this->ota_pausing_) {
233 this->scan_pending_before_ota_ = false;
234 this->scan_continuous_before_ota_ = false;
235 }
236#endif
237 this->stop_scan_();
238 // stop_scan_() early-returns when idle, so park here too - once set up, and
239 // re-checked: its synchronous on_scan_end may have restarted the scan.
240 if (this->is_ready() && !this->scan_running_ && !this->pending_start_) {
241 this->disable_loop();
242 }
243}
244
245// Stamp-and-start for every controller scan attempt: the stamp keeps the
246// SCAN_START_RETRY_MS floor covering all callers, not only loop()'s retry.
249 const bool ok = this->parent_->scan_start(static_cast<uint16_t>(this->scan_interval_),
250 static_cast<uint16_t>(this->scan_window_), this->scan_active_);
251 this->last_start_failed_ = !ok;
252 return ok;
253}
254
256 if (this->scan_running_)
257 return;
258
259 if (!this->controller_scan_start_())
260 return;
261
262 this->scan_running_ = true;
263 // Symmetric with stop_scan_()'s stop log; asymmetry would read as the
264 // scanner failing to come back.
265 ESP_LOGD(TAG, "Scan started (%s, window=%.0fms, interval=%.0fms)",
266 this->scan_active_ ? LOG_STR_LITERAL("active") : LOG_STR_LITERAL("passive"),
267 this->scan_window_ * BLE_SCAN_UNIT_MS, this->scan_interval_ * BLE_SCAN_UNIT_MS);
268 // Anchor the period to the scan, not to boot, so a restart after a long gap
269 // does not fire on_scan_end immediately. Same clock as loop()'s now.
272}
273
275 if (!this->scan_running_)
276 return;
277 this->parent_->scan_stop();
278 this->scan_running_ = false;
279 ESP_LOGD(TAG, "Scan stopped");
280 this->fire_scan_end_();
281 // Reset the period clock so on_scan_end does not double-fire; same clock as loop().
283 // on_scan_end runs synchronously and may restart the scan; re-check before
284 // parking or that scan runs untimed.
285 if (!this->scan_continuous_ && !this->scan_running_ && !this->pending_start_) {
286 // Nothing left to time; start_scan() re-enables the loop.
287 this->disable_loop();
288 }
289}
290
292 // Deliver held advertisements whose scan response never came (unmerged)
293 // BEFORE on_scan_end fires.
294 this->merger_.flush();
295 this->dispatcher_.on_scan_end();
296}
297
298} // namespace esphome::rp2_ble_tracker
299
300#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
bool is_ready() const
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