mirror of
https://github.com/esphome/esphome.git
synced 2026-08-20 21:30:57 +08:00
[rp2_ble_tracker] BLE tracker for Raspberry Pi Pico W (#18002)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1,214 @@
|
||||
#ifdef USE_RP2
|
||||
|
||||
#include "rp2_ble_tracker.h"
|
||||
|
||||
#include <cinttypes>
|
||||
|
||||
#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<uint16_t>(this->scan_interval_),
|
||||
static_cast<uint16_t>(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
|
||||
@@ -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 <cstdint>
|
||||
|
||||
#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<rp2040_ble::RP2040BLE>
|
||||
#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<ble_device_base::ESPBTDeviceListener *, ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT> 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
|
||||
@@ -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
|
||||
|
||||
@@ -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 ---
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
packages:
|
||||
rp2_ble_tracker: !include common.yaml
|
||||
@@ -0,0 +1,2 @@
|
||||
packages:
|
||||
rp2_ble_tracker: !include common-boundary.yaml
|
||||
Reference in New Issue
Block a user