Return to the configured window when the last connection drops; review fixes

This commit is contained in:
J. Nick Koston
2026-08-21 16:49:20 -05:00
parent a3675dfacb
commit fe3b7ed84c
5 changed files with 50 additions and 15 deletions
@@ -234,13 +234,13 @@ def validate_scan_parameters(config: ConfigType) -> ConfigType:
# 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)
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."
)
for name, value in windows:
if to_ble_units(value) == interval_units and value < interval:
raise cv.Invalid(
f"Scan {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."
)
if interval.total_microseconds * 3 > duration.total_microseconds:
raise cv.Invalid(
@@ -189,7 +189,9 @@ def _raise_defaulted_scan_window(config: ConfigType) -> ConfigType:
params[CONF_WINDOW] = copy.copy(params[CONF_INTERVAL])
# A full-duty scan must not compete with an active GATT connection
# for airtime, so arm the connection-time fallback window as well
# unless the user picked one themselves.
# unless the user picked one themselves. Injected after validation;
# safe only because it equals the window default, which already
# passed the <= interval and controller range checks.
if CONF_CONNECTION_SCAN_WINDOW not in params:
params[CONF_CONNECTION_SCAN_WINDOW] = cv.positive_time_period(
ble_device_base.DEFAULT_SCAN_WINDOW
@@ -144,6 +144,16 @@ void ESP32BLETracker::loop() {
(this->scan_set_param_failed_ && this->scanner_state_ == ScannerState::RUNNING)) {
this->handle_scanner_failure_();
}
// The window is chosen at scan start, so when the last connection drops
// mid-scan the reduced connection-time window would persist for the rest
// of the scan period (up to scan_duration_); stop the scan so the restart
// below returns to the configured window. Only for continuous scanning:
// a user-started scan would not be restarted.
if (this->scan_window_reduced_ && !counts.active && this->scan_continuous_ &&
this->scanner_state_ == ScannerState::RUNNING) {
this->stop_scan_();
}
/*
Avoid starting the scanner if:
@@ -248,11 +258,11 @@ void ESP32BLETracker::start_scan_(bool first) {
this->scan_params_.scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL;
this->scan_params_.scan_interval = this->scan_interval_;
uint32_t window = this->scan_window_;
if (this->connection_scan_window_ != 0 && this->client_state_counts_.active > 0) {
// The defaulted window was raised to full duty for advertisement
// throughput; while a GATT connection is active, fall back so the
// connection events get guaranteed airtime instead of competing with
// a wall-to-wall scan on the shared radio.
this->scan_window_reduced_ = this->connection_scan_window_ != 0 && this->client_state_counts_.active > 0;
if (this->scan_window_reduced_) {
// While a GATT connection is active, fall back to the connection scan
// window so the connection events get guaranteed airtime instead of
// competing with a wall-to-wall scan on the shared radio.
ESP_LOGV(TAG, "Connection active, using %" PRIu32 " unit scan window", this->connection_scan_window_);
window = this->connection_scan_window_;
}
@@ -417,6 +427,9 @@ 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_));
if (this->connection_scan_window_ != 0) {
ESP_LOGCONFIG(TAG, " Connection Scan Window: %.1f ms", this->connection_scan_window_ * 0.625f);
}
ESP_LOGCONFIG(TAG,
" Scanner State: %s\n"
" Connecting: %d, discovered: %d, disconnecting: %d, active: %d",
@@ -314,8 +314,8 @@ class ESP32BLETracker final : public Component,
uint32_t scan_duration_;
uint32_t scan_interval_;
uint32_t scan_window_;
/// Window used while a GATT connection is active; only set when the
/// defaulted window was raised to full duty (0 = no fallback).
/// 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};
esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS};
esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS};
@@ -341,6 +341,9 @@ class ESP32BLETracker final : public Component,
#endif
bool ble_was_disabled_{true};
bool parse_advertisements_{false};
/// The running scan was started with connection_scan_window_; lets loop()
/// restart the scan at the configured window when the last connection drops.
bool scan_window_reduced_{false};
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
bool coex_prefer_ble_{false};
#endif
@@ -163,6 +163,23 @@ def test_connection_scan_window_above_interval_rejected(
_scan_params({"scan_parameters": {"connection_scan_window": "400ms"}})
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 window .* both truncate"):
_scan_params(
{
"scan_parameters": {
"interval": "320.5ms",
"connection_scan_window": "320.2ms",
}
}
)
def test_codegen_connection_window_when_raised(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],