From 9a5aab63d5f0d425b9c19656306baada6329fbde Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 9 Aug 2026 16:24:03 -0500 Subject: [PATCH] Address review: retry a dropped freed-slot notification too The connections-free retry left its sibling asymmetric: on the same full buffer the slot could report free while the device still read as connected, with nothing resending the connected=false. A one-deep latch (address + error) drains at the same 100 ms cadence and re-latches on a repeat failure; a re-reservation of the address clears it, since the client re-requesting the connect has already acted on the disconnect and a late resend would shadow the new connection. A lost connected=true stays client-timeout territory. --- .../bluetooth_proxy/bluetooth_proxy.cpp | 19 ++++++++++++++++++- .../bluetooth_proxy/bluetooth_proxy.h | 4 ++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index e4cc0cb12a..c5281c5b4f 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -206,6 +206,11 @@ BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool rese return connection; if (reserve && conn_addr == 0) { + if (address == this->pending_disconnection_address_) { + // The client re-requested this address: it already acted on the + // disconnect, and a late resend would shadow the new connection. + this->pending_disconnection_address_ = 0; + } connection->send_service_ = INIT_SENDING_SERVICES; connection->set_address(address); // All connections must start at INIT @@ -487,6 +492,13 @@ void BluetoothProxy::loop() { return; this->last_advertisement_flush_time_ = now; + if (this->pending_disconnection_address_ != 0 && this->api_connection_ != nullptr) { + // Paced resend of a dropped freed-slot notification; a repeat failure + // re-latches through send_device_connection itself. + uint64_t addr = this->pending_disconnection_address_; + this->pending_disconnection_address_ = 0; + this->send_device_connection(addr, false, 0, this->pending_disconnection_error_); + } if (this->connections_free_pending_ && this->api_connection_ != nullptr) { // Resend a dropped slot-state update, paced by the 100 ms gate so the // retry does not hammer the congestion it exists to survive; the @@ -633,7 +645,12 @@ void BluetoothProxy::send_device_connection(uint64_t address, bool connected, ui call.connected = connected; call.mtu = mtu; call.error = error; - this->api_connection_->send_message(call); + if (!this->api_connection_->send_message(call) && !connected) { + // The freed-slot notification must eventually arrive; a lost + // connected=true is covered by the client's own connect timeout. + this->pending_disconnection_address_ = address; + this->pending_disconnection_error_ = error; + } } void BluetoothProxy::send_connections_free() { if (this->api_connection_ != nullptr) { diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index 26f99fcca2..584f390f35 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -251,6 +251,10 @@ class BluetoothProxy final : public Component { // Pre-allocated response message - always ready to send api::BluetoothConnectionsFreeResponse connections_free_response_; + // One-deep retry latch for a dropped connected=false notification (the + // freed-slot half of the pair the connections-free retry covers). + uint64_t pending_disconnection_address_{0}; + conn_err_t pending_disconnection_error_{0}; // Group 4: 1-byte types grouped together bool active_;