From 2e1c517821067632512ed45c00ba603f2c1a76a0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Aug 2026 13:51:41 -0500 Subject: [PATCH] [bluetooth_proxy] Enable active connections on rp2 (#18132) --- .../components/bluetooth_proxy/__init__.py | 172 +++++++++++++++--- esphome/components/rp2040_ble/__init__.py | 2 + .../components/rp2_ble_tracker/__init__.py | 3 +- esphome/core/defines.h | 7 +- .../test_idf_max_connections_mirror.py | 10 +- .../test_outer_schema_mirror.py | 6 +- .../bluetooth_proxy/test_platform_gates.py | 100 +++++++++- .../bluetooth_connection/common.yaml | 8 + .../validate.rp2040-ard.yaml | 11 ++ .../test-passive.rp2040-ard.yaml | 11 ++ .../bluetooth_proxy/test.rp2040-ard.yaml | 4 +- 11 files changed, 287 insertions(+), 47 deletions(-) create mode 100644 tests/components/bluetooth_connection/common.yaml create mode 100644 tests/components/bluetooth_connection/validate.rp2040-ard.yaml create mode 100644 tests/components/bluetooth_proxy/test-passive.rp2040-ard.yaml diff --git a/esphome/components/bluetooth_proxy/__init__.py b/esphome/components/bluetooth_proxy/__init__.py index ed6dbfe557e..057b15193a1 100644 --- a/esphome/components/bluetooth_proxy/__init__.py +++ b/esphome/components/bluetooth_proxy/__init__.py @@ -59,15 +59,17 @@ _LOGGER = logging.getLogger(__name__) CONF_CONNECTION_SLOTS = "connection_slots" CONF_CACHE_SERVICES = "cache_services" CONF_CONNECTIONS = "connections" +CONF_BACKEND_ID = "backend_id" DEFAULT_CONNECTION_SLOTS = 3 bluetooth_proxy_ns = cg.esphome_ns.namespace("bluetooth_proxy") BluetoothProxy = bluetooth_proxy_ns.class_("BluetoothProxy", cg.Component) -# Mirrors esp32_ble.IDF_MAX_CONNECTIONS as a literal so the statically walkable -# CONFIG_SCHEMA below can state the connection_slots range without importing the -# esp32 BLE stack. tests/component_tests/bluetooth_proxy/ pins the two together. +# Mirrors esp32_ble.IDF_MAX_CONNECTIONS (the loosest platform cap): the esp32 +# schema builder asserts the two agree, tests/component_tests/bluetooth_proxy/ +# pins them together, and the outer walkable schema uses it as the +# connection_slots bound (per-platform schemas tighten it). _IDF_MAX_CONNECTIONS = 9 @@ -147,17 +149,99 @@ def _validate_no_active(config: ConfigType) -> ConfigType: return config -# Advertisement-only proxy on a neutral BLE hub: the hub's raw-advertisement -# callback feeds the same API batching. GATT/active connections are excluded at -# compile time — only the esp32 build compiles the connection stack; nothing -# reads HubCapabilities::gatt at runtime for this today. -# Keys both platform schemas must declare identically; each arm spreads this -# dict so the shared surface cannot drift. CONF_ACTIVE deliberately stays -# per-arm: its default differs (esp32 True, hub arms False — no GATT). +@functools.cache +def _rp2_config_schema() -> cv.All: + """Full proxy on the rp2 BLE hub: active connections through the BTstack + GATT client backend in bluetooth_connection. The slot limit comes from the + prebuilt BTstack library (one connection today); the code is built for N.""" + from esphome.components import rp2040_ble + + connection_schema = cv.Schema( + { + cv.GenerateID(): cv.declare_id(bluetooth_connection.HubBluetoothConnection), + cv.GenerateID(CONF_BACKEND_ID): cv.declare_id( + bluetooth_connection.RP2GattClient + ), + } + ) + + def populate_connections(config: ConfigType) -> ConfigType: + # One wrapper + backend pair per slot, declared during validation so + # their ids exist for codegen (the esp32 arm's `connections` pattern). + if not config[CONF_ACTIVE]: + return config + return { + **config, + CONF_CONNECTIONS: [ + connection_schema({}) for _ in range(config[CONF_CONNECTION_SLOTS]) + ], + } + + max_conn = bluetooth_connection.HUB_MAX_CONNECTIONS[PLATFORM_RP2] + schema = ( + cv.Schema( + { + **_COMMON_SCHEMA_KEYS, + # The GATT backend drives the controller directly (connect, GATT + # ops), not through the tracker hub. + cv.GenerateID(rp2040_ble.CONF_RP2040_BLE_ID): cv.use_id( + rp2040_ble.RP2040BLE + ), + cv.Optional(CONF_ACTIVE, default=True): cv.boolean, + cv.Optional( + CONF_CONNECTION_SLOTS, + default=min(DEFAULT_CONNECTION_SLOTS, max_conn), + ): cv.All( + cv.positive_int, + cv.Range( + min=1, + max=max_conn, + msg=f"rp2 supports at most {max_conn} connection slot(s); " + "the framework's BTstack library is built with " + f"MAX_NR_GATT_CLIENTS {max_conn}", + ), + ), + } + ) + .extend( + # ble_hub_id with the friendly no-tracker-configured guard. + ble_device_base.BLE_DEVICE_SCHEMA + ) + .extend(cv.COMPONENT_SCHEMA) + ) + return cv.All(schema, populate_connections) + + +async def _rp2_connections_to_code(var: cg.MockObj, config: ConfigType) -> None: + from esphome.components import rp2040_ble + + # One wrapper + backend pair per slot (the esp32 arm's pattern). + for connection_conf in config[CONF_CONNECTIONS]: + ble_device_base.request_gatt_client() + backend = cg.new_Pvariable(connection_conf[CONF_BACKEND_ID]) + await cg.register_component(backend, connection_conf) + await cg.register_parented(backend, config[rp2040_ble.CONF_RP2040_BLE_ID]) + connection = cg.new_Pvariable(connection_conf[CONF_ID]) + cg.add(connection.set_backend(backend)) + cg.add(var.register_connection(connection)) + + +# Per-platform schema builders and connection codegen; every key of +# bluetooth_connection.HUB_MAX_CONNECTIONS needs an entry in both (pinned by +# tests/component_tests/bluetooth_proxy/). +_GATT_HUB_SCHEMAS = {PLATFORM_RP2: _rp2_config_schema} +_GATT_HUB_TO_CODE = {PLATFORM_RP2: _rp2_connections_to_code} + + +# Keys every platform arm declares identically; each arm spreads this dict so +# the shared surface cannot drift. CONF_ACTIVE stays per-arm: its default +# differs (esp32 True, rp2 True, advertisement-only False). _COMMON_SCHEMA_KEYS = { cv.GenerateID(): cv.declare_id(BluetoothProxy), } +# Advertisement-only proxy on a neutral BLE hub: the hub's raw-advertisement +# callback feeds the same API batching, no connection stack compiled. _BLE_HUB_CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -178,9 +262,10 @@ _BLE_HUB_CONFIG_SCHEMA = cv.All( def _validate_platform(config: ConfigType) -> ConfigType: """Apply the schema for the platform actually being compiled. - esp32 keeps the full GATT proxy; every other platform gets the - advertisement-only shape, which rejects the connection-oriented options - above because its schema does not define them. + Three-way dispatch: esp32 gets the full GATT proxy, HUB_MAX_CONNECTIONS + platforms get their _GATT_HUB_SCHEMAS arm, the remaining hub platforms get + the advertisement-only shape; unsupported keys were already rejected by + name in _reject_unsupported_connection_keys. """ if config is SCHEMA_EXTRACT: # The language-schema dumper runs without a platform. Expose the esp32 @@ -196,18 +281,21 @@ def _validate_platform(config: ConfigType) -> ConfigType: raise cv.Invalid( f"bluetooth_proxy is not supported on {CORE.target_platform}: no " "active-scan-capable BLE tracker hub is available for this " - "platform. It runs on esp32 (full proxy), and the ln882x and rp2 " - "families (advertisement-only)." + "platform. It runs on esp32 and rp2 (full proxy) and the ln882x " + "family (advertisement-only)." ) + if CORE.target_platform in bluetooth_connection.HUB_MAX_CONNECTIONS: + return _GATT_HUB_SCHEMAS[CORE.target_platform]()(config) return _BLE_HUB_CONFIG_SCHEMA(config) -def _reject_connection_keys_off_esp32(config: ConfigType) -> ConfigType: - """Reject connection-oriented options by name on hub-only platforms. +def _reject_unsupported_connection_keys(config: ConfigType) -> ConfigType: + """Reject connection options a platform does not support, by name. - Runs before the walkable schema below so the user gets "this option does - not exist here" instead of the option's esp32 value range (which would - imply a smaller number is accepted). + GATT hub platforms keep connection_slots but reject the esp32-only keys; + advertisement-only hubs reject all three. Runs before the walkable schema + below so the user gets "this option does not exist here" instead of a + value-range error implying the option works. """ if not isinstance(config, dict) or CORE.is_esp32 or CORE.target_platform is None: return config @@ -216,14 +304,28 @@ def _reject_connection_keys_off_esp32(config: ConfigType) -> ConfigType: # reports "not supported on {platform}" instead of a key-level message # implying an advertisement-only proxy is available. return config - for key in (CONF_CONNECTION_SLOTS, CONF_CACHE_SERVICES, CONF_CONNECTIONS): + if CORE.target_platform in bluetooth_connection.HUB_MAX_CONNECTIONS: + # Full proxy: connection_slots is real here; the per-connection list + # exists internally but carries no user options, and the Bluedroid + # NVS service cache is esp32-only. + rejected = { + CONF_CONNECTIONS: ( + "has no per-connection options on this platform; use " + "'connection_slots' to set the count" + ), + CONF_CACHE_SERVICES: "is esp32-only (Bluedroid NVS service cache)", + } + else: + reason = ( + "requires active connection support; this platform runs the " + "advertisement-only proxy and has no such option" + ) + rejected = dict.fromkeys( + (CONF_CONNECTION_SLOTS, CONF_CACHE_SERVICES, CONF_CONNECTIONS), reason + ) + for key, reason in rejected.items(): if key in config: - raise cv.Invalid( - f"'{key}' requires active connection support, which needs the " - "esp32 GATT stack; this platform runs the advertisement-only " - "proxy and has no such option", - path=[key], - ) + raise cv.Invalid(f"'{key}' {reason}", path=[key]) return config @@ -241,11 +343,14 @@ def _reject_connection_keys_off_esp32(config: ConfigType) -> ConfigType: # rejects it as empty. extra=ALLOW_EXTRA passes `connections` through untouched # for _ESP32_CONFIG_SCHEMA to validate exactly once. CONFIG_SCHEMA = cv.All( - _reject_connection_keys_off_esp32, + _reject_unsupported_connection_keys, cv.Schema( { cv.Optional(CONF_ACTIVE): cv.boolean, cv.Optional(CONF_CACHE_SERVICES): cv.boolean, + # Bounded by the loosest platform cap so range walkers (the + # device-builder field-range sync) see a real Range; the + # per-platform schemas tighten it (1 on rp2) with their own error. cv.Optional(CONF_CONNECTION_SLOTS): cv.All( cv.positive_int, cv.Range(min=1, max=_IDF_MAX_CONNECTIONS), @@ -295,8 +400,15 @@ async def _to_code_ble_hub(config: ConfigType) -> None: cg.add(var.set_ble_hub(hub)) # The api component sizes BluetoothConnectionsFreeResponse.allocated with - # this define whenever a proxy is present; no connections off-esp32. - cg.add_define("BLUETOOTH_PROXY_MAX_CONNECTIONS", 0) + # this define whenever a proxy is present. Zero on advertisement-only hubs. + # Sized from the instantiated connections so the define can never diverge + # from the loop below (the define sizes fixed storage in the proxy). + slots = len(config.get(CONF_CONNECTIONS, ())) + cg.add_define("BLUETOOTH_PROXY_MAX_CONNECTIONS", slots) + if not slots: + return + + await _GATT_HUB_TO_CODE[CORE.target_platform](var, config) async def to_code(config: ConfigType) -> None: diff --git a/esphome/components/rp2040_ble/__init__.py b/esphome/components/rp2040_ble/__init__.py index 4baee7e234c..e49dceb0003 100644 --- a/esphome/components/rp2040_ble/__init__.py +++ b/esphome/components/rp2040_ble/__init__.py @@ -6,6 +6,8 @@ from esphome.types import ConfigType DEPENDENCIES = ["rp2"] CODEOWNERS = ["@bdraco"] +CONF_RP2040_BLE_ID = "rp2040_ble_id" + rp2040_ble_ns = cg.esphome_ns.namespace("rp2040_ble") RP2040BLE = rp2040_ble_ns.class_("RP2040BLE", cg.Component) diff --git a/esphome/components/rp2_ble_tracker/__init__.py b/esphome/components/rp2_ble_tracker/__init__.py index cfdd78f7297..58401857687 100644 --- a/esphome/components/rp2_ble_tracker/__init__.py +++ b/esphome/components/rp2_ble_tracker/__init__.py @@ -12,6 +12,7 @@ Scan modes: import esphome.codegen as cg from esphome.components import ble_device_base, ota, rp2040_ble from esphome.components.const import CONF_SCAN_PARAMETERS, CONF_WINDOW +from esphome.components.rp2040_ble import CONF_RP2040_BLE_ID import esphome.config_validation as cv from esphome.const import ( CONF_ACTIVE, @@ -22,8 +23,6 @@ from esphome.const import ( ) from esphome.types import ConfigType -CONF_RP2040_BLE_ID = "rp2040_ble_id" - DEPENDENCIES = ["rp2"] AUTO_LOAD = ["ble_device_base", "rp2040_ble"] CODEOWNERS = ["@bdraco"] diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 63b9c879182..1685467a4b4 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -253,10 +253,13 @@ #if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_RP2) #define USE_BLUETOOTH_PROXY // Mirror the codegen values per platform: _to_code_esp32() emits the connection -// count (default 3), _to_code_ble_hub() emits 0 — so static analysis checks the -// same std::array instantiation a real build produces. +// 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 instantiation a real build produces. #ifdef USE_ESP32 #define BLUETOOTH_PROXY_MAX_CONNECTIONS 3 +#elif defined(USE_RP2) +#define BLUETOOTH_PROXY_MAX_CONNECTIONS 1 #else #define BLUETOOTH_PROXY_MAX_CONNECTIONS 0 #endif diff --git a/tests/component_tests/bluetooth_proxy/test_idf_max_connections_mirror.py b/tests/component_tests/bluetooth_proxy/test_idf_max_connections_mirror.py index 3042c91f841..58e463b32a8 100644 --- a/tests/component_tests/bluetooth_proxy/test_idf_max_connections_mirror.py +++ b/tests/component_tests/bluetooth_proxy/test_idf_max_connections_mirror.py @@ -1,10 +1,10 @@ """bluetooth_proxy mirrors esp32_ble.IDF_MAX_CONNECTIONS; pin them together. -The mirror exists so the statically walkable CONFIG_SCHEMA can express the -connection_slots range without importing the esp32 BLE stack (that import -registers esp32-only automations on every platform). The runtime check in -_esp32_config_schema() only fires while validating an esp32 config, so this -test is what actually catches drift when the upstream constant changes. +The mirror doubles as the outer CONFIG_SCHEMA's connection_slots bound, and +the esp32 schema builder lazily imports esp32_ble and asserts the two values +agree, but that assert only fires while building the esp32 schema. This test +catches drift when the upstream constant changes without any esp32 config +being validated. """ from esphome.components import esp32_ble diff --git a/tests/component_tests/bluetooth_proxy/test_outer_schema_mirror.py b/tests/component_tests/bluetooth_proxy/test_outer_schema_mirror.py index 17a05a67b9e..32e5daf4bbf 100644 --- a/tests/component_tests/bluetooth_proxy/test_outer_schema_mirror.py +++ b/tests/component_tests/bluetooth_proxy/test_outer_schema_mirror.py @@ -4,8 +4,10 @@ them without importing the esp32 BLE stack; pin the two declarations together. The outer schema carries no defaults (the per-platform schema applies them), so drift cannot surface in validation output — a key renamed or removed in _esp32_config_schema() but not here would silently vanish from the dashboard's -field extractor. This test is what catches that; validator bounds are pinned -separately only for connection_slots (test_idf_max_connections_mirror). +field extractor. This test is what catches that. The outer schema bounds +connection_slots with the loosest platform cap (_IDF_MAX_CONNECTIONS) so range +walkers see a real Range; per-platform schemas tighten it, and the cap itself +is pinned by test_idf_max_connections_mirror. """ import voluptuous as vol diff --git a/tests/component_tests/bluetooth_proxy/test_platform_gates.py b/tests/component_tests/bluetooth_proxy/test_platform_gates.py index 036530d942c..55e6fe2ca78 100644 --- a/tests/component_tests/bluetooth_proxy/test_platform_gates.py +++ b/tests/component_tests/bluetooth_proxy/test_platform_gates.py @@ -2,6 +2,9 @@ real reason, hub platforms reject GATT-only options by name, and the advertisement-only arm applies its own defaults.""" +from pathlib import Path +import re + import pytest from esphome import config_validation as cv @@ -19,9 +22,10 @@ from esphome.core import CORE from ..types import SetCoreConfigCallable +# Advertisement-only hub platforms; rp2 runs the full proxy and has its own +# tests below. HUB_PLATFORM_FRAMEWORKS = [ PlatformFramework.LN882X_ARDUINO, - PlatformFramework.RP2_ARDUINO, ] HUB_TRACKERS = { @@ -32,9 +36,11 @@ HUB_TRACKERS = { def test_hub_platform_list_covers_every_hub_platform() -> None: # A platform added to _HUB_PLATFORMS (bk72xx is planned) would otherwise - # get no gate coverage at all. - covered = {pf.value[0] for pf in HUB_PLATFORM_FRAMEWORKS} - assert covered == set(bluetooth_proxy._HUB_PLATFORMS) + # get no gate coverage at all; GATT platforms have their own tests. + advertisement_only = set(bluetooth_proxy._HUB_PLATFORMS) - set( + bluetooth_connection.HUB_MAX_CONNECTIONS + ) + assert {pf.value[0] for pf in HUB_PLATFORM_FRAMEWORKS} == advertisement_only assert set(HUB_TRACKERS) == set(bluetooth_proxy._HUB_PLATFORMS) @@ -122,6 +128,55 @@ def test_hub_platform_accepts_the_advertisement_only_shape( assert validated[CONF_ACTIVE] is False +def test_rp2_defaults_to_the_full_proxy( + set_core_config: SetCoreConfigCallable, +) -> None: + # esp32 parity: active defaults to true, with the platform's slot limit, + # and one populated connection entry for the codegen to index. + set_core_config(PlatformFramework.RP2_ARDUINO) + _register_tracker(PLATFORM_RP2) + validated = bluetooth_proxy.CONFIG_SCHEMA({}) + assert validated[CONF_ACTIVE] is True + assert validated[bluetooth_proxy.CONF_CONNECTION_SLOTS] == 1 + assert len(validated[bluetooth_proxy.CONF_CONNECTIONS]) == 1 + + +def test_rp2_accepts_explicit_passive( + set_core_config: SetCoreConfigCallable, +) -> None: + set_core_config(PlatformFramework.RP2_ARDUINO) + _register_tracker(PLATFORM_RP2) + validated = bluetooth_proxy.CONFIG_SCHEMA({CONF_ACTIVE: False}) + assert validated[CONF_ACTIVE] is False + assert bluetooth_proxy.CONF_CONNECTIONS not in validated + + +def test_rp2_rejects_slots_beyond_the_btstack_limit( + set_core_config: SetCoreConfigCallable, +) -> None: + # The prebuilt BTstack library allows exactly one GATT client connection. + set_core_config(PlatformFramework.RP2_ARDUINO) + _register_tracker(PLATFORM_RP2) + with pytest.raises(cv.Invalid, match="at most 1 connection slot"): + bluetooth_proxy.CONFIG_SCHEMA({"connection_slots": 2}) + # Values past even the loosest platform cap stop at the outer walkable + # schema, which stays bounded for range walkers (device-builder sync); + # in-range values get the platform message above. + with pytest.raises(cv.Invalid, match="at most 9"): + bluetooth_proxy.CONFIG_SCHEMA({"connection_slots": 12}) + + +def test_rp2_rejects_esp32_only_keys_by_name( + set_core_config: SetCoreConfigCallable, +) -> None: + set_core_config(PlatformFramework.RP2_ARDUINO) + _register_tracker(PLATFORM_RP2) + with pytest.raises(cv.Invalid, match="'cache_services' is esp32-only"): + bluetooth_proxy.CONFIG_SCHEMA({"cache_services": True}) + with pytest.raises(cv.Invalid, match="'connections' has no per-connection options"): + bluetooth_proxy.CONFIG_SCHEMA({"connections": [{}]}) + + def test_bluetooth_connection_auto_load_covers_its_includes() -> None: # The esp32 connection header includes esp32_ble_client; the auto load # must satisfy that closure itself (regression: it once relied on the @@ -134,3 +189,40 @@ def test_bluetooth_connection_auto_load_covers_its_includes() -> None: # dependency closures stay complete for build_codeowners and friends. _set_platform(None) assert bluetooth_connection.AUTO_LOAD() == ["ble_device_base", "esp32_ble_client"] + + +def test_every_registered_hub_platform_has_a_schema_arm() -> None: + # A platform added to HUB_MAX_CONNECTIONS without a schema builder, + # codegen arm, or _HUB_PLATFORMS entry would only fail when a config for + # it is validated (or not even then); pin all three couplings here. + registered = set(bluetooth_connection.HUB_MAX_CONNECTIONS) + assert registered <= set(bluetooth_proxy._GATT_HUB_SCHEMAS) + assert registered <= set(bluetooth_proxy._GATT_HUB_TO_CODE) + assert registered <= set(bluetooth_proxy._HUB_PLATFORMS) + # The outer walkable schema's bound must stay the loosest platform cap. + assert ( + max(bluetooth_connection.HUB_MAX_CONNECTIONS.values()) + <= bluetooth_proxy._IDF_MAX_CONNECTIONS + ) + + +def test_defines_h_mirrors_the_rp2_slot_cap() -> None: + # esphome/core/defines.h carries a literal BLUETOOTH_PROXY_MAX_CONNECTIONS + # for static analysis; pin it to the real rp2 cap. + defines = (Path(__file__).parents[3] / "esphome" / "core" / "defines.h").read_text() + cap = bluetooth_connection.RP2_MAX_CONNECTIONS + # The rp2 arm's define, tolerating blank/comment lines in between. + match = re.search( + r"#elif defined\(USE_RP2\)\s*(?:(?://[^\n]*)?\n)+#define BLUETOOTH_PROXY_MAX_CONNECTIONS (\d+)", + defines, + ) + assert match is not None, "no USE_RP2 arm defines BLUETOOTH_PROXY_MAX_CONNECTIONS" + assert int(match.group(1)) == cap, ( + f"defines.h rp2 arm carries {match.group(1)}, expected {cap}" + ) + # The static-analysis client count scales with the same cap. + match = re.search(r"#define ESPHOME_BLE_GATT_CLIENT_COUNT (\d+)", defines) + assert match is not None, "ESPHOME_BLE_GATT_CLIENT_COUNT missing from defines.h" + assert int(match.group(1)) == cap, ( + f"ESPHOME_BLE_GATT_CLIENT_COUNT is {match.group(1)}, expected {cap}" + ) diff --git a/tests/components/bluetooth_connection/common.yaml b/tests/components/bluetooth_connection/common.yaml new file mode 100644 index 00000000000..5e84f4a6783 --- /dev/null +++ b/tests/components/bluetooth_connection/common.yaml @@ -0,0 +1,8 @@ +wifi: + ssid: MySSID + password: password1 + +ota: + - platform: esphome + +api: diff --git a/tests/components/bluetooth_connection/validate.rp2040-ard.yaml b/tests/components/bluetooth_connection/validate.rp2040-ard.yaml new file mode 100644 index 00000000000..620aaa177b4 --- /dev/null +++ b/tests/components/bluetooth_connection/validate.rp2040-ard.yaml @@ -0,0 +1,11 @@ +# Distinct shape from bluetooth_proxy's own rp2 fixtures: explicit slot count +# on the platform whose backend lives in this component (validate-only, so it +# never collides with grouped builds). +packages: + common: !include common.yaml + +rp2_ble_tracker: + +bluetooth_proxy: + active: true + connection_slots: 1 diff --git a/tests/components/bluetooth_proxy/test-passive.rp2040-ard.yaml b/tests/components/bluetooth_proxy/test-passive.rp2040-ard.yaml new file mode 100644 index 00000000000..ae0f00d765b --- /dev/null +++ b/tests/components/bluetooth_proxy/test-passive.rp2040-ard.yaml @@ -0,0 +1,11 @@ +# Advertisement-only proxy on rp2 by explicit choice. Variant tests compile +# as their own builds when this component is tested individually; under CI +# batch grouping the active default build is what runs, so this fixture's +# guarantee is the individual run plus config validation. +packages: + common: !include common.yaml + +rp2_ble_tracker: + +bluetooth_proxy: + active: false diff --git a/tests/components/bluetooth_proxy/test.rp2040-ard.yaml b/tests/components/bluetooth_proxy/test.rp2040-ard.yaml index fd327bcc78f..e219c7542d4 100644 --- a/tests/components/bluetooth_proxy/test.rp2040-ard.yaml +++ b/tests/components/bluetooth_proxy/test.rp2040-ard.yaml @@ -1,5 +1,5 @@ -# Advertisement-only proxy on the rp2 BLE hub — the one non-esp32 platform the -# proxy admits today (active-scan-capable), and a target CI fully compiles. +# Full proxy on the rp2 BLE hub: active defaults to true here (esp32 parity), +# so this compiles the BTstack GATT client backend and one connection slot. # No explicit ble_hub_id: the generated binding resolves the single declared # hub, and an inline id here would collide with rp2_ble_tracker's own fixture # once CI merges both components into one grouped rp2040-ard build (grouped