mirror of
https://github.com/esphome/esphome.git
synced 2026-08-24 07:06:20 +00:00
Validate connection window against the window; skip no-op restarts; pack tracker flags
This commit is contained in:
@@ -149,6 +149,7 @@ 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:
|
||||
@@ -177,14 +178,16 @@ 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.
|
||||
parameters, so no re-validation is needed. Also checks the connection
|
||||
window against the window here, after the raise, so a fallback below a
|
||||
raised window validates while one that would widen the scan is rejected.
|
||||
"""
|
||||
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])
|
||||
@@ -197,6 +200,16 @@ def _raise_defaulted_scan_window(config: ConfigType) -> ConfigType:
|
||||
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, the exact
|
||||
# airtime contention the option exists to remove.
|
||||
raise cv.Invalid(
|
||||
f"{CONF_CONNECTION_SCAN_WINDOW} ({connection_window}) needs to be "
|
||||
f"smaller than the scan window ({params[CONF_WINDOW]})"
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
@@ -311,6 +324,15 @@ async def to_code(config: ConfigType) -> None:
|
||||
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:
|
||||
# A user-set value that cannot take effect deserves a heads-up;
|
||||
# the auto-injected default is dropped 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]))
|
||||
|
||||
@@ -158,6 +158,10 @@ void ESP32BLETracker::loop() {
|
||||
// in flight, matching the restart gate below.
|
||||
if (this->using_connection_window_ && !counts.active && !counts.disconnecting && this->scan_continuous_ &&
|
||||
this->scanner_state_ == ScannerState::RUNNING) {
|
||||
// The restart continues the same logical scan period, so the listeners
|
||||
// must not get an extra on_scan_end (ble_rssi would publish NAN for
|
||||
// beacons not yet seen in the period).
|
||||
this->skip_next_scan_end_ = true;
|
||||
this->stop_scan_();
|
||||
}
|
||||
#endif
|
||||
@@ -247,7 +251,15 @@ void ESP32BLETracker::start_scan_(bool first) {
|
||||
}
|
||||
this->set_scanner_state_(ScannerState::STARTING);
|
||||
ESP_LOGV(TAG, "Starting scan, set scanner state to STARTING.");
|
||||
if (!first) {
|
||||
bool notify_scan_end = !first;
|
||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
|
||||
if (this->skip_next_scan_end_) {
|
||||
// Window-change restart: the same logical scan period continues.
|
||||
this->skip_next_scan_end_ = false;
|
||||
notify_scan_end = false;
|
||||
}
|
||||
#endif
|
||||
if (notify_scan_end) {
|
||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
|
||||
for (auto *listener : this->listeners_)
|
||||
listener->on_scan_end();
|
||||
@@ -267,8 +279,12 @@ void ESP32BLETracker::start_scan_(bool first) {
|
||||
uint32_t window = this->scan_window_;
|
||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
|
||||
// Count fresh rather than reading the loop() cache: an automation can start
|
||||
// a scan before loop() has refreshed the counts for a new connection.
|
||||
this->using_connection_window_ = this->connection_scan_window_ != 0 && this->count_client_states_().active > 0;
|
||||
// a scan before loop() has refreshed the counts for a new connection. An
|
||||
// equal connection window changes nothing, so it must not arm the
|
||||
// last-disconnect restart in loop().
|
||||
this->using_connection_window_ = this->connection_scan_window_ != 0 &&
|
||||
this->connection_scan_window_ != this->scan_window_ &&
|
||||
this->count_client_states_().active > 0;
|
||||
if (this->using_connection_window_) {
|
||||
// While a GATT connection is active, fall back to the connection scan
|
||||
// window so the connection events get guaranteed airtime instead of
|
||||
|
||||
@@ -338,20 +338,24 @@ 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};
|
||||
bool scan_continuous_;
|
||||
bool scan_active_;
|
||||
// Single-bit flags packed into one byte so new flags stop growing the class.
|
||||
bool scan_continuous_ : 1;
|
||||
bool scan_active_ : 1;
|
||||
#ifdef USE_OTA_STATE_LISTENER
|
||||
bool scan_continuous_before_ota_{false};
|
||||
bool scan_continuous_before_ota_ : 1 {false};
|
||||
#endif
|
||||
bool ble_was_disabled_{true};
|
||||
bool parse_advertisements_{false};
|
||||
bool ble_was_disabled_ : 1 {true};
|
||||
bool parse_advertisements_ : 1 {false};
|
||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
|
||||
/// The running scan was started with connection_scan_window_; lets loop()
|
||||
/// restart the scan at the configured window when the last connection drops.
|
||||
bool using_connection_window_{false};
|
||||
bool using_connection_window_ : 1 {false};
|
||||
/// The next start_scan_ continues the same logical scan period (set by the
|
||||
/// window-change restart), so the on_scan_end sweep is skipped once.
|
||||
bool skip_next_scan_end_ : 1 {false};
|
||||
#endif
|
||||
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
|
||||
bool coex_prefer_ble_{false};
|
||||
bool coex_prefer_ble_ : 1 {false};
|
||||
#endif
|
||||
// Scan timeout state machine
|
||||
enum class ScanTimeoutState : uint8_t {
|
||||
|
||||
@@ -163,6 +163,21 @@ def test_connection_scan_window_above_interval_rejected(
|
||||
_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:
|
||||
|
||||
Reference in New Issue
Block a user