From 5c2c11f6cb0f059080d1f20d6c751e5bb7d529ce Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 10 Aug 2026 17:00:43 -0500 Subject: [PATCH] Build the shared rp2350 target on rpipico2w and test the slot accounting --- esphome/components/rp2040_ble/__init__.py | 24 +++++++---- .../rp2040_ble/test_connection_slots.py | 41 +++++++++++++++++++ .../bluetooth_proxy/test.rp2350-ard.yaml | 9 ++++ .../build_components_base.rp2350-ard.yaml | 4 +- 4 files changed, 68 insertions(+), 10 deletions(-) create mode 100644 tests/component_tests/rp2040_ble/test_connection_slots.py create mode 100644 tests/components/bluetooth_proxy/test.rp2350-ard.yaml diff --git a/esphome/components/rp2040_ble/__init__.py b/esphome/components/rp2040_ble/__init__.py index 9ab3da554c..332ea73a61 100644 --- a/esphome/components/rp2040_ble/__init__.py +++ b/esphome/components/rp2040_ble/__init__.py @@ -57,17 +57,23 @@ def consume_connection_slots( return _consume_connection_slots +def validate_connection_slots() -> None: + """Fail when consumers claimed more slots than the platform cap.""" + # Skip in testing mode to allow component grouping (esp32_ble parity). + if CORE.testing_mode: + return + used = CORE.data.get(KEY_RP2040_BLE, {}).get(KEY_USED_CONNECTION_SLOTS, []) + if len(used) > MAX_CONNECTIONS: + raise cv.Invalid( + f"BLE components require {len(used)} connection slots but the " + f"rp2 maximum is {MAX_CONNECTIONS}. " + f"Components: {', '.join(used)}" + ) + + def _final_validate(config: ConfigType) -> ConfigType: _validate_board(config) - # Skip in testing mode to allow component grouping (esp32_ble parity). - if not CORE.testing_mode: - used = CORE.data.get(KEY_RP2040_BLE, {}).get(KEY_USED_CONNECTION_SLOTS, []) - if len(used) > MAX_CONNECTIONS: - raise cv.Invalid( - f"BLE components require {len(used)} connection slots but the " - f"rp2 maximum is {MAX_CONNECTIONS}. " - f"Components: {', '.join(used)}" - ) + validate_connection_slots() return config diff --git a/tests/component_tests/rp2040_ble/test_connection_slots.py b/tests/component_tests/rp2040_ble/test_connection_slots.py new file mode 100644 index 0000000000..f33180e2d0 --- /dev/null +++ b/tests/component_tests/rp2040_ble/test_connection_slots.py @@ -0,0 +1,41 @@ +"""Connection-slot accounting: consumers claim against MAX_CONNECTIONS and +final validation rejects over-subscription with the consumer list.""" + +from __future__ import annotations + +from collections.abc import Callable +from pathlib import Path + +import pytest + +from esphome import config_validation as cv +from esphome.components import rp2040_ble +from esphome.core import CORE + + +def test_proxy_claims_its_slots_through_the_shared_accounting( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + # A default (3-slot) proxy build records one claim per slot, attributed + # to the consumer, and passes final validation. + generate_main(component_config_path("rp2_proxy_default.yaml")) + used = CORE.data[rp2040_ble.KEY_RP2040_BLE][rp2040_ble.KEY_USED_CONNECTION_SLOTS] + assert used == ["bluetooth_proxy"] * 3 + + +def test_oversubscription_is_rejected_with_the_consumer_list() -> None: + # No YAML shape reaches this today (the proxy schema caps at the same + # limit); the guard exists for a second consumer such as ble_client. + rp2040_ble.consume_connection_slots(3, "bluetooth_proxy")({}) + rp2040_ble.consume_connection_slots(1, "ble_client")({}) + with pytest.raises( + cv.Invalid, + match=r"4 connection slots.*maximum is 3.*bluetooth_proxy.*ble_client", + ): + rp2040_ble.validate_connection_slots() + + +def test_at_cap_passes() -> None: + rp2040_ble.consume_connection_slots(3, "bluetooth_proxy")({}) + rp2040_ble.validate_connection_slots() diff --git a/tests/components/bluetooth_proxy/test.rp2350-ard.yaml b/tests/components/bluetooth_proxy/test.rp2350-ard.yaml new file mode 100644 index 0000000000..1abc62cedb --- /dev/null +++ b/tests/components/bluetooth_proxy/test.rp2350-ard.yaml @@ -0,0 +1,9 @@ +# Pico 2 W build of the full proxy: links the rp2350 framework archive, so +# the pool --wrap overrides and their per-architecture layout asserts are +# exercised for this chip too (see test.rp2040-ard.yaml for the slot shape). +packages: + common: !include common.yaml + +rp2_ble_tracker: + +bluetooth_proxy: diff --git a/tests/test_build_components/build_components_base.rp2350-ard.yaml b/tests/test_build_components/build_components_base.rp2350-ard.yaml index 5df1670862..f76c5fc3f9 100644 --- a/tests/test_build_components/build_components_base.rp2350-ard.yaml +++ b/tests/test_build_components/build_components_base.rp2350-ard.yaml @@ -2,8 +2,10 @@ esphome: name: componenttestrp2040pico2ard friendly_name: $component_name +# rpipico2w: superset of rpipico2 with the CYW43 radio, so wireless +# components (wifi, BLE) can share this target too. rp2: - board: rpipico2 + board: rpipico2w logger: level: VERY_VERBOSE