From 9f28638ee77056850456e505f38904613f03dd0f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 11 Aug 2026 19:15:21 -0500 Subject: [PATCH] [bluetooth_proxy] Reset every owed reply in one place (#18276) --- .../bluetooth_proxy/bluetooth_proxy.cpp | 46 +++++++++++-------- .../bluetooth_proxy/bluetooth_proxy.h | 6 +++ 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index 4c4cbf27a2..0d91b541e8 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -704,6 +704,31 @@ void BluetoothProxy::bluetooth_set_connection_params(const api::BluetoothSetConn #endif // !BLUETOOTH_CONNECTION_HAS_GATT +void BluetoothProxy::reset_owed_replies_() { + this->connections_free_pending_ = false; +#ifdef USE_BLE_SCANNER_STATE_CALLBACK + // Owed on unsubscribe; on subscribe the trailing send_scanner_state_() + // re-drives it from the hub, so clearing it there is free. + this->scanner_state_pending_ = false; +#else + // Force a poll-arm mismatch: a frame refused at subscribe time could + // otherwise match the stale detector and never be retried. Inert on + // unsubscribe: loop() returns at the no-subscriber gate before the + // detector runs, and a re-subscribe re-arms this anyway. + this->last_scan_running_ = !this->hub_->scan_running(); +#endif +#ifdef BLUETOOTH_CONNECTION_HAS_GATT + this->pending_unpairing_.clear(); + this->pending_disconnections_.fill({}); + for (uint8_t i = 0; i < this->connection_count_; i++) { + // Neither a partial stream's tail nor an owed done belongs to the next + // session; silence (the client's timeout) arbitrates. + this->connections_[i]->park_service_stream_(); + this->connections_[i]->clear_pending_ack_(); + } +#endif +} + void BluetoothProxy::subscribe_api_connection(api::APIConnection *api_connection, uint32_t flags) { if (api_connection != this->api_connection_) { if (this->api_connection_ != nullptr) { @@ -720,18 +745,7 @@ void BluetoothProxy::subscribe_api_connection(api::APIConnection *api_connection } // Stale retry latches belong to the previous subscriber's session; a // re-subscribe by the current one keeps what it is still owed. - this->connections_free_pending_ = false; -#ifdef BLUETOOTH_CONNECTION_HAS_GATT - this->pending_unpairing_.clear(); - for (uint8_t i = 0; i < this->connection_count_; i++) { - // Neither a partial stream's tail nor an owed done belongs to the new - // session; silence (the client's timeout) arbitrates. - this->connections_[i]->park_service_stream_(); - // An ack owed to the previous subscriber means nothing to the new one. - this->connections_[i]->clear_pending_ack_(); - } - this->pending_disconnections_.fill({}); -#endif + this->reset_owed_replies_(); } this->api_connection_ = api_connection; #ifdef USE_BLE_SCANNER_STATE_CALLBACK @@ -748,13 +762,7 @@ void BluetoothProxy::unsubscribe_api_connection(api::APIConnection *api_connecti return; } this->api_connection_ = nullptr; - this->connections_free_pending_ = false; -#ifdef BLUETOOTH_CONNECTION_HAS_GATT - this->pending_unpairing_.clear(); -#endif -#ifdef USE_BLE_SCANNER_STATE_CALLBACK - this->scanner_state_pending_ = false; -#endif + this->reset_owed_replies_(); } void BluetoothProxy::send_connections_free() { diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index e7c7c3cb20..16b153625f 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -286,6 +286,12 @@ class BluetoothProxy final : public Component { void latch_pending_disconnection_(uint64_t address, conn_err_t error); #endif + /// Drop everything the ending session was owed. One list, so a new latch is + /// one edit rather than two call sites where an omission looks deliberate. + /// Drops state only, never sends: api_connection_ is the departing + /// subscriber on subscribe and nullptr on unsubscribe. + void reset_owed_replies_(); + // Memory optimized layout for 32-bit systems // Group 1: Pointers (4 bytes each, naturally aligned) api::APIConnection *api_connection_{nullptr};