[bk72xx_ble] BLE controller support for BK72xx (BLE 5.x) (#17775)

This commit is contained in:
Edvard Filistovič
2026-07-23 08:54:30 +00:00
committed by GitHub
parent 427a504308
commit 33fa9f5069
8 changed files with 327 additions and 2 deletions
+1
View File
@@ -69,6 +69,7 @@ esphome/components/bh1750/* @OttoWinter
esphome/components/bh1900nux/* @B48D81EFCC
esphome/components/binary_sensor/* @esphome/core
esphome/components/bk72xx/* @kuba2k2
esphome/components/bk72xx_ble/* @Bl00d-B0b
esphome/components/bl0906/* @athom-tech @jesserockz @tarontop
esphome/components/bl0939/* @ziceva
esphome/components/bl0940/* @dan-s-github @tobias-
+94
View File
@@ -0,0 +1,94 @@
"""BK72xx BLE — BLE controller support for the BLE-5.x LibreTiny Beken chips.
The platform analog of esp32_ble / rp2040_ble: owns the Beken BDK BLE stack
bring-up and the controller BLE address. Consumers (bk72xx_ble_tracker) build
on this component and contain no SDK calls of their own.
Supported SoCs (BLE 5.x): BK7231N/BK7236 (BLE 5.1), BK7238/BK7252N/BK7253
(BLE 5.2), and any future BLE-5.x SoC. Capability is detected at compile time,
not by a chip list: the C++ guards on `__has_include("ble_api.h")` — the Beken
BLE 5.x public API header, which the LibreTiny beken-72xx builder ships only
for BLE-5.x SoCs. BK7231T/BK7251/BK7271 (BLE 4.2) and BK7231Q (no BLE) fail
with a clear #error.
No framework patch is needed: the LibreTiny beken-72xx builder already compiles
and links the BLE 5.x stack (CFG_SUPPORT_BLE=1 + CFG_BLE_VERSION=BLE_VERSION_5_x;
prebuilt libble_<chip>.a per SoC). This component only calls into it via the
public ble_api.h.
"""
import logging
import esphome.codegen as cg
from esphome.components import libretiny
from esphome.components.libretiny.const import FAMILY_BK7231N, FAMILY_BK7238
import esphome.config_validation as cv
from esphome.const import CONF_ENABLE_ON_BOOT, CONF_ID
from esphome.types import ConfigType
DEPENDENCIES = ["bk72xx"]
CODEOWNERS = ["@Bl00d-B0b"]
_LOGGER = logging.getLogger(__name__)
bk72xx_ble_ns = cg.esphome_ns.namespace("bk72xx_ble")
BK72xxBLE = bk72xx_ble_ns.class_("BK72xxBLE", cg.Component)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(BK72xxBLE),
# Default off: on the single-core BK72xx, bringing the BLE stack up during
# boot competes with the WiFi connection handshake. Consumers enable the
# stack lazily on first use (e.g. the tracker's first scan start).
cv.Optional(CONF_ENABLE_ON_BOOT, default=False): cv.boolean,
}
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT]))
# Enable the BLE stack in the build (the '#h' maps to sys_config.h; the value
# is a list). ESPHome's libretiny platform normally appends CFG_SUPPORT_BLE=0
# on BK7231N/BK7238 (saves ~21KB RAM/~200KB Flash when BLE is unused) to this
# SAME key — and add_platformio_option appends list values, it never replaces.
# The platform therefore skips its disable when this component is configured,
# so this =1 is the single CFG_SUPPORT_BLE define emitted.
cg.add_platformio_option("custom_options.sys_config#h", ["CFG_SUPPORT_BLE=1"])
# Pin the Beken BDK release the BLE 5.x stack is validated against. The
# bundled 3.0.33 has an older BLE header/library layout — and with
# CFG_SUPPORT_BLE=1 the SDK runs its BLE init unconditionally during boot
# (the reason the libretiny platform sets =0 when BLE is unused), so a
# mismatched BDK can crash the device before WiFi comes up regardless of
# enable_on_boot. Pinning here makes a plain config build against the
# validated BDK without any manual platformio_options.
_LOGGER.warning(
"bk72xx_ble builds with beken-bdk 3.0.78 instead of the platform's bundled "
"default: the default's older BLE layout can crash the device at boot when "
"BLE is compiled in"
)
cg.add_platformio_option("custom_versions.beken-bdk", "3.0.78")
# The BDK exposes the controller's BLE address as `common_default_bdaddr` on
# BK7231N, but NOT on BK7238 (its BLE stack has no such symbol; the address is
# derived from the WiFi MAC instead — the BDK's own fallback). Tell the C++
# which path is available so it doesn't reference a missing symbol.
family = libretiny.get_libretiny_family()
if family == FAMILY_BK7231N:
cg.add_define("BK72XX_BLE_HAS_COMMON_BDADDR")
elif family == FAMILY_BK7238:
# ESPHome's LibreTiny disables BLE on BK7238 because the SDK can hang at
# WiFi STA startup when BLE init runs. This component re-enables BLE, so
# warn loudly: BK7238 is accepted but not hardware-verified and may be
# WiFi-unstable with BLE on.
_LOGGER.warning(
"bk72xx_ble on BK7238: enabling BLE is known to risk a WiFi STA startup "
"hang on this family and is not yet hardware-verified. Expect possible "
"instability."
)
cg.add_define("USE_BK72XX_BLE")
@@ -0,0 +1,176 @@
// bk72xx_ble.cpp
//
// BLE controller support for the BK72xx BLE-5.x chips (LibreTiny beken-72xx
// family) — the platform analog of esp32_ble / rp2040_ble. Owns the Beken BDK
// BLE stack bring-up (ble_entry()) and the controller BLE address. Consumers
// (bk72xx_ble_tracker) build on this component and contain no SDK calls of
// their own.
//
// NOTE: the Beken BDK BLE 5.x stack is compiled and linked by the LibreTiny
// beken-72xx builder itself (prebuilt libble_<chip>.a + ble_5_x sources, gated
// on CFG_SUPPORT_BLE / CFG_BLE_VERSION in sys_config.h). This component only
// calls into it via its public API — no framework patch is required.
#include "bk72xx_ble.h" // pulls esphome/core/defines.h for USE_BK72XX_BLE
#ifdef USE_BK72XX_BLE
#include <cstring>
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h" // get_mac_address_raw()
#include "esphome/core/log.h"
// ---------------------------------------------------------------------------
// SDK-capability gate (not a chip allowlist).
// This component drives the Beken BLE *5.x* controller via its public API,
// `ble_api.h`, which the LibreTiny beken-72xx builder ships only for the
// BLE-5.x SoCs (it selects the `ble_pub` 5.x stack from CFG_BLE_VERSION; the
// 4.2 SoCs build a different, older API with no ble_api.h). Gate on the header
// itself so any BLE-5.x Beken chip — present or future — is supported without a
// hard-coded list, and a non-5.x build fails here with a clear message instead
// of a cryptic "ble_api.h: No such file or directory".
// ---------------------------------------------------------------------------
#if defined(CLANG_TIDY)
// The clang-tidy environment does not carry the full Beken BDK BLE 5.x API
// (its ble_api.h variant lacks parts of the 5.x surface), so there is nothing
// accurate to analyze the SDK calls against — skip the file under analysis.
#define BK72XX_BLE_NO_SDK
#elif !__has_include("ble_api.h")
#error \
"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."
#endif
#ifndef BK72XX_BLE_NO_SDK
// ---------------------------------------------------------------------------
// Beken BDK BLE 5.x SDK surface used here. Wrapped in extern "C" because these
// are C symbols consumed from C++.
// ---------------------------------------------------------------------------
extern "C" {
#ifdef BK72XX_BLE_HAS_COMMON_BDADDR
#include "common_bt_defines.h" // struct bd_addr
// The controller's public BLE address, populated by the BDK during ble_entry().
// Present on BK7231N; the other BLE-5.x chips' stacks have no such symbol — there the
// address is derived from the WiFi MAC instead (matching the BDK's own fallback).
extern struct bd_addr common_default_bdaddr;
#endif
// ble_entry() brings up the BDK BLE stack; it is not declared in ble_api.h, so
// declare it here.
void ble_entry(void);
}
namespace esphome::bk72xx_ble {
static const char *const TAG = "bk72xx_ble";
// ---------------------------------------------------------------------------
// Component lifecycle
// ---------------------------------------------------------------------------
void BK72xxBLE::setup() {
// Resolve the MAC early so get_mac_lsb_first() is valid for consumers before
// the stack is up (it is re-read once ble_entry() has run).
this->resolve_mac_();
if (this->enable_on_boot_) {
this->enable();
}
}
// AFTER_WIFI, not BLUETOOTH: replicates the proven pre-split timing — the BDK
// is first touched only once WiFi is up (single-core WiFi/BLE bring-up order).
float BK72xxBLE::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
void BK72xxBLE::enable() {
if (this->state_ != BLEComponentState::STATE_OFF)
return;
this->state_ = BLEComponentState::ENABLING;
// One-time BLE stack init. The BDK has no teardown path — init happens at most once.
ble_entry();
delay(100); // NOLINT — one-time BLE stack init; the SDK needs this settle time
// Re-read the BLE MAC now that the controller is up (common_default_bdaddr is
// populated by ble_entry()); resolve_mac_() may have fallen back earlier.
this->resolve_mac_();
#ifdef BK72XX_BLE_HAS_COMMON_BDADDR
// Liveness heuristic (BK7231N): a healthy ble_entry() populates
// common_default_bdaddr during init, so all-zero after the settle delay
// suggests the stack did not come up. The BDK entry point returns void — no
// return code exists — so warn rather than fail: scan starts against a dead
// stack already fail cleanly downstream (no idle activity handle).
bool bdaddr_live = false;
for (uint8_t b : common_default_bdaddr.addr) {
if (b != 0) {
bdaddr_live = true;
break;
}
}
if (!bdaddr_live)
ESP_LOGW(TAG, "Controller address still unset after init; BLE stack may not have started");
#endif
this->state_ = BLEComponentState::ACTIVE;
ESP_LOGD(TAG, "BLE stack initialised");
}
void BK72xxBLE::get_mac_lsb_first(uint8_t out[6]) const {
for (int i = 0; i < 6; i++)
out[i] = this->ble_mac_[i];
}
void BK72xxBLE::dump_config() {
// ble_mac_ is stored LSB-first (BLE convention); print [5..0] for the
// MSB-first order Home Assistant shows.
ESP_LOGCONFIG(TAG,
"BK72xx BLE:\n"
" MAC address: %02X:%02X:%02X:%02X:%02X:%02X\n"
" Active: %s",
this->ble_mac_[5], this->ble_mac_[4], this->ble_mac_[3], this->ble_mac_[2], this->ble_mac_[1],
this->ble_mac_[0], YESNO(this->is_active()));
}
// ---------------------------------------------------------------------------
// MAC resolution
// ---------------------------------------------------------------------------
void BK72xxBLE::resolve_mac_() {
#ifdef BK72XX_BLE_HAS_COMMON_BDADDR
// BK7231N: the BDK populates common_default_bdaddr (LSB-first, BLE convention)
// during ble_entry(). It may still be zero before the stack is up; if so, fall
// through to the WiFi-derived MAC below.
bool nonzero = false;
for (uint8_t b : common_default_bdaddr.addr) {
if (b != 0) {
nonzero = true;
break;
}
}
if (nonzero) {
memcpy(this->ble_mac_, common_default_bdaddr.addr, 6);
return;
}
#endif
// Chips whose BLE stack does not export common_default_bdaddr (BK7238 and the other
// BLE-5.x SoCs), or BK7231N before the stack is up: derive the BLE MAC exactly as the
// Beken BDK does in bdaddr_env_init() — the WiFi STA MAC with only its last byte
// incremented (sta_mac[5] += 1, a plain byte increment with no carry into the next
// byte), OUI unchanged. This reproduces the address the controller advertises with
// (verified against the BK7231N BLE-5.1 and BK7252N/BK7238 BLE-5.2 SDK sources), so it
// matches on every device, including the last-byte == 0xFF edge that a 24-bit increment
// would carry differently.
uint8_t wifi_mac[6];
get_mac_address_raw(wifi_mac); // MSB-first
const uint8_t ble[6] = {wifi_mac[0], wifi_mac[1], wifi_mac[2],
wifi_mac[3], wifi_mac[4], static_cast<uint8_t>(wifi_mac[5] + 1)};
// Store LSB-first to match the BLE controller's address ordering.
for (int i = 0; i < 6; i++)
this->ble_mac_[i] = ble[5 - i];
}
} // namespace esphome::bk72xx_ble
#endif // BK72XX_BLE_NO_SDK
#endif // USE_BK72XX_BLE
@@ -0,0 +1,44 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_BK72XX_BLE
#include "esphome/core/component.h"
#include <cstdint>
namespace esphome::bk72xx_ble {
enum class BLEComponentState : uint8_t {
STATE_OFF = 0,
ENABLING,
ACTIVE,
};
class BK72xxBLE final : public Component {
public:
void setup() override;
void dump_config() override;
float get_setup_priority() const override;
/// Bring up the BDK BLE stack (one-time; the BDK has no teardown path).
void enable();
bool is_active() const { return this->state_ == BLEComponentState::ACTIVE; }
void set_enable_on_boot(bool enable_on_boot) { this->enable_on_boot_ = enable_on_boot; }
/// Controller BLE address, least-significant octet first (BLE convention).
void get_mac_lsb_first(uint8_t out[6]) const;
protected:
void resolve_mac_();
uint8_t ble_mac_[6]{0}; // LSB-first (BLE convention)
BLEComponentState state_{BLEComponentState::STATE_OFF};
bool enable_on_boot_{false};
};
} // namespace esphome::bk72xx_ble
#endif // USE_BK72XX_BLE
+7 -2
View File
@@ -580,8 +580,13 @@ async def component_to_code(config):
cg.add_platformio_option("custom_fw_name", "esphome")
cg.add_platformio_option("custom_fw_version", __version__)
# Apply chip-specific SDK options to save RAM/Flash
if config[CONF_FAMILY] in (FAMILY_BK7231N, FAMILY_BK7238):
# Apply chip-specific SDK options to save RAM/Flash.
# Skipped when bk72xx_ble is configured: add_platformio_option APPENDS list
# values (it never replaces), so emitting the disable here as well would put
# both CFG_SUPPORT_BLE=0 and =1 into the generated sys_config.h and rely on
# last-wins emission order. Skipping keeps it a single unambiguous define.
ble_requested = "bk72xx_ble" in CORE.config
if config[CONF_FAMILY] in (FAMILY_BK7231N, FAMILY_BK7238) and not ble_requested:
cg.add_platformio_option(
"custom_options.sys_config#h", _BLE5_BK_SYS_CONFIG_OPTIONS
)
+1
View File
@@ -446,6 +446,7 @@
#endif
#ifdef USE_LIBRETINY
#define USE_BK72XX_BLE
#define USE_CAPTIVE_PORTAL
#define USE_SOCKET_IMPL_LWIP_SOCKETS
#define USE_LWIP_FAST_SELECT
+2
View File
@@ -0,0 +1,2 @@
bk72xx_ble:
enable_on_boot: true
@@ -0,0 +1,2 @@
packages:
bk72xx_ble: !include common.yaml