Enforce the hub surface with a concept

This commit is contained in:
J. Nick Koston
2026-08-08 02:00:17 -05:00
parent 8fae4f6767
commit 7fe447a4a2
4 changed files with 38 additions and 87 deletions
+16 -2
View File
@@ -86,8 +86,8 @@ struct HubCapabilities {
bool scan_mode_switch;
};
// The BLEHub method surface. Duck-typed: a tracker that misses one of these
// fails to compile at the consumer's call site, not at a class definition.
// The BLEHub method surface, enforced by the BLEHubContract concept below
// (asserted where ble_hub_impl.h binds the alias).
//
// /// Register a parsed-advertisement consumer (BLE sensors, automation triggers).
// void register_listener(ESPBTDeviceListener *listener);
@@ -118,4 +118,18 @@ struct HubCapabilities {
// /// consumers can gate features on the switch without probing.
// bool request_scan_mode(bool active);
// set_scanner_state_callback is deliberately absent: it exists only on push
// hubs under USE_BLE_SCANNER_STATE_CALLBACK.
template<typename T>
concept BLEHubContract = requires(T hub, ESPBTDeviceListener *listener, RawAdvertisementCallback raw_callback,
uint8_t *mac) {
hub.register_listener(listener);
hub.set_raw_advertisement_callback(raw_callback);
T::get_capabilities();
hub.get_adapter_mac(mac);
hub.scan_running();
hub.scan_active();
hub.request_scan_mode(true);
};
} // namespace esphome::ble_device_base
@@ -6,28 +6,30 @@
#pragma once
#include "ble_hub.h"
#include "esphome/core/defines.h"
#if defined(USE_ESP32_BLE_TRACKER)
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
#define ESPHOME_BLE_HUB_TYPE esp32_ble_tracker::ESP32BLETracker
#elif defined(USE_RP2_BLE_TRACKER)
#include "esphome/components/rp2_ble_tracker/rp2_ble_tracker.h"
#define ESPHOME_BLE_HUB_TYPE rp2_ble_tracker::RP2BLETracker
#elif defined(USE_BK72XX_BLE_TRACKER)
#include "esphome/components/bk72xx_ble_tracker/bk72xx_ble_tracker.h"
#define ESPHOME_BLE_HUB_TYPE bk72xx_ble_tracker::BK72xxBLETracker
#elif defined(USE_LN882H_BLE_TRACKER)
#include "esphome/components/ln882h_ble_tracker/ln882h_ble_tracker.h"
#define ESPHOME_BLE_HUB_TYPE ln882h_ble_tracker::LN882HBLETracker
#endif
// No #else on purpose: builds without a tracker (host unit tests) get no BLEHub.
namespace esphome::ble_device_base {
#if defined(USE_ESP32_BLE_TRACKER)
using BLEHub = esp32_ble_tracker::ESP32BLETracker;
#elif defined(USE_RP2_BLE_TRACKER)
using BLEHub = rp2_ble_tracker::RP2BLETracker;
#elif defined(USE_BK72XX_BLE_TRACKER)
using BLEHub = bk72xx_ble_tracker::BK72xxBLETracker;
#elif defined(USE_LN882H_BLE_TRACKER)
using BLEHub = ln882h_ble_tracker::LN882HBLETracker;
#ifdef ESPHOME_BLE_HUB_TYPE
using BLEHub = ESPHOME_BLE_HUB_TYPE;
static_assert(BLEHubContract<BLEHub>, "The build's BLE tracker is missing part of the BLEHub surface (ble_hub.h)");
#undef ESPHOME_BLE_HUB_TYPE
#endif
} // namespace esphome::ble_device_base
+12 -6
View File
@@ -252,12 +252,12 @@
// platforms whose API/network types the proxy header cannot assume.
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_RP2)
#define USE_BLUETOOTH_PROXY
#define USE_BLE_SCANNER_STATE_CALLBACK
// Mirror the codegen values per platform: _to_code_esp32() emits the connection
// count (default 3), _to_code_ble_hub() emits the slot count (1 on rp2, 0 on
// advertisement-only hubs) — so static analysis checks the same
// std::array<uint64_t, N> instantiation a real build produces.
// count (default 3) and the scanner-state push slot, _to_code_ble_hub() emits
// the slot count (1 on rp2, 0 on advertisement-only hubs) — so static analysis
// checks the same instantiations a real build produces.
#ifdef USE_ESP32
#define USE_BLE_SCANNER_STATE_CALLBACK
#define BLUETOOTH_PROXY_MAX_CONNECTIONS 3
#elif defined(USE_RP2)
#define BLUETOOTH_PROXY_MAX_CONNECTIONS 1
@@ -488,11 +488,17 @@
#ifdef USE_LIBRETINY
#define USE_BK72XX_BLE
#define USE_BK72XX_BLE_TRACKER
#define BK72XX_BLE_SCAN_LISTENER_COUNT 1
#define USE_LN882H_BLE
#define USE_LN882H_BLE_TRACKER
#define LN882H_BLE_SCAN_LISTENER_COUNT 1
// One tracker arm per build: ln882x gets its real hub; bk72xx also stands in
// for hub-less LibreTiny chips (rtl87xx) so bluetooth_proxy.h has a BLEHub
// to parse against.
#ifdef USE_LN882X
#define USE_LN882H_BLE_TRACKER
#else
#define USE_BK72XX_BLE_TRACKER
#endif
#define ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT 1
#define USE_CAPTIVE_PORTAL
#define USE_WIFI_SCAN_RESULTS_LOCK
@@ -1,71 +0,0 @@
#include <gtest/gtest.h>
#include <cstdint>
#include "esphome/components/ble_device_base/ble_hub.h"
namespace esphome::ble_device_base::testing {
// Pins the request_scan_mode() contract documented in ble_hub.h: a hub
// without a mode switch refuses without changing any state, a switching hub
// honors and applies. Minimal host hubs mirror the in-tree tracker stubs.
namespace {
class RefusingHub {
public:
static constexpr HubCapabilities get_capabilities() { return {false, false, false}; }
bool scan_active() { return this->active_; }
// The refusing shape every non-switching tracker provides: no state touched.
bool request_scan_mode(bool active) { return false; }
protected:
bool active_{true};
};
class SwitchingHub : public RefusingHub {
public:
static constexpr HubCapabilities get_capabilities() { return {true, false, false, /* scan_mode_switch = */ true}; }
bool request_scan_mode(bool active) {
this->active_ = active;
return true;
}
};
// The esp32 shape: the controller supports active scanning but the hub keeps
// the refusing stub (mode is driven through its own tracker API).
class CapableRefusingHub : public RefusingHub {
public:
static constexpr HubCapabilities get_capabilities() { return {true, false, false}; }
};
} // namespace
TEST(BLEHubScanModeRequest, RefusingHubChangesNothing) {
RefusingHub hub;
EXPECT_TRUE(hub.scan_active());
EXPECT_FALSE(hub.request_scan_mode(false));
// Refused, not applied-and-reported-false: the state is untouched.
EXPECT_TRUE(hub.scan_active());
EXPECT_FALSE(hub.request_scan_mode(true));
EXPECT_TRUE(hub.scan_active());
}
TEST(BLEHubScanModeRequest, SwitchingHubHonorsAndApplies) {
SwitchingHub hub;
EXPECT_TRUE(hub.get_capabilities().scan_mode_switch);
EXPECT_TRUE(hub.request_scan_mode(true));
EXPECT_TRUE(hub.scan_active());
EXPECT_TRUE(hub.request_scan_mode(false));
EXPECT_FALSE(hub.scan_active());
}
TEST(BLEHubScanModeRequest, CapabilityAndSwitchAreIndependent) {
CapableRefusingHub hub;
EXPECT_TRUE(hub.get_capabilities().active_scan);
// The esp32 shape advertises no runtime switch, and the request refuses.
EXPECT_FALSE(hub.get_capabilities().scan_mode_switch);
EXPECT_FALSE(hub.request_scan_mode(false));
EXPECT_TRUE(hub.scan_active());
}
} // namespace esphome::ble_device_base::testing