From a50369fc0e5762c8cc05d4dbe4a494037d0b0ab2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 3 Aug 2026 19:53:07 -0500 Subject: [PATCH] [rp2_ble_tracker] BLE tracker for Raspberry Pi Pico W (#18002) --- CODEOWNERS | 1 + esphome/components/rp2040_ble/__init__.py | 14 ++ .../components/rp2_ble_tracker/__init__.py | 73 ++++++ .../rp2_ble_tracker/rp2_ble_tracker.cpp | 214 ++++++++++++++++++ .../rp2_ble_tracker/rp2_ble_tracker.h | 114 ++++++++++ esphome/core/defines.h | 1 + .../test_scan_parameter_validation.py | 15 ++ .../rp2_ble_tracker/common-boundary.yaml | 11 + tests/components/rp2_ble_tracker/common.yaml | 16 ++ .../rp2_ble_tracker/test.rp2040-ard.yaml | 2 + .../validate-boundary.rp2040-ard.yaml | 2 + 11 files changed, 463 insertions(+) create mode 100644 esphome/components/rp2_ble_tracker/__init__.py create mode 100644 esphome/components/rp2_ble_tracker/rp2_ble_tracker.cpp create mode 100644 esphome/components/rp2_ble_tracker/rp2_ble_tracker.h create mode 100644 tests/components/rp2_ble_tracker/common-boundary.yaml create mode 100644 tests/components/rp2_ble_tracker/common.yaml create mode 100644 tests/components/rp2_ble_tracker/test.rp2040-ard.yaml create mode 100644 tests/components/rp2_ble_tracker/validate-boundary.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index cb1be26a61..03172a5b1b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -438,6 +438,7 @@ esphome/components/rp2/* @jesserockz esphome/components/rp2040_ble/* @bdraco esphome/components/rp2040_pio_led_strip/* @Papa-DMan esphome/components/rp2040_pwm/* @jesserockz +esphome/components/rp2_ble_tracker/* @bdraco esphome/components/rpi_dpi_rgb/* @clydebarrow esphome/components/rtl87xx/* @kuba2k2 esphome/components/rtttl/* @glmnet @ximex diff --git a/esphome/components/rp2040_ble/__init__.py b/esphome/components/rp2040_ble/__init__.py index ac012b5e85..8e50c8e1ef 100644 --- a/esphome/components/rp2040_ble/__init__.py +++ b/esphome/components/rp2040_ble/__init__.py @@ -17,6 +17,20 @@ CONFIG_SCHEMA = cv.Schema( ).extend(cv.COMPONENT_SCHEMA) +def _validate_board(config: ConfigType) -> ConfigType: + from esphome.components.rp2 import board_has_wifi, get_board + + if not board_has_wifi(): + raise cv.Invalid( + f"Board '{get_board()}' does not have Bluetooth support (no CYW43 wireless " + f"chip). Use a board like 'rpipicow' or 'rpipico2w'." + ) + return config + + +FINAL_VALIDATE_SCHEMA = _validate_board + + 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/rp2_ble_tracker/__init__.py b/esphome/components/rp2_ble_tracker/__init__.py new file mode 100644 index 0000000000..2ae53cfe30 --- /dev/null +++ b/esphome/components/rp2_ble_tracker/__init__.py @@ -0,0 +1,73 @@ +"""BLE scanner for the Raspberry Pi Pico W / Pico 2 W (BLEHub on rp2040_ble). + +Scan modes: + continuous: true — scan runs forever; never stops automatically. + continuous: false — a started scan runs for `duration`, then stops. The first + start is external too; nothing starts a non-continuous + scan on boot. Until start/stop automation actions land + (follow-up PR), starting means a lambda: + `id(my_tracker).start_scan();`. +""" + +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 +import esphome.config_validation as cv +from esphome.const import CONF_CONTINUOUS, CONF_DURATION, CONF_ID, CONF_INTERVAL +from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.types import ConfigType + +CONF_RP2040_BLE_ID = "rp2040_ble_id" + +DEPENDENCIES = ["rp2"] +AUTO_LOAD = ["ble_device_base", "rp2040_ble"] +CODEOWNERS = ["@bdraco"] + +rp2_ble_tracker_ns = cg.esphome_ns.namespace("rp2_ble_tracker") +RP2BLETracker = rp2_ble_tracker_ns.class_( + "RP2BLETracker", ble_device_base.BLEHub, cg.Component +) + + +# interval defaults to 100 ms with the shared 30 ms window, a 30 % duty cycle — +# the same defaults as bk72xx_ble_tracker, leaving the radio mostly free for +# WiFi on the shared CYW43. Converted to the controller's 0.625 ms BLE units in +# to_code(). +SCAN_PARAMETERS_SCHEMA = ble_device_base.scan_parameters_schema("100ms") + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(RP2BLETracker), + cv.GenerateID(CONF_RP2040_BLE_ID): cv.use_id(rp2040_ble.RP2040BLE), + cv.Optional(CONF_SCAN_PARAMETERS, default={}): SCAN_PARAMETERS_SCHEMA, + } +).extend(cv.COMPONENT_SCHEMA) + + +# Runs at FINAL priority so every BLE sensor has registered through +# ble_device_base (and any tracker-owned listeners have been counted) before +# the StaticVector size is emitted. Same pattern as esp32_ble_tracker. +@coroutine_with_priority(CoroPriority.FINAL) +async def _emit_listener_count() -> None: + count = ble_device_base.get_listener_count() + if count > 0: + cg.add_define("ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT", count) + + +async def to_code(config: ConfigType) -> None: + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + parent = await cg.get_variable(config[CONF_RP2040_BLE_ID]) + cg.add(var.set_parent(parent)) + + # Get notified when an OTA update starts, to pause scanning (esp32_ble_tracker parity) + ota.request_ota_state_listeners() + + scan = config[CONF_SCAN_PARAMETERS] + cg.add(var.set_scan_interval(ble_device_base.to_ble_units(scan[CONF_INTERVAL]))) + cg.add(var.set_scan_window(ble_device_base.to_ble_units(scan[CONF_WINDOW]))) + cg.add(var.set_scan_duration(scan[CONF_DURATION].total_milliseconds)) + cg.add(var.set_scan_continuous(scan[CONF_CONTINUOUS])) + + CORE.add_job(_emit_listener_count) diff --git a/esphome/components/rp2_ble_tracker/rp2_ble_tracker.cpp b/esphome/components/rp2_ble_tracker/rp2_ble_tracker.cpp new file mode 100644 index 0000000000..b2c25a8d0e --- /dev/null +++ b/esphome/components/rp2_ble_tracker/rp2_ble_tracker.cpp @@ -0,0 +1,214 @@ +#ifdef USE_RP2 + +#include "rp2_ble_tracker.h" + +#include + +#include "esphome/core/application.h" +#include "esphome/core/log.h" + +namespace esphome::rp2_ble_tracker { + +static const char *const TAG = "rp2_ble_tracker"; + +// Minimum interval between scan start attempts on an active stack. The +// controller start has no failure mode once HCI is WORKING, so this fires at +// most once per enable cycle today; the floor is insurance against a future +// scan_start() failure being retried every main-loop iteration. +static constexpr uint32_t SCAN_START_RETRY_MS = 1000; + +// One BLE scan unit in milliseconds; the controller programs interval/window in these units. +static constexpr float BLE_SCAN_UNIT_MS = 0.625f; + +void RP2BLETracker::setup() { + // Receive the controller's scan reports; the controller queues them from the + // BTstack packet handler (IRQ) and delivers here on the main loop. + this->parent_->register_scan_listener(this); +#ifdef USE_OTA_STATE_LISTENER + // Pause scanning while an OTA update is in flight — the BLE scan competes with + // the OTA download on the shared CYW43 radio. Mirrors esp32_ble_tracker. + ota::get_global_ota_callback()->add_global_state_listener(this); +#endif + if (!this->scan_continuous_) { + // Nothing to do until an external start_scan(); the loop is re-enabled there. + this->disable_loop(); + } +} + +#ifdef USE_OTA_STATE_LISTENER +void RP2BLETracker::on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) { + if (state == ota::OTA_STARTED) { + this->scan_continuous_before_ota_ = this->scan_continuous_; + // A one-shot scan counts as pending when it is running or still retrying + // its start (loop enabled); captured before stop_scan() disables the loop. + this->scan_pending_before_ota_ = !this->scan_continuous_ && (this->scan_running_ || this->is_in_loop_state()); + this->stop_scan(); + } else if (state == ota::OTA_ERROR || state == ota::OTA_ABORT) { + // On success the device reboots, so restore only on a failed/aborted update; + // loop()'s retry branch restarts the scan on its next iteration. + if (this->scan_continuous_before_ota_) { + this->scan_continuous_before_ota_ = false; + this->scan_continuous_ = true; + this->enable_loop(); + } + // A one-shot scan interrupted by the OTA resumes for a fresh duration + // rather than silently staying idle — an OTA failure does not reboot, so + // nothing external would restart it. + if (this->scan_pending_before_ota_) { + this->scan_pending_before_ota_ = false; + this->enable_loop(); + } + } +} +#endif // USE_OTA_STATE_LISTENER + +void RP2BLETracker::loop() { + const uint32_t now = App.get_loop_component_start_time(); + if (this->scan_running_ && !this->parent_->is_active()) { + // The controller was disabled underneath us (e.g. a lambda calling + // rp2040_ble's disable()); the scan died with the stack. Reconcile so the + // retry branch below takes over once the user re-enables the stack. + this->scan_running_ = false; + this->fire_scan_end_(); + } + if (!this->scan_running_) { + // A scan should be running but is not: continuous mode is always in this + // state until the start succeeds, and non-continuous mode only reaches + // here between start_scan() and a successful controller start, because + // stop_scan_() disables the loop otherwise. + if (!this->parent_->is_active()) { + // Stack not up (still booting, or the user called disable()) — + // scan_start() cannot succeed, so there is nothing to attempt; scanning + // starts on the first iteration after HCI reaches WORKING. + return; + } + if (now - this->last_scan_start_attempt_ >= SCAN_START_RETRY_MS) { + this->start_scan_(); + } + return; + } + + if (this->scan_continuous_) { + // Period timer: fire on_scan_end() once per scan_duration_ window, mirroring + // esp32_ble_tracker::cleanup_scan_state_(). + if (now - this->scan_period_start_ >= this->scan_duration_) { + this->fire_scan_end_(); + this->scan_period_start_ = now; + } + return; + } + + // Non-continuous mode: run for scan_duration_ ms, then stop and fire on_scan_end. + // Restart is driven externally (e.g. api: on_client_connected:). + if (now - this->scan_period_start_ >= this->scan_duration_) { + this->stop_scan_(); + } +} + +void RP2BLETracker::dump_config() { + ESP_LOGCONFIG(TAG, + "RP2 BLE Tracker:\n" + " Scan Duration: %" PRIu32 " s\n" + " Scan Interval: %.0f ms (%" PRIu32 " BLE units)\n" + " Scan Window: %.0f ms (%" PRIu32 " BLE units)\n" + " Scan Type: PASSIVE\n" + " Continuous Scanning: %s", + this->scan_duration_ / 1000, this->scan_interval_ * BLE_SCAN_UNIT_MS, this->scan_interval_, + this->scan_window_ * BLE_SCAN_UNIT_MS, this->scan_window_, YESNO(this->scan_continuous_)); +} + +void RP2BLETracker::on_scan_report(const rp2040_ble::BLEScanReport &report) { + // Raw callback (the raw-advertisement path). + if (this->raw_advertisement_callback_.is_set()) { + const ble_device_base::RawAdvertisement adv{.mac = report.mac, + .data = report.data, + .data_len = report.data_len, + .rssi = report.rssi, + .addr_type = report.addr_type}; + this->raw_advertisement_callback_.invoke(adv); + } + +#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT + ble_device_base::ESPBTDevice device; + device.from_scan_result(report.mac, report.rssi, report.addr_type, report.data, report.data_len); + bool found = false; + for (auto *listener : this->listeners_) { + if (listener->parse_device(device)) + found = true; + } + // Mirror esp32_ble_tracker: log a newly-seen device only when nothing claimed + // it and the scan is one-shot (continuous scans would spam). + if (!found && !this->scan_continuous_) + this->discovered_log_.log_device(TAG, device); +#endif // ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT +} + +void RP2BLETracker::start_scan() { + // Mirrors esp32_ble_tracker::start_scan(): caller sets scan_continuous_ via + // set_scan_continuous() first, then calls start_scan() to begin scanning. + this->enable_loop(); + this->start_scan_(); +} + +void RP2BLETracker::stop_scan() { + this->scan_continuous_ = false; + this->stop_scan_(); + // stop_scan_() early-returns when no scan is running, so disable the loop + // here too: a scan that never came up (stack still powering on at OTA start) + // must not keep attempting scan_start() from the loop's retry branch. + this->disable_loop(); +} + +void RP2BLETracker::start_scan_() { + if (this->scan_running_) + return; + + // Stamp every attempt regardless of caller so the loop's rate limit also + // covers a failed start that came through the public start_scan(). + this->last_scan_start_attempt_ = App.get_loop_component_start_time(); + + if (!this->parent_->scan_start(static_cast(this->scan_interval_), + static_cast(this->scan_window_))) + return; + + this->scan_running_ = true; + // Log every explicit start at DEBUG — stop_scan_() logs every stop at DEBUG, and + // in non-continuous mode each period is an explicit start, so asymmetric logging + // would read as the scanner failing to come back up. + ESP_LOGD(TAG, "Scan started (passive, window=%.0fms, interval=%.0fms)", this->scan_window_ * BLE_SCAN_UNIT_MS, + this->scan_interval_ * BLE_SCAN_UNIT_MS); + // Re-anchor the scan period to every successful start — first start (so the + // period counts from the scan, not from boot) and every restart after a stop (so + // resuming after longer than scan_duration, e.g. a failed OTA restoring continuous + // mode 10 minutes later, does not fire on_scan_end before an advertisement can + // arrive). Same clock as loop()'s `now`: a fresh millis() here would be ahead of + // the cached loop time and make the same-iteration period check underflow. + this->scan_period_start_ = App.get_loop_component_start_time(); +} + +void RP2BLETracker::stop_scan_() { + if (!this->scan_running_) + return; + this->parent_->scan_stop(); + this->scan_running_ = false; + ESP_LOGD(TAG, "Scan stopped"); + this->fire_scan_end_(); + // Reset the period clock so on_scan_end does not double-fire; same clock as loop(). + this->scan_period_start_ = App.get_loop_component_start_time(); + if (!this->scan_continuous_) { + // Nothing left to time; start_scan() re-enables the loop. + this->disable_loop(); + } +} + +void RP2BLETracker::fire_scan_end_() { +#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT + for (auto *listener : this->listeners_) + listener->on_scan_end(); + this->discovered_log_.clear(); // reset per-scan "Found device" dedup (esp32_ble_tracker parity) +#endif +} + +} // namespace esphome::rp2_ble_tracker + +#endif // USE_RP2 diff --git a/esphome/components/rp2_ble_tracker/rp2_ble_tracker.h b/esphome/components/rp2_ble_tracker/rp2_ble_tracker.h new file mode 100644 index 0000000000..808e84a70f --- /dev/null +++ b/esphome/components/rp2_ble_tracker/rp2_ble_tracker.h @@ -0,0 +1,114 @@ +#pragma once + +#ifdef USE_RP2 + +#include "esphome/components/ble_device_base/ble_device.h" +#include "esphome/components/ble_device_base/ble_hub.h" +#include "esphome/components/rp2040_ble/rp2040_ble.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +#include + +#ifdef USE_OTA_STATE_LISTENER +#include "esphome/components/ota/ota_backend.h" +#endif + +namespace esphome::rp2_ble_tracker { + +class RP2BLETracker : public Component, + public ble_device_base::BLEHub, + public rp2040_ble::BLEScanListener, + public Parented +#ifdef USE_OTA_STATE_LISTENER + , + public ota::OTAGlobalStateListener +#endif +{ + public: + // ---- ESPHome Component ---- + void setup() override; + void loop() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + +#ifdef USE_OTA_STATE_LISTENER + // Pause scanning while an OTA update runs (the BLE scan competes with the OTA + // download on the shared CYW43 radio); mirrors esp32_ble_tracker. + void on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) override; +#endif + + // ---- YAML configuration setters ---- + void set_scan_interval(uint32_t scan_interval) { this->scan_interval_ = scan_interval; } + void set_scan_window(uint32_t scan_window) { this->scan_window_ = scan_window; } + void set_scan_duration(uint32_t scan_duration) { this->scan_duration_ = scan_duration; } + void set_scan_continuous(bool scan_continuous) { this->scan_continuous_ = scan_continuous; } + + // ---- Public scan control ---- + // Mirrors esp32_ble_tracker: set_scan_continuous() + start_scan() / stop_scan(). + void start_scan(); + void stop_scan(); + + // ---- ble_device_base::BLEHub contract ---- + void register_listener(ble_device_base::ESPBTDeviceListener *listener) override { +#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT + this->listeners_.push_back(listener); +#endif + } + void set_raw_advertisement_callback(ble_device_base::RawAdvertisementCallback callback) override { + this->raw_advertisement_callback_ = callback; + } + ble_device_base::HubCapabilities get_capabilities() const override { + // BTstack on the CYW43 supports active scanning and GATT, but this tracker + // drives the controller passively (scan_type 0) and exposes no GATT path + // yet — capabilities describe what this component delivers, so all three + // stay false until those paths are implemented. Consumers relying on + // scan-response fields (device names) get them only where the receiver + // merges per address (Home Assistant does). + return {.active_scan = false, .merges_scan_response = false, .gatt = false}; + } + // The controller stores the address in printable (MSB-first) order, which is + // exactly what the contract wants. + void get_adapter_mac(uint8_t out[6]) override { this->parent_->get_mac_msb_first(out); } + bool scan_running() override { return this->scan_running_; } + bool scan_active() override { return false; } // passive-only (initial implementation) + + // ---- rp2040_ble::BLEScanListener ---- + // Delivered by the controller's loop() on the ESPHome main loop — the + // IRQ → main-loop handoff already happened in the controller's queue. + void on_scan_report(const rp2040_ble::BLEScanReport &report) override; + + protected: + void start_scan_(); + void stop_scan_(); + void fire_scan_end_(); + + // Defaults: 30 % duty cycle (interval 100 ms / window 30 ms), in 0.625 ms + // BLE units — same defaults as bk72xx_ble_tracker. + uint32_t scan_interval_{160}; // 160 × 0.625 ms = 100 ms + uint32_t scan_window_{48}; // 48 × 0.625 ms = 30 ms (30/100 = 30 %) + uint32_t scan_duration_{300000}; + uint32_t last_scan_start_attempt_{0}; // loop time of last start_scan_() attempt; rate-limits retries + uint32_t scan_period_start_{0}; // loop time at start of current scan period; rate-limits on_scan_end() + bool scan_running_{false}; + bool scan_continuous_{true}; +#ifdef USE_OTA_STATE_LISTENER + bool scan_continuous_before_ota_{false}; // continuous mode saved at OTA start, restored on OTA failure + bool scan_pending_before_ota_{false}; // one-shot scan in flight at OTA start, resumed on OTA failure +#endif + + ble_device_base::RawAdvertisementCallback raw_advertisement_callback_{}; +#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT + // Parsed-advertisement consumers registered through ble_device_base. + // Codegen-sized: no heap allocation, no std::vector template instantiations. + StaticVector listeners_; + // Per-period "Found device" DEBUG log with MAC dedup — shared implementation + // in ble_device_base, identical output on every tracker backend. Guarded like + // its only writer so a no-listener build does not carry an unused vector. + ble_device_base::DiscoveredDeviceLog discovered_log_{}; +#endif +}; + +} // namespace esphome::rp2_ble_tracker + +#endif // USE_RP2 diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 65726bdb1d..1d4cbcfc37 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -433,6 +433,7 @@ #define USE_LOGGER_USB_CDC #define USE_SOCKET_IMPL_LWIP_TCP #define USE_RP2040_BLE +#define ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT 1 #define USE_RP2040_VARIANT_RP2040 #define USE_SPI #ifndef USE_ETHERNET diff --git a/tests/component_tests/ble_device_base/test_scan_parameter_validation.py b/tests/component_tests/ble_device_base/test_scan_parameter_validation.py index 363c129f4b..e3226eaa41 100644 --- a/tests/component_tests/ble_device_base/test_scan_parameter_validation.py +++ b/tests/component_tests/ble_device_base/test_scan_parameter_validation.py @@ -10,6 +10,7 @@ from esphome.components.bk72xx_ble_tracker import ( ) from esphome.components.ble_device_base import to_ble_units from esphome.components.esp32_ble_tracker import SCAN_PARAMETERS_SCHEMA as ESP32_SCHEMA +from esphome.components.rp2_ble_tracker import SCAN_PARAMETERS_SCHEMA as RP2_SCHEMA def _validate(**kwargs: str) -> dict: @@ -59,6 +60,15 @@ def test_esp32_defaults_are_valid() -> None: assert config["active"] is True +def test_rp2_defaults_are_valid() -> None: + """rp2 pins 100 ms interval / 30 ms window — a 30 % duty cycle leaving the + shared CYW43 radio mostly free for WiFi.""" + config = RP2_SCHEMA({}) + assert to_ble_units(config["interval"]) == 160 + assert to_ble_units(config["window"]) == 48 + assert "active" not in config + + def test_esp32_active_can_disable() -> None: config = ESP32_SCHEMA({"active": False}) assert config["active"] is False @@ -102,6 +112,11 @@ def test_window_equal_to_interval_accepted() -> None: assert to_ble_units(config["interval"]) == to_ble_units(config["window"]) +def test_duration_equal_to_three_intervals_accepted() -> None: + """The three-interval floor is inclusive, mirroring the ceilings above.""" + _validate(duration="3s", interval="1s", window="500ms") + + # --- rejected configurations --- diff --git a/tests/components/rp2_ble_tracker/common-boundary.yaml b/tests/components/rp2_ble_tracker/common-boundary.yaml new file mode 100644 index 0000000000..c8b558e226 --- /dev/null +++ b/tests/components/rp2_ble_tracker/common-boundary.yaml @@ -0,0 +1,11 @@ +rp2_ble_tracker: + id: ble_tracker + scan_parameters: + # Boundary coverage: the documented 2.5 ms floor on window (expressible only + # via the microsecond-accurate validation), a non-round interval exercising the + # 0.625 ms unit conversion without collapsing onto the window's unit count, + # and the non-continuous config path. + interval: 5000us + window: 2500us + duration: 5min + continuous: false diff --git a/tests/components/rp2_ble_tracker/common.yaml b/tests/components/rp2_ble_tracker/common.yaml new file mode 100644 index 0000000000..12482a6124 --- /dev/null +++ b/tests/components/rp2_ble_tracker/common.yaml @@ -0,0 +1,16 @@ +rp2_ble_tracker: + id: ble_tracker + scan_parameters: + interval: 100ms + window: 30ms + duration: 5min + continuous: true + +# Pulls in USE_OTA_STATE_LISTENER so the OTA scan-pause path compiles in CI +# (same coverage arrangement as the esp32_ble_tracker tests). +wifi: + ssid: MySSID + password: password1 + +ota: + - platform: esphome diff --git a/tests/components/rp2_ble_tracker/test.rp2040-ard.yaml b/tests/components/rp2_ble_tracker/test.rp2040-ard.yaml new file mode 100644 index 0000000000..8b94f3cade --- /dev/null +++ b/tests/components/rp2_ble_tracker/test.rp2040-ard.yaml @@ -0,0 +1,2 @@ +packages: + rp2_ble_tracker: !include common.yaml diff --git a/tests/components/rp2_ble_tracker/validate-boundary.rp2040-ard.yaml b/tests/components/rp2_ble_tracker/validate-boundary.rp2040-ard.yaml new file mode 100644 index 0000000000..b62a401320 --- /dev/null +++ b/tests/components/rp2_ble_tracker/validate-boundary.rp2040-ard.yaml @@ -0,0 +1,2 @@ +packages: + rp2_ble_tracker: !include common-boundary.yaml