From 33fa9f5069e6b79e0f3f90e69f0f976e9e139d2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edvard=20Filistovi=C4=8D?= Date: Thu, 23 Jul 2026 11:54:30 +0300 Subject: [PATCH] [bk72xx_ble] BLE controller support for BK72xx (BLE 5.x) (#17775) --- CODEOWNERS | 1 + esphome/components/bk72xx_ble/__init__.py | 94 ++++++++++ esphome/components/bk72xx_ble/bk72xx_ble.cpp | 176 ++++++++++++++++++ esphome/components/bk72xx_ble/bk72xx_ble.h | 44 +++++ esphome/components/libretiny/__init__.py | 9 +- esphome/core/defines.h | 1 + tests/components/bk72xx_ble/common.yaml | 2 + .../bk72xx_ble/validate.bk72xx-ard.yaml | 2 + 8 files changed, 327 insertions(+), 2 deletions(-) create mode 100644 esphome/components/bk72xx_ble/__init__.py create mode 100644 esphome/components/bk72xx_ble/bk72xx_ble.cpp create mode 100644 esphome/components/bk72xx_ble/bk72xx_ble.h create mode 100644 tests/components/bk72xx_ble/common.yaml create mode 100644 tests/components/bk72xx_ble/validate.bk72xx-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index b73ed319c8..fe09bd96cf 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -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- diff --git a/esphome/components/bk72xx_ble/__init__.py b/esphome/components/bk72xx_ble/__init__.py new file mode 100644 index 0000000000..29e2a6b13d --- /dev/null +++ b/esphome/components/bk72xx_ble/__init__.py @@ -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_.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") diff --git a/esphome/components/bk72xx_ble/bk72xx_ble.cpp b/esphome/components/bk72xx_ble/bk72xx_ble.cpp new file mode 100644 index 0000000000..db6b665ba6 --- /dev/null +++ b/esphome/components/bk72xx_ble/bk72xx_ble.cpp @@ -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_.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 + +#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(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 diff --git a/esphome/components/bk72xx_ble/bk72xx_ble.h b/esphome/components/bk72xx_ble/bk72xx_ble.h new file mode 100644 index 0000000000..a327f7cbd8 --- /dev/null +++ b/esphome/components/bk72xx_ble/bk72xx_ble.h @@ -0,0 +1,44 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_BK72XX_BLE + +#include "esphome/core/component.h" + +#include + +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 diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index 62cef331fd..97c0fd455b 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -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 ) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index ca1d22bf3e..03175bf2cc 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -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 diff --git a/tests/components/bk72xx_ble/common.yaml b/tests/components/bk72xx_ble/common.yaml new file mode 100644 index 0000000000..5ada71a141 --- /dev/null +++ b/tests/components/bk72xx_ble/common.yaml @@ -0,0 +1,2 @@ +bk72xx_ble: + enable_on_boot: true diff --git a/tests/components/bk72xx_ble/validate.bk72xx-ard.yaml b/tests/components/bk72xx_ble/validate.bk72xx-ard.yaml new file mode 100644 index 0000000000..e5009aa940 --- /dev/null +++ b/tests/components/bk72xx_ble/validate.bk72xx-ard.yaml @@ -0,0 +1,2 @@ +packages: + bk72xx_ble: !include common.yaml