Compare commits

..
16 changed files with 195 additions and 320 deletions
+16 -27
View File
@@ -206,36 +206,32 @@ def validate_scan_parameters(config: ConfigType) -> ConfigType:
interval = config[CONF_INTERVAL]
window = config[CONF_WINDOW]
# Labels are reused in every error below; the optional one names its key.
windows = [("Scan window", window)]
if (connection_window := config.get(CONF_CONNECTION_SCAN_WINDOW)) is not None:
windows.append((CONF_CONNECTION_SCAN_WINDOW, connection_window))
for name, value in windows:
if value > interval:
raise cv.Invalid(
f"{name} ({value}) needs to be smaller than scan interval ({interval})"
)
if window > interval:
raise cv.Invalid(
f"Scan window ({window}) needs to be smaller than scan interval ({interval})"
)
# BLE scan interval/window are programmed in 0.625 ms units as a 16-bit value; the
# controller only accepts 2.5 ms .. 10240 ms (0x0004 .. 0x4000). Reject out-of-range
# values here instead of letting the unit conversion silently overflow.
for name, value in (("Scan interval", interval), *windows):
for name, value in (("interval", interval), ("window", window)):
if value.total_microseconds < 2500 or value.total_microseconds > 10_240_000:
raise cv.Invalid(f"{name} ({value}) must be between 2.5 ms and 10240 ms")
raise cv.Invalid(
f"Scan {name} ({value}) must be between 2.5 ms and 10240 ms"
)
# Validate what actually reaches the controller: both values are truncated to
# whole 0.625 ms units, so a window/interval pair that differs by less than one
# unit collapses to the same value — silently programming a 100 % duty cycle
# (radio permanently on) from a config that asked for less.
interval_units = to_ble_units(interval)
for name, value in windows:
if to_ble_units(value) == interval_units and value < interval:
raise cv.Invalid(
f"{name} ({value}) and interval ({interval}) both truncate to "
f"{interval_units} x 0.625 ms, which the controller scans at a 100 % duty "
f"cycle. Separate them by at least 0.625 ms."
)
window_units = to_ble_units(window)
if window_units == interval_units and window < interval:
raise cv.Invalid(
f"Scan window ({window}) and interval ({interval}) both truncate to "
f"{interval_units} x 0.625 ms, which the controller scans at a 100 % duty "
f"cycle. Separate them by at least 0.625 ms."
)
if interval.total_microseconds * 3 > duration.total_microseconds:
raise cv.Invalid(
@@ -251,14 +247,11 @@ def validate_scan_parameters(config: ConfigType) -> ConfigType:
# their own; also the fallback for esp32's conditional default.
DEFAULT_SCAN_WINDOW = "30ms"
CONF_CONNECTION_SCAN_WINDOW = "connection_scan_window"
def scan_parameters_schema(
interval_default: str,
*,
window_default: str | Callable[[], TimePeriod] = DEFAULT_SCAN_WINDOW,
connection_window: bool = False,
) -> cv.All:
"""Build the scan_parameters value schema shared by all BLE trackers.
@@ -270,9 +263,7 @@ def scan_parameters_schema(
can adjust it once sibling keys are resolved). The `active` option
(default on) is unconditional: active scanning is part of the tracker
contract — every current proxy client assumes it, so a passive-only
tracker must not share this schema. connection_window opts in to the
`connection_scan_window` option for trackers that can fall back to a
smaller window while a GATT connection is active.
tracker must not share this schema.
"""
schema = {
cv.Optional(CONF_DURATION, default="5min"): cv.positive_time_period_seconds,
@@ -281,8 +272,6 @@ def scan_parameters_schema(
cv.Optional(CONF_CONTINUOUS, default=True): cv.boolean,
cv.Optional(CONF_ACTIVE, default=True): cv.boolean,
}
if connection_window:
schema[cv.Optional(CONF_CONNECTION_SCAN_WINDOW)] = cv.positive_time_period
return cv.All(cv.Schema(schema), validate_scan_parameters)
@@ -7,7 +7,6 @@ import logging
from esphome import automation
import esphome.codegen as cg
from esphome.components import ble_device_base, esp32_ble, ota
from esphome.components.ble_device_base import CONF_CONNECTION_SCAN_WINDOW
from esphome.components.const import CONF_ON_SCAN_END, CONF_SCAN_PARAMETERS, CONF_WINDOW
from esphome.components.esp32 import (
add_idf_sdkconfig_option,
@@ -74,9 +73,8 @@ def _get_required_features() -> set[BLEFeatures]:
# Slot counters sizing the tracker's StaticVector storage; one request per
# registered listener or client.
CLIENT_COUNT_DEFINE = "ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT"
_request_listener_slot = cg.slot_counter("ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT")
_request_client_slot = cg.slot_counter(CLIENT_COUNT_DEFINE)
_request_client_slot = cg.slot_counter("ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT")
def register_ble_features(features: set[BLEFeatures]) -> None:
@@ -149,7 +147,6 @@ class TrackerData:
"""Per-run validation state, namespaced under DOMAIN in CORE.data."""
scan_window_defaulted: bool = False
connection_window_injected: bool = False
def _get_data() -> TrackerData:
@@ -178,34 +175,17 @@ def _raise_defaulted_scan_window(config: ConfigType) -> ConfigType:
honors the window strictly (>= 5.5.5); without the arbiter a full-duty
scan would starve wifi outright, and a user-set window is never touched.
Raising to the interval cannot invalidate the already-validated
parameters, so no re-validation is needed. The connection window is
checked against the window here, after the raise.
parameters, so no re-validation is needed.
"""
params = config[CONF_SCAN_PARAMETERS]
if (
_get_data().scan_window_defaulted
and config.get(CONF_SOFTWARE_COEXISTENCE)
and idf_version() >= IDF_SCAN_WINDOW_FIX_VERSION
):
params = config[CONF_SCAN_PARAMETERS]
# Copy so the config dump shows a plain value instead of a YAML
# anchor/alias pair pointing at the interval.
params[CONF_WINDOW] = copy.copy(params[CONF_INTERVAL])
# Arm the connection-time fallback unless the user set one. Injected
# after validation; safe because it equals the validated window default.
if CONF_CONNECTION_SCAN_WINDOW not in params:
params[CONF_CONNECTION_SCAN_WINDOW] = cv.positive_time_period(
ble_device_base.DEFAULT_SCAN_WINDOW
)
_get_data().connection_window_injected = True
if (
connection_window := params.get(CONF_CONNECTION_SCAN_WINDOW)
) is not None and connection_window > params[CONF_WINDOW]:
# A larger value would widen the scan during connections.
raise cv.Invalid(
f"{CONF_CONNECTION_SCAN_WINDOW} ({connection_window}) needs to be "
f"smaller than the scan window ({params[CONF_WINDOW]})",
path=[CONF_SCAN_PARAMETERS, CONF_CONNECTION_SCAN_WINDOW],
)
return config
@@ -214,7 +194,7 @@ def _raise_defaulted_scan_window(config: ConfigType) -> ConfigType:
# window/interval pairs that collapse to the same 0.625 ms unit count.
# The window default is conditional (see _scan_window_default above).
SCAN_PARAMETERS_SCHEMA = ble_device_base.scan_parameters_schema(
"320ms", window_default=_scan_window_default, connection_window=True
"320ms", window_default=_scan_window_default
)
# Codegen helpers are owned by ble_device_base; kept under the historical names
@@ -308,25 +288,6 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_scan_duration(params[CONF_DURATION]))
cg.add(var.set_scan_interval(ble_device_base.to_ble_units(params[CONF_INTERVAL])))
cg.add(var.set_scan_window(ble_device_base.to_ble_units(params[CONF_WINDOW])))
if (connection_window := params.get(CONF_CONNECTION_SCAN_WINDOW)) is not None:
# Emitted at FINAL so a scan-only build, where the guarded C++ path
# compiles out, skips the call entirely.
window_units = ble_device_base.to_ble_units(connection_window)
@coroutine_with_priority(CoroPriority.FINAL)
async def _emit_connection_scan_window() -> None:
if cg.get_slot_count(CLIENT_COUNT_DEFINE):
cg.add(var.set_connection_scan_window(window_units))
elif not _get_data().connection_window_injected:
# Warn only for a user-set value; the injected default drops silently.
_LOGGER.warning(
"'%s' has no effect because this build has no BLE client "
"components (for example bluetooth_proxy with active "
"connections, or ble_client)",
CONF_CONNECTION_SCAN_WINDOW,
)
CORE.add_job(_emit_connection_scan_window)
cg.add(var.set_scan_active(params[CONF_ACTIVE]))
cg.add(var.set_scan_continuous(params[CONF_CONTINUOUS]))
@@ -122,9 +122,6 @@ void ESP32BLETracker::loop() {
// - start_scan_(): scanner_state_ becomes IDLE via set_scanner_state_() in cleanup_scan_state_()
// - try_promote_discovered_clients_(): client enters DISCOVERED via set_state(), or
// connecting client finishes (state change), or scanner reaches RUNNING/IDLE
// - connection-window restart: scan_params_ is only written in start_scan_()
// (which changes scanner state via set_scanner_state_()), and
// counts.active/disconnecting only change on client state changes
//
// All conditions that affect the logic below are tied to state changes that increment
// state_version_, so the fast path is safe.
@@ -147,19 +144,6 @@ void ESP32BLETracker::loop() {
(this->scan_set_param_failed_ && this->scanner_state_ == ScannerState::RUNNING)) {
this->handle_scanner_failure_();
}
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
// The programmed window no longer matches the connection state (typically
// the last connection dropped): restart so the right window applies now
// instead of at the end of the scan period. Continuous only (a user-started
// scan would not restart); !disconnecting matches the restart gate below.
if (this->scanner_state_ == ScannerState::RUNNING && this->scan_continuous_ && !counts.disconnecting &&
this->scan_params_.scan_window != this->desired_scan_window_(counts.active)) {
// Same logical scan period continues: no on_scan_end sweeps for this
// restart. Only armed when the stop was issued.
this->skip_next_scan_end_ = this->stop_scan_();
}
#endif
/*
Avoid starting the scanner if:
@@ -211,23 +195,19 @@ void ESP32BLETracker::stop_scan() {
// reason at D themselves, and the user-facing stop action is deliberate.
ESP_LOGV(TAG, "Stopping scan.");
this->scan_continuous_ = false;
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
// The window-change restart is abandoned with continuous scanning.
this->skip_next_scan_end_ = false;
#endif
this->stop_scan_();
}
void ESP32BLETracker::ble_before_disabled_event_handler() { this->stop_scan_(); }
bool ESP32BLETracker::stop_scan_() {
void ESP32BLETracker::stop_scan_() {
if (this->scanner_state_ != ScannerState::RUNNING && this->scanner_state_ != ScannerState::FAILED) {
// IDLE means there is nothing to stop; STOPPING means a stop is already in
// flight and will finish on its own. Neither is an error.
if (this->scanner_state_ != ScannerState::IDLE && this->scanner_state_ != ScannerState::STOPPING) {
ESP_LOGE(TAG, "Cannot stop scan: %s", this->scanner_state_to_string_(this->scanner_state_));
}
return false;
return;
}
// Reset timeout state machine when stopping scan
this->scan_timeout_state_ = ScanTimeoutState::INACTIVE;
@@ -235,9 +215,8 @@ bool ESP32BLETracker::stop_scan_() {
esp_err_t err = esp_ble_gap_stop_scanning();
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gap_stop_scanning failed: %d", err);
return false;
return;
}
return true;
}
void ESP32BLETracker::start_scan_(bool first) {
@@ -251,11 +230,16 @@ void ESP32BLETracker::start_scan_(bool first) {
}
this->set_scanner_state_(ScannerState::STARTING);
ESP_LOGV(TAG, "Starting scan, set scanner state to STARTING.");
if (!first)
this->notify_scan_end_();
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
this->skip_next_scan_end_ = false;
if (!first) {
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
for (auto *listener : this->listeners_)
listener->on_scan_end();
#endif
#ifdef ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT
for (auto *listener : this->neutral_listeners_)
listener->on_scan_end();
#endif
}
#ifdef USE_ESP32_BLE_DEVICE
this->discovered_log_.clear();
#endif
@@ -263,17 +247,7 @@ void ESP32BLETracker::start_scan_(bool first) {
this->scan_params_.own_addr_type = BLE_ADDR_TYPE_PUBLIC;
this->scan_params_.scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL;
this->scan_params_.scan_interval = this->scan_interval_;
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
// Count fresh: an automation can start a scan before loop() refreshes the counts.
const uint32_t window = this->desired_scan_window_(this->count_client_states_().active);
if (window != this->scan_window_) {
// Guarantee the connection airtime instead of scanning wall to wall.
ESP_LOGV(TAG, "Connection active, using %" PRIu32 " unit scan window", window);
}
#else
const uint32_t window = this->scan_window_;
#endif
this->scan_params_.scan_window = window;
this->scan_params_.scan_window = this->scan_window_;
// Start timeout monitoring in loop() instead of using scheduler
// This prevents false reboots when the loop is blocked
@@ -434,11 +408,6 @@ void ESP32BLETracker::dump_config() {
" Continuous Scanning: %s",
this->scan_duration_, this->scan_interval_ * 0.625f, this->scan_window_ * 0.625f,
this->scan_active_ ? "ACTIVE" : "PASSIVE", YESNO(this->scan_continuous_));
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
if (this->connection_scan_window_ != 0) {
ESP_LOGCONFIG(TAG, " Connection Scan Window: %.1f ms", this->connection_scan_window_ * 0.625f);
}
#endif
ESP_LOGCONFIG(TAG,
" Scanner State: %s\n"
" Connecting: %d, discovered: %d, disconnecting: %d, active: %d",
@@ -518,18 +487,6 @@ void ESP32BLETracker::cleanup_scan_state_(bool is_stop_complete) {
// Reset timeout state machine instead of cancelling scheduler timeout
this->scan_timeout_state_ = ScanTimeoutState::INACTIVE;
this->notify_scan_end_();
this->set_scanner_state_(ScannerState::IDLE);
}
void ESP32BLETracker::notify_scan_end_() {
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
// Window-change restart continues the same scan period; the flag stays set
// across the stop and is cleared by the restart in start_scan_.
if (this->skip_next_scan_end_)
return;
#endif
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
for (auto *listener : this->listeners_)
listener->on_scan_end();
@@ -538,6 +495,8 @@ void ESP32BLETracker::notify_scan_end_() {
for (auto *listener : this->neutral_listeners_)
listener->on_scan_end();
#endif
this->set_scanner_state_(ScannerState::IDLE);
}
void ESP32BLETracker::handle_scanner_failure_() {
@@ -575,8 +534,6 @@ void ESP32BLETracker::try_promote_discovered_clients_() {
}
ESP_LOGD(TAG, "Promoting client to connect");
// A connect ends the scan period a window-change restart was continuing.
this->skip_next_scan_end_ = false;
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
this->update_coex_preference_(true);
#endif
@@ -169,9 +169,6 @@ class ESP32BLETracker final : public Component,
void set_scan_duration(uint32_t scan_duration) { scan_duration_ = scan_duration; }
void set_scan_interval(uint32_t scan_interval) { scan_interval_ = scan_interval; }
void set_scan_window(uint32_t scan_window) { scan_window_ = scan_window; }
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
void set_connection_scan_window(uint32_t scan_window) { connection_scan_window_ = scan_window; }
#endif
void set_scan_active(bool scan_active) { scan_active_ = scan_active; }
bool get_scan_active() const { return scan_active_; }
void set_scan_continuous(bool scan_continuous) { scan_continuous_ = scan_continuous; }
@@ -229,10 +226,7 @@ class ESP32BLETracker final : public Component,
ScannerState get_scanner_state() const { return this->scanner_state_; }
protected:
/// Returns true when a stop was issued to the controller.
bool stop_scan_();
/// Fire on_scan_end on every listener unless a window-change restart suppressed it.
void notify_scan_end_();
void stop_scan_();
/// Start a single scan by setting up the parameters and doing some esp-idf calls.
void start_scan_(bool first);
/// Called when a `ESP_GAP_BLE_SCAN_RESULT_EVT` event is received.
@@ -319,15 +313,6 @@ class ESP32BLETracker final : public Component,
uint32_t scan_duration_;
uint32_t scan_interval_;
uint32_t scan_window_;
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
/// Window used while a GATT connection is active; set by the user, or
/// defaulted when the window was raised to full duty (0 = no fallback).
uint32_t connection_scan_window_{0};
/// The window to scan at for the given number of active GATT connections.
uint32_t desired_scan_window_(uint8_t active) const {
return (this->connection_scan_window_ != 0 && active > 0) ? this->connection_scan_window_ : this->scan_window_;
}
#endif
esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS};
esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS};
@@ -345,20 +330,15 @@ class ESP32BLETracker final : public Component,
/// state_version_ to detect if any state changed since last iteration.
uint8_t last_processed_version_{0};
ScannerState scanner_state_{ScannerState::IDLE};
// Packed 1-bit flags.
bool scan_continuous_ : 1;
bool scan_active_ : 1;
bool scan_continuous_;
bool scan_active_;
#ifdef USE_OTA_STATE_LISTENER
bool scan_continuous_before_ota_ : 1 {false};
#endif
bool ble_was_disabled_ : 1 {true};
bool parse_advertisements_ : 1 {false};
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
/// Suppress the window-change restart's on_scan_end sweeps (stop and start).
bool skip_next_scan_end_ : 1 {false};
bool scan_continuous_before_ota_{false};
#endif
bool ble_was_disabled_{true};
bool parse_advertisements_{false};
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
bool coex_prefer_ble_ : 1 {false};
bool coex_prefer_ble_{false};
#endif
// Scan timeout state machine
enum class ScanTimeoutState : uint8_t {
@@ -366,10 +346,10 @@ class ESP32BLETracker final : public Component,
MONITORING, // Actively monitoring for timeout
EXCEEDED_WAIT, // Timeout exceeded, waiting one loop before reboot
};
ScanTimeoutState scan_timeout_state_{ScanTimeoutState::INACTIVE};
uint32_t scan_start_time_{0};
/// Precomputed timeout value: scan_duration_ * 2000
uint32_t scan_timeout_ms_{0};
ScanTimeoutState scan_timeout_state_{ScanTimeoutState::INACTIVE};
};
// NOLINTNEXTLINE
+25
View File
@@ -26,6 +26,16 @@ _LOGGER = logging.getLogger(__name__)
# Components can request high performance networking and this configures lwip and WiFi settings
KEY_HIGH_PERFORMANCE_NETWORKING = "high_performance_networking"
CONF_ENABLE_HIGH_PERFORMANCE = "enable_high_performance"
CONF_TCP_SEND_BUFFER = "tcp_send_buffer"
# lwIP queues at most this many unsent/unacked bytes per TCP socket; the
# stock ESP-IDF default (5744 bytes) stalls bursty senders like a Bluetooth
# proxy streaming GATT notifications. Bounds follow the lwIP guidance for the
# default 1440 byte MSS: at least 2 x MSS, at most 65535 without window
# scaling. The cap is kept even when window scaling is on (high performance
# with PSRAM) as a deliberate conservative bound.
TCP_SEND_BUFFER_MIN = 2880
TCP_SEND_BUFFER_MAX = 65535
# Network priority tracking infrastructure
# Components can query this to determine their relative setup priority.
@@ -306,6 +316,11 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_ENABLE_HIGH_PERFORMANCE): cv.All(
cv.boolean, cv.only_on_esp32
),
cv.Optional(CONF_TCP_SEND_BUFFER): cv.All(
cv.validate_bytes,
cv.int_range(min=TCP_SEND_BUFFER_MIN, max=TCP_SEND_BUFFER_MAX),
cv.only_on_esp32,
),
cv.Optional(CONF_PRIORITY): _validate_priority_list,
}
),
@@ -446,6 +461,16 @@ async def to_code(config):
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_RECVMBOX_SIZE", 64)
add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_RECVMBOX_SIZE", 64)
# After the high performance block so an explicit size wins over the
# bundle's 65534 (last write wins in the sdkconfig store).
if (tcp_send_buffer := config.get(CONF_TCP_SEND_BUFFER)) is not None:
if CORE.is_esp32 and should_enable:
_LOGGER.info(
"TCP send buffer set to %d bytes by configuration (overriding high performance value)",
tcp_send_buffer,
)
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT", tcp_send_buffer)
if CORE.is_nrf52:
zephyr_add_prj_conf("NETWORKING", True)
zephyr_add_prj_conf("NET_IPV6", True)
@@ -6,6 +6,7 @@
#include "esp_err.h"
#include "esp_netif.h"
#include "esp_event.h"
#include "lwip/opt.h"
#ifdef USE_NETWORK_DEFAULT_ROUTE
#include "esphome/core/application.h"
@@ -43,6 +44,15 @@ void NetworkComponent::setup() {
}
}
void NetworkComponent::dump_config() {
// The effective compile-time lwIP value, so the log reflects tcp_send_buffer
// or the high performance bundle when either changed it.
ESP_LOGCONFIG(TAG,
"Network:\n"
" TCP send buffer: %d bytes",
TCP_SND_BUF);
}
#ifdef USE_NETWORK_DEFAULT_ROUTE
static esp_netif_t *connected_wifi_netif() {
#ifdef USE_WIFI
@@ -13,6 +13,7 @@ namespace esphome::network {
class NetworkComponent final : public Component {
public:
void setup() override;
void dump_config() override;
// AFTER_BLUETOOTH: BLE controller must initialize before esp_netif_init per IDF guidance.
float get_setup_priority() const override { return setup_priority::AFTER_BLUETOOTH; }
@@ -1,19 +0,0 @@
esphome:
name: scan-window-explicit
esp32:
board: esp32dev
framework:
type: esp-idf
wifi:
ssid: MySSID
esp32_ble_tracker:
scan_parameters:
window: 30ms
bluetooth_proxy:
active: true
api:
@@ -1,17 +0,0 @@
esphome:
name: scan-window-raised
esp32:
board: esp32dev
framework:
type: esp-idf
wifi:
ssid: MySSID
esp32_ble_tracker:
bluetooth_proxy:
active: true
api:
@@ -1,12 +0,0 @@
esphome:
name: scan-window-scan-only
esp32:
board: esp32dev
framework:
type: esp-idf
wifi:
ssid: MySSID
esp32_ble_tracker:
@@ -1,14 +0,0 @@
esphome:
name: scan-window-user-scan-only
esp32:
board: esp32dev
framework:
type: esp-idf
wifi:
ssid: MySSID
esp32_ble_tracker:
scan_parameters:
connection_scan_window: 20ms
@@ -12,12 +12,11 @@ arbiter a full-duty scan would starve wifi, so the 30 ms default is kept.
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.ble_device_base import CONF_CONNECTION_SCAN_WINDOW, to_ble_units
from esphome.components.ble_device_base import to_ble_units
from esphome.components.const import CONF_SCAN_PARAMETERS, CONF_WINDOW
from esphome.components.esp32 import KEY_IDF_VERSION
from esphome.components.esp32_ble_tracker import (
@@ -121,103 +120,3 @@ def test_short_interval_without_window_still_rejected(
stage_esp32("5.5.5", wifi=True)
with pytest.raises(cv.Invalid, match="needs to be smaller than scan interval"):
_scan_params({"scan_parameters": {"interval": "20ms"}})
# The connection-time fallback window: while a GATT connection is active the
# scanner drops from a raised full-duty window back to this value so the
# connection gets guaranteed airtime.
def test_raise_arms_connection_scan_window_default(
stage_esp32: Callable[..., None],
) -> None:
stage_esp32("5.5.5", wifi=True)
params = _scan_params({})
assert params[CONF_WINDOW] == params[CONF_INTERVAL]
assert to_ble_units(params[CONF_CONNECTION_SCAN_WINDOW]) == 48
def test_user_connection_scan_window_survives_raise(
stage_esp32: Callable[..., None],
) -> None:
stage_esp32("5.5.5", wifi=True)
params = _scan_params({"scan_parameters": {"connection_scan_window": "60ms"}})
assert params[CONF_WINDOW] == params[CONF_INTERVAL]
assert to_ble_units(params[CONF_CONNECTION_SCAN_WINDOW]) == 96
def test_unraised_window_gets_no_connection_scan_window_default(
stage_esp32: Callable[..., None],
) -> None:
stage_esp32("5.5.4", wifi=True)
assert CONF_CONNECTION_SCAN_WINDOW not in _scan_params({})
def test_connection_scan_window_above_interval_rejected(
stage_esp32: Callable[..., None],
) -> None:
stage_esp32("5.5.5", wifi=True)
with pytest.raises(
cv.Invalid, match="connection_scan_window .* needs to be smaller"
):
_scan_params({"scan_parameters": {"connection_scan_window": "400ms"}})
def test_connection_scan_window_above_window_rejected(
stage_esp32: Callable[..., None],
) -> None:
"""A connection window above the (post-raise) window would widen the scan
during connections; the reject runs after the raise so a fallback below a
raised window still validates (covered by the survives-raise test)."""
stage_esp32("5.5.5", wifi=True)
with pytest.raises(
cv.Invalid, match="connection_scan_window .* needs to be smaller"
):
_scan_params(
{"scan_parameters": {"window": "30ms", "connection_scan_window": "300ms"}}
)
def test_connection_scan_window_truncation_collapse_rejected(
stage_esp32: Callable[..., None],
) -> None:
"""A connection window that truncates into the interval's 0.625 ms unit
would silently program a full-duty scan during connections."""
stage_esp32("5.5.5", wifi=True)
with pytest.raises(cv.Invalid, match="connection_scan_window .* both truncate"):
_scan_params(
{
"scan_parameters": {
"interval": "320.5ms",
"connection_scan_window": "320.2ms",
}
}
)
@pytest.mark.parametrize(
("config_file", "window_call", "connection_call", "warns"),
[
# Raised window with GATT clients: the injected fallback is emitted.
("scan_window_raised.yaml", "set_scan_window(512)", True, False),
# Explicit window: nothing injected.
("scan_window_explicit.yaml", "set_scan_window(48)", False, False),
# Scan-only build compiles the path out: the injected default is
# dropped silently, a user-set value warns.
("scan_window_scan_only.yaml", "set_scan_window(512)", False, False),
("scan_window_user_set_scan_only.yaml", "set_scan_window(512)", False, True),
],
)
def test_connection_scan_window_codegen(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
caplog: pytest.LogCaptureFixture,
config_file: str,
window_call: str,
connection_call: bool,
warns: bool,
) -> None:
main_cpp = generate_main(component_config_path(config_file))
assert window_call in main_cpp
assert ("set_connection_scan_window(48)" in main_cpp) == connection_call
assert ("'connection_scan_window' has no effect" in caplog.text) == warns
@@ -0,0 +1,14 @@
esphome:
name: test
esp32:
board: esp32dev
framework:
type: esp-idf
wifi:
ssid: "test_ssid"
password: "test_password"
network:
tcp_send_buffer: 32kB
@@ -0,0 +1,15 @@
esphome:
name: test
esp32:
board: esp32dev
framework:
type: esp-idf
wifi:
ssid: "test_ssid"
password: "test_password"
network:
enable_high_performance: true
tcp_send_buffer: 16384
@@ -0,0 +1,85 @@
"""Tests for the ``network: tcp_send_buffer:`` option.
The option sets lwIP's per-socket TCP send buffer
(CONFIG_LWIP_TCP_SND_BUF_DEFAULT) on ESP-IDF. The stock default (5744 bytes)
stalls bursty senders such as a Bluetooth proxy streaming GATT notifications;
until now the only way to raise it was the all-or-nothing
``enable_high_performance`` bundle.
"""
from collections.abc import Callable
from pathlib import Path
import pytest
from voluptuous import Invalid
from esphome import config_validation as cv
from esphome.components.esp32.const import (
KEY_SDKCONFIG_OPTIONS,
KEY_VARIANT,
VARIANT_ESP32,
)
from esphome.components.network import (
CONF_TCP_SEND_BUFFER,
CONFIG_SCHEMA,
TCP_SEND_BUFFER_MAX,
TCP_SEND_BUFFER_MIN,
)
from esphome.const import KEY_ESP32, KEY_FRAMEWORK_VERSION, PlatformFramework
from esphome.core import CORE
from tests.component_tests.types import SetCoreConfigCallable
def _sdkconfig_option(name: str) -> int | None:
return CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS].get(name)
def test_tcp_send_buffer_sets_sdkconfig(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
generate_main(component_config_path("tcp_send_buffer.yaml"))
assert _sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT") == 32000
def test_tcp_send_buffer_overrides_high_performance(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""An explicit size wins over the high performance bundle's 65534."""
generate_main(component_config_path("tcp_send_buffer_high_perf.yaml"))
assert _sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT") == 16384
@pytest.mark.parametrize("value", [TCP_SEND_BUFFER_MIN, TCP_SEND_BUFFER_MAX])
def test_boundary_values_accepted(
set_core_config: SetCoreConfigCallable, value: int
) -> None:
set_core_config(
PlatformFramework.ESP32_IDF,
core_data={KEY_FRAMEWORK_VERSION: cv.Version(5, 5, 5)},
platform_data={KEY_VARIANT: VARIANT_ESP32},
)
assert CONFIG_SCHEMA({"tcp_send_buffer": value})[CONF_TCP_SEND_BUFFER] == value
@pytest.mark.parametrize("value", ["1kB", "128kB"])
def test_out_of_range_rejected(
set_core_config: SetCoreConfigCallable, value: str
) -> None:
set_core_config(
PlatformFramework.ESP32_IDF,
core_data={KEY_FRAMEWORK_VERSION: cv.Version(5, 5, 5)},
platform_data={KEY_VARIANT: VARIANT_ESP32},
)
with pytest.raises(Invalid):
CONFIG_SCHEMA({"tcp_send_buffer": value})
def test_rejected_on_esp8266(set_core_config: SetCoreConfigCallable) -> None:
set_core_config(
PlatformFramework.ESP8266_ARDUINO,
core_data={KEY_FRAMEWORK_VERSION: cv.Version(3, 1, 2)},
)
with pytest.raises(Invalid, match="esp32"):
CONFIG_SCHEMA({"tcp_send_buffer": "32kB"})
@@ -2,3 +2,4 @@
network:
enable_high_performance: true
tcp_send_buffer: 32kB