ESPHome 2026.8.0-dev
Loading...
Searching...
No Matches
bk72xx_ble.cpp
Go to the documentation of this file.
1// bk72xx_ble.cpp
2//
3// BLE controller support for the BK72xx BLE-5.x chips (LibreTiny beken-72xx
4// family) — the platform analog of esp32_ble / rp2040_ble. Owns everything that
5// talks to the Beken BDK BLE stack:
6// - one-time stack bring-up (ble_set_notice_cb() + ble_entry()),
7// - the controller BLE address,
8// - the raw controller scan primitives (bk_ble_scan_start/stop),
9// - the scan-report ring: the BDK notice callback (BLE task) takes a report
10// from a fixed pool and pushes it on a lock-free SPSC queue; loop() drains,
11// dispatches on the main task and returns reports to the pool — the same
12// EventPool + LockFreeQueue handoff esp32_ble uses, zero allocation at
13// steady state.
14// Consumers contain no SDK calls of their own.
15//
16// NOTE: the Beken BDK BLE 5.x stack is compiled and linked by the LibreTiny
17// beken-72xx builder itself (prebuilt libble_<chip>.a + ble_5_x sources, gated
18// on CFG_SUPPORT_BLE / CFG_BLE_VERSION in sys_config.h). This component only
19// calls into it via the public ble_api.h — no framework patch is required.
20
21#include "bk72xx_ble.h" // pulls esphome/core/defines.h for USE_BK72XX_BLE
22
23#ifdef USE_BK72XX_BLE
24
25#include <cstring>
26
27#include "esphome/core/hal.h"
28#include "esphome/core/helpers.h" // get_mac_address_raw()
29#include "esphome/core/log.h"
30
31// ---------------------------------------------------------------------------
32// SDK-capability gate (not a chip allowlist).
33// This component drives the Beken BLE *5.x* controller via its public API,
34// `ble_api.h`, which the LibreTiny beken-72xx builder ships only for the
35// BLE-5.x SoCs (it selects the `ble_pub` 5.x stack from CFG_BLE_VERSION; the
36// 4.2 SoCs build a different, older API with no ble_api.h). Gate on the header
37// itself so any BLE-5.x Beken chip — present or future — is supported without a
38// hard-coded list, and a non-5.x build fails here with a clear message instead
39// of a cryptic "ble_api.h: No such file or directory".
40// ---------------------------------------------------------------------------
41#if defined(CLANG_TIDY)
42// The clang-tidy environment does not carry the full Beken BDK BLE 5.x API
43// (its ble_api.h variant lacks parts of the 5.x surface), so there is nothing
44// accurate to analyze the SDK calls against — skip the file under analysis.
45#define BK72XX_BLE_NO_SDK
46#elif !__has_include("ble_api.h")
47#error \
48 "bk72xx_ble requires a BLE 5.x Beken SDK (ble_api.h). Supported SoCs: BK7231N/BK7236 (BLE 5.1) and BK7238/BK7252N/BK7253 (BLE 5.2). BK7231T/BK7251/BK7271 (BLE 4.2) and BK7231Q (no BLE) are not supported."
49#endif
50
51#ifndef BK72XX_BLE_NO_SDK
52
53// ---------------------------------------------------------------------------
54// Beken BDK BLE 5.x SDK — public API.
55// Exposed on the include path by the LibreTiny beken-72xx builder
56// (cores/.../ble_5_x_rw + driver/include). Wrapped in extern "C" because these
57// are C headers consumed from C++ (a standard C-header-from-C++ pattern).
58// ---------------------------------------------------------------------------
59extern "C" {
60#include "ble_api.h" // bk_ble_scan_start/stop, ble_entry, ble_set_notice_cb,
61 // app_ble_get_idle_actv_idx_handle, struct scan_param,
62 // recv_adv_t, ble_notice_t, BLE_5_REPORT_ADV, SCAN_ACTV
63#ifdef BK72XX_BLE_HAS_COMMON_BDADDR
64#include "common_bt_defines.h" // struct bd_addr
65// The controller's public BLE address, populated by the BDK during ble_entry().
66// Present on BK7231N; the other BLE-5.x chips' stacks have no such symbol — there the
67// address is derived from the WiFi MAC instead (matching the BDK's own fallback).
68extern struct bd_addr common_default_bdaddr;
69#endif
70// ble_entry() brings up the BDK BLE stack; it is not declared in ble_api.h, so
71// declare it here.
72void ble_entry(void);
73}
74
76
77static const char *const TAG = "bk72xx_ble";
78
79// The BDK notice callback is a plain C function pointer with no user argument,
80// so it reaches the (single) component instance through a file-static pointer.
81static BK72xxBLE *s_ble = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
82
83// ---------------------------------------------------------------------------
84// BLE notice callback — runs in the BDK BLE task context.
85// The BK controller reports every advertisement as a BLE_5_REPORT_ADV notice
86// carrying a recv_adv_t. Copy it into the queue and return; all dispatch
87// happens in loop() on the main task.
88// ---------------------------------------------------------------------------
89static void ble_notice_callback(ble_notice_t notice, void *param) {
90 if (s_ble == nullptr || param == nullptr)
91 return;
92 if (notice != BLE_5_REPORT_ADV)
93 return;
94
95 const recv_adv_t *info = reinterpret_cast<const recv_adv_t *>(param);
96 // rssi is a signed dBm carried in a uint8_t; cast through int8_t (standard for
97 // a signed dBm value packed in a uint8_t).
98 s_ble->enqueue_scan_report(info->adv_addr, static_cast<int8_t>(info->rssi), info->adv_addr_type, info->data,
99 info->data_len);
100}
101
102void BK72xxBLE::enqueue_scan_report(const uint8_t *mac, int8_t rssi, uint8_t addr_type, const uint8_t *data,
103 uint16_t data_len) {
104 BLEScanReport *report = this->report_pool_.allocate();
105 if (report == nullptr) {
106 // Pool exhausted — the queue is full; count and drop.
107 this->report_queue_.increment_dropped_count();
108 return;
109 }
110 memcpy(report->mac, mac, 6);
111 report->rssi = rssi;
112 report->addr_type = addr_type;
113 report->data_len =
114 (data_len <= sizeof(report->data)) ? static_cast<uint8_t>(data_len) : static_cast<uint8_t>(sizeof(report->data));
115 memcpy(report->data, data, report->data_len);
116 // Cannot fail: the pool is sized to the queue capacity.
117 this->report_queue_.push(report);
118}
119
120// ---------------------------------------------------------------------------
121// Component lifecycle
122// ---------------------------------------------------------------------------
123
125 s_ble = this;
126 // Resolve the MAC early so get_mac_lsb_first() is valid for consumers before
127 // the stack is up (it is re-read once ble_entry() has run).
128 this->resolve_mac_();
129 if (this->enable_on_boot_) {
130 this->enable();
131 }
132}
133
134// AFTER_WIFI, not BLUETOOTH: replicates the proven pre-split timing — the BDK
135// is first touched only once WiFi is up (single-core WiFi/BLE bring-up order).
137
140 return;
142
143 // One-time BLE stack init: register the notice callback, then bring up the
144 // BDK BLE stack. The BDK has no teardown path — init happens at most once.
145 ble_set_notice_cb(ble_notice_callback);
146 ble_entry();
147
148 delay(100); // NOLINT — one-time BLE stack init; the SDK needs this settle time
149
150 // Re-read the BLE MAC now that the controller is up (common_default_bdaddr is
151 // populated by ble_entry()); resolve_mac_() may have fallen back earlier.
152 this->resolve_mac_();
153
154#ifdef BK72XX_BLE_HAS_COMMON_BDADDR
155 // Liveness heuristic (BK7231N): a healthy ble_entry() populates
156 // common_default_bdaddr during init, so all-zero after the settle delay
157 // suggests the stack did not come up. The BDK entry point returns void — no
158 // return code exists — so warn rather than fail: scan starts against a dead
159 // stack already fail cleanly downstream (no idle activity handle).
160 bool bdaddr_live = false;
161 for (uint8_t b : common_default_bdaddr.addr) {
162 if (b != 0) {
163 bdaddr_live = true;
164 break;
165 }
166 }
167 if (!bdaddr_live)
168 ESP_LOGW(TAG, "Controller address still unset after init; BLE stack may not have started");
169#endif
170
172 ESP_LOGD(TAG, "BLE stack initialised");
173}
174
176 // Drain the lock-free ring filled by the BLE task; all per-report work runs
177 // here on the main task, then the report returns to the pool.
178 BLEScanReport *report = this->report_queue_.pop();
179 if (report == nullptr)
180 return;
181 do {
182 for (auto *listener : this->scan_listeners_)
183 listener->on_scan_report(*report);
184 this->report_pool_.release(report);
185 } while ((report = this->report_queue_.pop()) != nullptr);
186
187 // Log dropped reports — only reachable when reports were processed; drops can
188 // only occur while the queue is full, and only this loop drains it.
189 uint16_t dropped = this->report_queue_.get_and_reset_dropped_count();
190 if (dropped > 0)
191 ESP_LOGW(TAG, "Dropped %u scan reports due to queue overflow", dropped);
192}
193
194void BK72xxBLE::get_mac_lsb_first(uint8_t out[6]) const {
195 for (int i = 0; i < 6; i++)
196 out[i] = this->ble_mac_[i];
197}
198
200 // ble_mac_ is stored LSB-first (BLE convention); print [5..0] for the
201 // MSB-first order Home Assistant shows.
202 ESP_LOGCONFIG(TAG,
203 "BK72xx BLE:\n"
204 " MAC address: %02X:%02X:%02X:%02X:%02X:%02X\n"
205 " Active: %s",
206 this->ble_mac_[5], this->ble_mac_[4], this->ble_mac_[3], this->ble_mac_[2], this->ble_mac_[1],
207 this->ble_mac_[0], YESNO(this->is_active()));
208}
209
210// ---------------------------------------------------------------------------
211// MAC resolution
212// ---------------------------------------------------------------------------
213
215#ifdef BK72XX_BLE_HAS_COMMON_BDADDR
216 // BK7231N: the BDK populates common_default_bdaddr (LSB-first, BLE convention)
217 // during ble_entry(). It may still be zero before the stack is up; if so, fall
218 // through to the WiFi-derived MAC below.
219 bool nonzero = false;
220 for (uint8_t b : common_default_bdaddr.addr) {
221 if (b != 0) {
222 nonzero = true;
223 break;
224 }
225 }
226 if (nonzero) {
227 memcpy(this->ble_mac_, common_default_bdaddr.addr, 6);
228 return;
229 }
230#endif
231 // Chips whose BLE stack does not export common_default_bdaddr (BK7238 and the other
232 // BLE-5.x SoCs), or BK7231N before the stack is up: derive the BLE MAC exactly as the
233 // Beken BDK does in bdaddr_env_init() — the WiFi STA MAC with only its last byte
234 // incremented (sta_mac[5] += 1, a plain byte increment with no carry into the next
235 // byte), OUI unchanged. This reproduces the address the controller advertises with
236 // (verified against the BK7231N BLE-5.1 and BK7252N/BK7238 BLE-5.2 SDK sources), so it
237 // matches on every device, including the last-byte == 0xFF edge that a 24-bit increment
238 // would carry differently.
239 uint8_t wifi_mac[6];
240 get_mac_address_raw(wifi_mac); // MSB-first
241 const uint8_t ble[6] = {wifi_mac[0], wifi_mac[1], wifi_mac[2],
242 wifi_mac[3], wifi_mac[4], static_cast<uint8_t>(wifi_mac[5] + 1)};
243 // Store LSB-first to match recv_adv_t adv_addr ordering.
244 for (int i = 0; i < 6; i++)
245 this->ble_mac_[i] = ble[5 - i];
246}
247
248// ---------------------------------------------------------------------------
249// Controller scan primitives
250// ---------------------------------------------------------------------------
251
252bool BK72xxBLE::scan_start(uint16_t interval, uint16_t window) {
253 if (!this->is_active())
254 this->enable();
255
256 if (this->scan_actv_idx_ != 0xFF) {
257 // Already scanning — stop first so this call cleanly restarts with the new
258 // parameters (the BDK cannot start a second scan on a busy activity).
259 this->scan_stop();
260 }
261
262 struct scan_param sp;
263 memset(&sp, 0, sizeof(sp));
264 sp.channel_map = 7; // advertising channels 37/38/39
265 sp.interval = interval;
266 sp.window = window;
267
268 this->scan_actv_idx_ = app_ble_get_idle_actv_idx_handle(SCAN_ACTV);
269 if (this->scan_actv_idx_ == 0xFF) {
270 ESP_LOGE(TAG, "Scan start failed: no idle activity handle");
271 return false;
272 }
273 ble_err_t ret = bk_ble_scan_start(this->scan_actv_idx_, &sp, nullptr);
274 if (ret != ERR_SUCCESS) {
275 ESP_LOGE(TAG, "Scan start failed (err %d)", static_cast<int>(ret));
276 this->scan_actv_idx_ = 0xFF;
277 return false;
278 }
279 return true;
280}
281
283 if (this->scan_actv_idx_ != 0xFF) {
284 bk_ble_scan_stop(this->scan_actv_idx_, nullptr);
285 this->scan_actv_idx_ = 0xFF;
286 }
287}
288
289} // namespace esphome::bk72xx_ble
290
291#endif // BK72XX_BLE_NO_SDK
292#endif // USE_BK72XX_BLE
struct bd_addr common_default_bdaddr
void ble_entry(void)
std::vector< BLEScanListener * > scan_listeners_
Definition bk72xx_ble.h:81
void enqueue_scan_report(const uint8_t *mac, int8_t rssi, uint8_t addr_type, const uint8_t *data, uint16_t data_len)
Internal: buffer one controller report (BDK notice callback, BLE task context — bounded copy under th...
void get_mac_lsb_first(uint8_t out[6]) const
Controller BLE address, least-significant octet first (BLE convention).
float get_setup_priority() const override
void enable()
Bring up the BDK BLE stack (one-time; the BDK has no teardown path).
bool scan_start(uint16_t interval, uint16_t window)
Start the controller scan.
esphome::LockFreeQueue< BLEScanReport, MAX_SCAN_REPORT_QUEUE_SIZE > report_queue_
Definition bk72xx_ble.h:85
esphome::EventPool< BLEScanReport, MAX_SCAN_REPORT_QUEUE_SIZE - 1 > report_pool_
Definition bk72xx_ble.h:89
void scan_stop()
Stop the controller scan (no-op when not scanning).
constexpr float AFTER_WIFI
For components that should be initialized after WiFi is connected.
Definition component.h:55
void get_mac_address_raw(uint8_t *mac)
Get the device MAC address as raw bytes, written into the provided byte array (6 bytes).
Definition helpers.cpp:74
void HOT delay(uint32_t ms)
Definition hal.cpp:85
uint32_t sp
One advertisement report from the controller.
Definition bk72xx_ble.h:23