ESPHome 2026.8.0b4
Loading...
Searching...
No Matches
rp2040_ble.cpp
Go to the documentation of this file.
1#include "rp2040_ble.h"
2
3#ifdef USE_RP2040_BLE
4
6#include "esphome/core/log.h"
7
8#include <BluetoothLock.h>
9
10#include <cstring>
11
12namespace esphome::rp2040_ble {
13
14static const char *const TAG = "rp2040_ble";
15
16// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
18
20 global_ble = this;
21
22 // Pre-create every pool entry so the packet handler's allocate() is always a
23 // free-list pop — the IRQ path must never reach malloc() (the newlib malloc
24 // lock is not IRQ-safe). Deliberately
25 // unconditional: warming lazily on the first scan would move the allocations
26 // after setup, and doing it here keeps the pool's RAM cost visible at
27 // startup instead of appearing once scanning begins. On an incomplete warm,
28 // refuse to run instead (the stack is never enabled, so the packet handler
29 // cannot fire).
30 if (!this->report_pool_.warm()) {
31 ESP_LOGE(TAG, "Scan report pool warm-up failed");
32 this->mark_failed();
33 return;
34 }
35
36 if (this->enable_on_boot_) {
37 this->enable();
38 } else {
40 }
41}
42
45 return;
46 }
47
48 ESP_LOGD(TAG, "Enabling BLE...");
50 this->active_logged_ = false;
51
52 if (!this->btstack_initialized_) {
53 // Serialize with the BTstack background worker while wiring the stack up
54 // (arduino-pico's BluetoothHCI::install() takes the same lock here).
55 BluetoothLock lock;
56
57 // BTstack init functions are not idempotent — only call once
58 l2cap_init();
59 sm_init();
60
61#ifdef USE_BLE_GATT_CLIENT
62 gatt_client_init();
63 // The GATT engine kicks the MTU exchange explicitly right after a
64 // connection completes (auto negotiation would only run on the first
65 // query, which a with-cache connection never issues).
66 gatt_client_mtu_enable_auto_negotiation(0);
67 // Just-works security for peripheral-initiated pairing.
68 sm_set_io_capabilities(IO_CAPABILITY_NO_INPUT_NO_OUTPUT);
69 sm_set_authentication_requirements(SM_AUTHREQ_BONDING);
70#endif
71
73 hci_add_event_handler(&this->hci_event_callback_registration_);
74
76 sm_add_event_handler(&this->sm_event_callback_registration_);
77
78 this->btstack_initialized_ = true;
79 }
80
81 BluetoothLock lock;
82 hci_power_control(HCI_POWER_ON);
83}
84
87 return;
88 }
89
90 ESP_LOGD(TAG, "Disabling BLE...");
92
93 {
94 BluetoothLock lock;
95 hci_power_control(HCI_POWER_OFF);
96 }
97
99 ESP_LOGD(TAG, "BLE disabled");
100}
101
103 if (this->state_ == BLEComponentState::ACTIVE && !this->active_logged_) {
104 this->active_logged_ = true;
105 // The controller address becomes readable once HCI reaches WORKING.
106 // bd_addr_to_str() formats into a BTstack-internal static buffer, so both
107 // calls stay under the lock like every other BTstack call from the loop.
108 BluetoothLock lock;
109 gap_local_bd_addr(this->ble_mac_);
110 ESP_LOGI(TAG, "BLE active (MAC %s)", bd_addr_to_str(this->ble_mac_));
111 }
112
113 // Drain the lock-free ring filled by the BTstack packet handler; all
114 // per-report work runs here on the main loop, then the report returns to
115 // the pool.
116 BLEScanReport *report = this->report_queue_.pop();
117 if (report == nullptr)
118 return;
119 do {
120#ifdef RP2040_BLE_SCAN_LISTENER_COUNT
121 for (auto *listener : this->scan_listeners_)
122 listener->on_scan_report(*report);
123#endif
124 this->report_pool_.release(report);
125 } while ((report = this->report_queue_.pop()) != nullptr);
126
127 uint16_t dropped = this->report_queue_.get_and_reset_dropped_count();
128 if (dropped > 0) {
129 ESP_LOGW(TAG, "Dropped %u scan reports (queue full)", dropped);
130 }
131}
132
133static const char *state_to_str(BLEComponentState state) {
134 switch (state) {
136 return "OFF";
138 return "ENABLING";
140 return "ACTIVE";
142 return "DISABLING";
144 return "DISABLED";
145 default:
146 return "UNKNOWN";
147 }
148}
149
151 ESP_LOGCONFIG(TAG,
152 "RP2040 BLE:\n"
153 " Enable on boot: %s\n"
154 " State: %s",
155 YESNO(this->enable_on_boot_), state_to_str(this->state_));
156}
157
159
160void RP2040BLE::packet_handler(uint8_t type, uint16_t channel, uint8_t *packet, uint16_t size) {
161 if (global_ble == nullptr) {
162 return;
163 }
164
165 if (type != HCI_EVENT_PACKET) {
166 return;
167 }
168
169 uint8_t event_type = hci_event_packet_get_type(packet);
170
171 switch (event_type) {
172 case BTSTACK_EVENT_STATE: {
173 uint8_t state = btstack_event_state_get_state(packet);
174 if (state == HCI_STATE_WORKING && global_ble->state_ == BLEComponentState::ENABLING) {
176 }
177 break;
178 }
179 case GAP_EVENT_ADVERTISING_REPORT: {
180 // Runs in the CYW43 async-context worker (low-priority IRQ), NOT the
181 // ESPHome main loop: bounded copy into the lock-free queue only.
182 bd_addr_t addr; // accessor returns printable (MSB-first) order
183 gap_event_advertising_report_get_address(packet, addr);
184 uint8_t mac_lsb[MAC_ADDRESS_SIZE];
185 reverse_bd_addr(addr, mac_lsb); // LSB-first, the BLE convention consumers expect
186 global_ble->enqueue_scan_report_(mac_lsb, static_cast<int8_t>(gap_event_advertising_report_get_rssi(packet)),
187 gap_event_advertising_report_get_address_type(packet),
188 gap_event_advertising_report_get_advertising_event_type(packet),
189 gap_event_advertising_report_get_data(packet),
190 gap_event_advertising_report_get_data_length(packet));
191 break;
192 }
193 default:
194 break;
195 }
196}
197
198// The analyzer traces a leak on the failed-push path, which cannot happen: the
199// pool is sized to the queue capacity (SIZE-1), so allocate() returns nullptr
200// before push() can find the ring full.
201// NOLINTBEGIN(clang-analyzer-unix.Malloc)
202void RP2040BLE::enqueue_scan_report_(const uint8_t *mac_lsb_first, int8_t rssi, uint8_t addr_type,
203 uint8_t adv_event_type, const uint8_t *data, uint16_t data_len) {
204 BLEScanReport *report = this->report_pool_.allocate();
205 if (report == nullptr) {
206 // Pool exhausted — the queue is full; count and drop.
207 this->report_queue_.increment_dropped_count();
208 return;
209 }
210 memcpy(report->mac, mac_lsb_first, MAC_ADDRESS_SIZE);
211 report->rssi = rssi;
212 report->addr_type = addr_type;
213 report->adv_event_type = adv_event_type;
214 report->data_len =
215 (data_len <= sizeof(report->data)) ? static_cast<uint8_t>(data_len) : static_cast<uint8_t>(sizeof(report->data));
216 memcpy(report->data, data, report->data_len);
217 this->report_queue_.push(report);
218}
219// NOLINTEND(clang-analyzer-unix.Malloc)
220
221void RP2040BLE::get_mac_msb_first(uint8_t out[MAC_ADDRESS_SIZE]) const {
222 memcpy(out, this->ble_mac_, MAC_ADDRESS_SIZE);
223}
224
225bool RP2040BLE::scan_start(uint16_t interval, uint16_t window, bool active) {
226 if (!this->is_active()) {
227 // Power control stays with the user (enable_on_boot or an explicit
228 // enable() call) — auto-enabling here would defeat enable_on_boot: false
229 // the moment a tracker retries. Callers retry until the stack is up.
230 return false;
231 }
232#ifdef USE_BLE_GATT_CLIENT
233 this->scan_interval_ = interval;
234 this->scan_window_ = window;
235 this->scan_active_mode_ = active;
236 this->scan_desired_ = true;
237 if (this->scan_inhibit_count_ > 0) {
238 // A connect attempt owns the radio; the scan starts physically when the
239 // inhibit is released. Report success — the controller will run it.
240 ESP_LOGV(TAG, "Scan start deferred (connect in progress)");
241 return true;
242 }
243#endif
244 // Serialize with the BTstack background worker (arduino-pico's BluetoothHCI
245 // takes the same lock around its gap_* calls).
246 BluetoothLock lock;
247 gap_set_scan_params(active ? 1 : 0, interval, window, 0 /* accept all */);
248 gap_start_scan();
249 return true;
250}
251
253#ifdef USE_BLE_GATT_CLIENT
254 this->scan_desired_ = false;
255#endif
256 if (!this->is_active()) {
257 return; // nothing can be scanning on a stack that is not up
258 }
259 BluetoothLock lock;
260 gap_stop_scan();
261}
262
263#ifdef USE_BLE_GATT_CLIENT
265 if (this->scan_inhibit_count_++ != 0) {
266 return; // another connect attempt already owns the radio
267 }
268 if (this->scan_desired_ && this->is_active()) {
269 BluetoothLock lock;
270 gap_stop_scan();
271 }
272}
273
275 if (this->scan_inhibit_count_ == 0) {
276 return;
277 }
278 this->scan_inhibit_count_--;
279 if (this->scan_inhibit_count_ != 0) {
280 return;
281 }
282 if (this->scan_desired_) {
283 // One physical-start path: scan_start re-applies the remembered params.
284 this->scan_start(this->scan_interval_, this->scan_window_, this->scan_active_mode_);
285 }
286}
287#endif // USE_BLE_GATT_CLIENT
288
289} // namespace esphome::rp2040_ble
290
291#endif // USE_RP2040_BLE
void mark_failed()
Mark this component as failed.
static void packet_handler(uint8_t type, uint16_t channel, uint8_t *packet, uint16_t size)
void scan_stop()
Stop the controller scan (no-op when not scanning).
uint8_t ble_mac_[MAC_ADDRESS_SIZE]
Definition rp2040_ble.h:138
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.
float get_setup_priority() const override
StaticVector< BLEScanListener *, RP2040_BLE_SCAN_LISTENER_COUNT > scan_listeners_
Definition rp2040_ble.h:124
esphome::EventPool< BLEScanReport, MAX_SCAN_REPORT_QUEUE_SIZE - 1 > report_pool_
Definition rp2040_ble.h:133
void inhibit_scan()
Pause the physical scan for the duration of a GATT connect attempt (initiating and scanning contend f...
void get_mac_msb_first(uint8_t out[MAC_ADDRESS_SIZE]) const
Controller BLE address in printable (MSB-first) order, as gap_local_bd_addr() delivers it — note BLES...
esphome::LockFreeQueue< BLEScanReport, MAX_SCAN_REPORT_QUEUE_SIZE > report_queue_
Definition rp2040_ble.h:129
btstack_packet_callback_registration_t hci_event_callback_registration_
Definition rp2040_ble.h:135
void enqueue_scan_report_(const uint8_t *mac_lsb_first, int8_t rssi, uint8_t addr_type, uint8_t adv_event_type, const uint8_t *data, uint16_t data_len)
Buffer one controller report (BTstack packet handler, CYW43 async-context IRQ — bounded copy into the...
btstack_packet_callback_registration_t sm_event_callback_registration_
Definition rp2040_ble.h:136
uint16_t type
bool state
Definition fan.h:2
RP2040BLE * global_ble
constexpr float BLUETOOTH
Definition component.h:48
uint16_t size
Definition helpers.cpp:25
uint32_t * scan_start
One advertisement report from the controller.
Definition rp2040_ble.h:27
uint8_t mac[MAC_ADDRESS_SIZE]
Definition rp2040_ble.h:28
SemaphoreHandle_t lock