[esp32_ble_tracker] Hold COEX_PREFER_BT for the lifetime of any active connection

count_client_states_() only tracks transient states (CONNECTING, DISCOVERED,
DISCONNECTING). When a connection settles into CONNECTED/ESTABLISHED, all
counted states go to zero, and loop() reverts coex from PREFER_BT back to
BALANCE while the connection is still live. Sustained WiFi traffic then
crowds out the BT radio's ability to receive the peer's GATT response,
causing esp_ble_gattc_write_char_descr to time out at ~20s with a
host-synthesized status=0x85 (commonly reported as status=133).

Fix: count CONNECTED + ESTABLISHED clients as "active" and gate the coex
revert on the active count. Coex now stays at PREFER_BT for the entire
lifetime of any active connection and only returns to BALANCE when all
connections are torn down.

Validated end-to-end: aioesphomeapi-driven write_descriptor on a Yale
BETA211123 lock at LOG_LEVEL_VERBOSE (heavy WiFi traffic, the
bug-triggering condition) — pre-patch: 19s hang then status=133.
Post-patch: 228ms SUCCESS. Verified live on an M5 Atom Lite running as a
bluetooth_proxy with host BlueZ stopped (M5 as the only path): lock ops
succeed cleanly with zero status=133 errors. A multi-day production
soak window will be added to the PR body before merge.
This commit is contained in:
Paul Lazarre
2026-04-26 19:23:20 -05:00
parent e87e78c544
commit 8c0b0b1302
2 changed files with 22 additions and 6 deletions
@@ -166,8 +166,9 @@ void ESP32BLETracker::loop() {
ClientStateCounts counts = this->count_client_states_();
if (counts != this->client_state_counts_) {
this->client_state_counts_ = counts;
ESP_LOGD(TAG, "connecting: %d, discovered: %d, disconnecting: %d", this->client_state_counts_.connecting,
this->client_state_counts_.discovered, this->client_state_counts_.disconnecting);
ESP_LOGD(TAG, "connecting: %d, discovered: %d, disconnecting: %d, active: %d", this->client_state_counts_.connecting,
this->client_state_counts_.discovered, this->client_state_counts_.disconnecting,
this->client_state_counts_.active);
}
// Scanner failure: reached when set_scanner_state_(FAILED) or scan_set_param_failed_ set
@@ -193,7 +194,13 @@ void ESP32BLETracker::loop() {
// all clients are idle (their state changes increment version when they finish)
if (this->scanner_state_ == ScannerState::IDLE && !counts.connecting && !counts.disconnecting && !counts.discovered) {
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
this->update_coex_preference_(false);
// Only revert to BALANCE when no connections are active. Established connections
// continue to need PREFER_BT so the lock's GATT Write Response can reach us while
// WiFi traffic (advertisement upload, log streaming) competes for the shared radio.
// Reverting too early causes Bluedroid to time out at ~20s and synthesize status=133.
if (!counts.active) {
this->update_coex_preference_(false);
}
#endif
if (this->scan_continuous_) {
this->start_scan_(false); // first = false
@@ -701,9 +708,10 @@ void ESP32BLETracker::dump_config() {
this->scan_active_ ? "ACTIVE" : "PASSIVE", YESNO(this->scan_continuous_));
ESP_LOGCONFIG(TAG,
" Scanner State: %s\n"
" Connecting: %d, discovered: %d, disconnecting: %d",
" Connecting: %d, discovered: %d, disconnecting: %d, active: %d",
this->scanner_state_to_string_(this->scanner_state_), this->client_state_counts_.connecting,
this->client_state_counts_.discovered, this->client_state_counts_.disconnecting);
this->client_state_counts_.discovered, this->client_state_counts_.disconnecting,
this->client_state_counts_.active);
if (this->scan_start_fail_count_) {
ESP_LOGCONFIG(TAG, " Scan Start Fail Count: %d", this->scan_start_fail_count_);
}
@@ -160,9 +160,13 @@ struct ClientStateCounts {
uint8_t connecting = 0;
uint8_t discovered = 0;
uint8_t disconnecting = 0;
// CONNECTED + ESTABLISHED clients. Tracked so coex stays at PREFER_BT
// while active connections may still need to send/receive GATT traffic.
uint8_t active = 0;
bool operator==(const ClientStateCounts &other) const {
return connecting == other.connecting && discovered == other.discovered && disconnecting == other.disconnecting;
return connecting == other.connecting && discovered == other.discovered && disconnecting == other.disconnecting &&
active == other.active;
}
bool operator!=(const ClientStateCounts &other) const { return !(*this == other); }
@@ -381,6 +385,10 @@ class ESP32BLETracker : public Component,
case ClientState::CONNECTING:
counts.connecting++;
break;
case ClientState::CONNECTED:
case ClientState::ESTABLISHED:
counts.active++;
break;
default:
break;
}