diff --git a/esphome/components/rp2040_ble/__init__.py b/esphome/components/rp2040_ble/__init__.py index 8e50c8e1ef..4baee7e234 100644 --- a/esphome/components/rp2040_ble/__init__.py +++ b/esphome/components/rp2040_ble/__init__.py @@ -31,6 +31,11 @@ def _validate_board(config: ConfigType) -> ConfigType: FINAL_VALIDATE_SCHEMA = _validate_board +# Once per registered scan listener; sizes the controller's StaticVector +# listener storage. +request_scan_listener_slot = cg.slot_counter("RP2040_BLE_SCAN_LISTENER_COUNT") + + async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/rp2040_ble/rp2040_ble.cpp b/esphome/components/rp2040_ble/rp2040_ble.cpp index 8280c19d56..405710f3e8 100644 --- a/esphome/components/rp2040_ble/rp2040_ble.cpp +++ b/esphome/components/rp2040_ble/rp2040_ble.cpp @@ -117,8 +117,10 @@ void RP2040BLE::loop() { if (report == nullptr) return; do { +#ifdef RP2040_BLE_SCAN_LISTENER_COUNT for (auto *listener : this->scan_listeners_) listener->on_scan_report(*report); +#endif this->report_pool_.release(report); } while ((report = this->report_queue_.pop()) != nullptr); diff --git a/esphome/components/rp2040_ble/rp2040_ble.h b/esphome/components/rp2040_ble/rp2040_ble.h index af7feddd26..cc015c0503 100644 --- a/esphome/components/rp2040_ble/rp2040_ble.h +++ b/esphome/components/rp2040_ble/rp2040_ble.h @@ -6,12 +6,12 @@ #include "esphome/core/component.h" #include "esphome/core/event_pool.h" +#include "esphome/core/helpers.h" #include "esphome/core/lock_free_queue.h" #include #include -#include namespace esphome::rp2040_ble { @@ -79,8 +79,12 @@ class RP2040BLE final : public Component { /// power-up). void get_mac_msb_first(uint8_t out[6]) const; +#ifdef RP2040_BLE_SCAN_LISTENER_COUNT /// Register a consumer for scan reports (delivered on the main loop via loop()). + /// Storage is codegen-sized: the consumer's codegen requests a slot via + /// request_scan_listener_slot(), which emits RP2040_BLE_SCAN_LISTENER_COUNT. void register_scan_listener(BLEScanListener *listener) { this->scan_listeners_.push_back(listener); } +#endif /// Start a controller scan; active sends scan requests and receives scan /// responses as separate reports. Interval/window are in BLE units @@ -102,7 +106,11 @@ class RP2040BLE final : public Component { void enqueue_scan_report_(const uint8_t *mac_lsb_first, int8_t rssi, uint8_t addr_type, uint8_t adv_event_type, const uint8_t *data, uint16_t data_len); - std::vector scan_listeners_; +#ifdef RP2040_BLE_SCAN_LISTENER_COUNT + // Codegen-sized: no heap allocation, no std::vector template instantiation — + // the same StaticVector pattern as the tracker's ble_device_base listeners. + StaticVector scan_listeners_; +#endif // Report ring: the BTstack packet handler (async-context IRQ) allocates a // report from the pool, fills it and pushes the pointer; loop() pops, // dispatches and releases. Lock-free SPSC — the esp32_ble/bk72xx_ble pattern. diff --git a/esphome/components/rp2_ble_tracker/__init__.py b/esphome/components/rp2_ble_tracker/__init__.py index cce6adabe6..651307fc5b 100644 --- a/esphome/components/rp2_ble_tracker/__init__.py +++ b/esphome/components/rp2_ble_tracker/__init__.py @@ -59,6 +59,9 @@ async def to_code(config: ConfigType) -> None: parent = await cg.get_variable(config[CONF_RP2040_BLE_ID]) cg.add(var.set_parent(parent)) + # The tracker registers itself as a controller scan listener in setup(); + # request the codegen-sized StaticVector slot for it. + rp2040_ble.request_scan_listener_slot() # Get notified when an OTA update starts, to pause scanning (esp32_ble_tracker parity) ota.request_ota_state_listeners() diff --git a/esphome/core/defines.h b/esphome/core/defines.h index d946b106d1..06a0ea5c46 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -436,6 +436,7 @@ #define USE_LOGGER_USB_CDC #define USE_SOCKET_IMPL_LWIP_TCP #define USE_RP2040_BLE +#define RP2040_BLE_SCAN_LISTENER_COUNT 1 #define ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT 1 #define USE_RP2040_VARIANT_RP2040 #define USE_SPI diff --git a/tests/component_tests/ble_device_base/config/rp2_controller_only.yaml b/tests/component_tests/ble_device_base/config/rp2_controller_only.yaml new file mode 100644 index 0000000000..e64b328051 --- /dev/null +++ b/tests/component_tests/ble_device_base/config/rp2_controller_only.yaml @@ -0,0 +1,7 @@ +esphome: + name: slotcount-rp2-controller + +rp2: + board: rpipicow + +rp2040_ble: diff --git a/tests/component_tests/ble_device_base/config/rp2_tracker.yaml b/tests/component_tests/ble_device_base/config/rp2_tracker.yaml new file mode 100644 index 0000000000..31686dd236 --- /dev/null +++ b/tests/component_tests/ble_device_base/config/rp2_tracker.yaml @@ -0,0 +1,7 @@ +esphome: + name: slotcount-rp2-tracker + +rp2: + board: rpipicow + +rp2_ble_tracker: diff --git a/tests/component_tests/ble_device_base/test_slot_counter.py b/tests/component_tests/ble_device_base/test_slot_counter.py index 852d749e51..b36a5f1a8a 100644 --- a/tests/component_tests/ble_device_base/test_slot_counter.py +++ b/tests/component_tests/ble_device_base/test_slot_counter.py @@ -11,12 +11,23 @@ from __future__ import annotations from collections.abc import Callable from pathlib import Path +import pytest + from esphome.core import CORE from ..helpers import get_define_value +@pytest.mark.parametrize( + ("config", "define"), + [ + ("bk72xx_tracker.yaml", "BK72XX_BLE_SCAN_LISTENER_COUNT"), + ("rp2_tracker.yaml", "RP2040_BLE_SCAN_LISTENER_COUNT"), + ], +) def test_tracker_requests_one_slot( + config: str, + define: str, generate_main: Callable[[str | Path], str], component_config_path: Callable[[str], Path], ) -> None: @@ -25,18 +36,27 @@ def test_tracker_requests_one_slot( The neutral listener count must stay absent from the same build: no BLE consumer registered through register_ble_device(). """ - generate_main(component_config_path("bk72xx_tracker.yaml")) - assert get_define_value("BK72XX_BLE_SCAN_LISTENER_COUNT") == "1" + generate_main(component_config_path(config)) + assert get_define_value(define) == "1" assert get_define_value("ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT") is None +@pytest.mark.parametrize( + ("config", "define"), + [ + ("bk72xx_controller_only.yaml", "BK72XX_BLE_SCAN_LISTENER_COUNT"), + ("rp2_controller_only.yaml", "RP2040_BLE_SCAN_LISTENER_COUNT"), + ], +) def test_controller_only_emits_no_count( + config: str, + define: str, generate_main: Callable[[str | Path], str], component_config_path: Callable[[str], Path], ) -> None: """No consumer, no define — the guarded listener storage compiles out.""" - generate_main(component_config_path("bk72xx_controller_only.yaml")) - assert get_define_value("BK72XX_BLE_SCAN_LISTENER_COUNT") is None + generate_main(component_config_path(config)) + assert get_define_value(define) is None def test_neutral_listener_count_emitted_when_requested() -> None: