mirror of
https://github.com/esphome/esphome.git
synced 2026-08-22 22:26:21 +00:00
[bluetooth_proxy] Retry dropped services-done, disconnect and scanner-state notifications (#18225)
This commit is contained in:
@@ -82,6 +82,16 @@ inline conn_err_t clear_gatt_cache(uint64_t) { return GATT_NOT_CONNECTED; }
|
||||
// send_service_ cursor states; >= 0 is the next service index to stream.
|
||||
static constexpr int DONE_SENDING_SERVICES = -2;
|
||||
static constexpr int INIT_SENDING_SERVICES = -3;
|
||||
static constexpr int SERVICES_DONE_PENDING = -4; // all batches delivered, done-message still owed
|
||||
// Every sentinel must stay below the >= 0 streaming gate and clear of
|
||||
// GATT_NOT_CONNECTED (-1) so cursor and error values can never be confused.
|
||||
static_assert(DONE_SENDING_SERVICES < 0 && INIT_SENDING_SERVICES < 0 && SERVICES_DONE_PENDING < 0);
|
||||
static_assert(DONE_SENDING_SERVICES != GATT_NOT_CONNECTED && INIT_SENDING_SERVICES != GATT_NOT_CONNECTED &&
|
||||
SERVICES_DONE_PENDING != GATT_NOT_CONNECTED);
|
||||
// Owed-done retries stop here (~3 s at the 100 ms drain cadence): a done
|
||||
// delivered near the client's 30 s timeout could land on a fresh request's
|
||||
// empty accumulator and cache as an empty database.
|
||||
static constexpr uint8_t SERVICES_DONE_RETRY_LIMIT = 30;
|
||||
|
||||
// ---- Service-streaming size budget, shared by every platform's streamer ----
|
||||
|
||||
|
||||
@@ -407,20 +407,16 @@ void BluedroidGattClient::stream_service_batch(BluetoothConnection &conn) {
|
||||
return;
|
||||
}
|
||||
if (conn.send_service_ >= this->service_total_) {
|
||||
conn.send_service_ = DONE_SENDING_SERVICES;
|
||||
conn.proxy_->send_gatt_services_done(conn.address_);
|
||||
this->release_services();
|
||||
conn.send_services_done_();
|
||||
return;
|
||||
}
|
||||
|
||||
// The subscriber vanished mid-stream: park the cursor at done WITHOUT
|
||||
// sending services-done (a resubscribing client gets silence and its 30 s
|
||||
// timeout, never an authoritative partial list).
|
||||
// The subscriber vanished mid-stream.
|
||||
auto *api_conn = conn.proxy_->get_api_connection();
|
||||
if (api_conn == nullptr) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] API connection lost while streaming services", conn.connection_index_, conn.address_str_);
|
||||
conn.send_service_ = DONE_SENDING_SERVICES;
|
||||
this->release_services();
|
||||
conn.park_service_stream_();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -299,25 +299,39 @@ conn_err_t BluetoothConnection::update_connection_params(uint16_t min_interval,
|
||||
|
||||
// ---- Service streaming ----
|
||||
|
||||
void BluetoothConnection::send_services_done_() {
|
||||
if (this->proxy_->send_gatt_services_done(this->address_)) {
|
||||
// Sent, or subscriber gone (park silently; its timeout arbitrates).
|
||||
this->send_service_ = DONE_SENDING_SERVICES;
|
||||
return;
|
||||
}
|
||||
if (this->send_service_ != SERVICES_DONE_PENDING) {
|
||||
// Warn on the transition only; retries stay silent.
|
||||
ESP_LOGW(TAG, "[%d] [%s] Failed to send services done, retrying", this->connection_index_, this->address_str_);
|
||||
this->services_done_retries_ = 0;
|
||||
this->send_service_ = SERVICES_DONE_PENDING;
|
||||
} else if (++this->services_done_retries_ >= SERVICES_DONE_RETRY_LIMIT) {
|
||||
// Undeliverable (see SERVICES_DONE_RETRY_LIMIT); silence arbitrates.
|
||||
ESP_LOGW(TAG, "[%d] [%s] Services done undeliverable, abandoning", this->connection_index_, this->address_str_);
|
||||
this->send_service_ = DONE_SENDING_SERVICES;
|
||||
}
|
||||
}
|
||||
|
||||
void BluetoothConnection::send_service_for_discovery_() {
|
||||
auto table = this->backend_->get_service_table();
|
||||
if (this->send_service_ >= table.service_count) {
|
||||
this->send_service_ = DONE_SENDING_SERVICES;
|
||||
this->proxy_->send_gatt_services_done(this->address_);
|
||||
this->backend_->release_services();
|
||||
this->send_services_done_();
|
||||
return;
|
||||
}
|
||||
|
||||
// The subscriber vanished mid-stream: park the cursor at done WITHOUT
|
||||
// sending services-done (a resubscribing client gets silence and its 30 s
|
||||
// timeout, never an authoritative partial list) and free the table; the
|
||||
// api-gone sweep tears the connection down anyway.
|
||||
// The subscriber vanished mid-stream; the api-gone sweep tears the
|
||||
// connection down anyway.
|
||||
auto *api_conn = this->proxy_->get_api_connection();
|
||||
if (api_conn == nullptr) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] API connection lost while streaming services", this->connection_index_,
|
||||
this->address_str_);
|
||||
this->send_service_ = DONE_SENDING_SERVICES;
|
||||
this->backend_->release_services();
|
||||
this->park_service_stream_();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -126,7 +126,23 @@ class BluetoothConnection final : public ble_device_base::GattClientListener {
|
||||
this->send_service_for_discovery_();
|
||||
}
|
||||
}
|
||||
/// Park the stream without services-done and free any held table: an
|
||||
/// interrupted stream must never be declared complete (the client's
|
||||
/// timeout arbitrates), and an owed done is dropped with it.
|
||||
void park_service_stream_() {
|
||||
if (this->send_service_ >= 0) {
|
||||
this->backend_->release_services();
|
||||
this->send_service_ = DONE_SENDING_SERVICES;
|
||||
} else if (this->send_service_ == SERVICES_DONE_PENDING) {
|
||||
this->send_service_ = DONE_SENDING_SERVICES;
|
||||
}
|
||||
}
|
||||
void send_service_for_discovery_();
|
||||
/// Send services-done and settle the cursor: DONE when it lands (or no
|
||||
/// subscriber), SERVICES_DONE_PENDING on a refused frame (proxy drain
|
||||
/// retries). Callers release the table first; the message needs only the
|
||||
/// address.
|
||||
void send_services_done_();
|
||||
void reset_connection_(conn_err_t reason);
|
||||
conn_err_t check_connected_op_(const char *action, const char *type) const;
|
||||
void log_gatt_operation_error_(const char *operation, uint16_t handle, int status);
|
||||
@@ -151,10 +167,14 @@ class BluetoothConnection final : public ble_device_base::GattClientListener {
|
||||
static_assert(static_cast<uint8_t>(ClientState::ESTABLISHED) < (1 << 3), "state_ bitfield too narrow");
|
||||
static_assert(static_cast<uint8_t>(ConnectionType::V3_WITHOUT_CACHE) < (1 << 2),
|
||||
"connection_type_ bitfield too narrow");
|
||||
// Ordered so neither byte's fields straddle a storage unit: 3+5 and
|
||||
// 4+2+1+1 fill the two tail bytes exactly.
|
||||
ClientState state_ : 3 {ClientState::IDLE};
|
||||
bool paired_ : 1 {false};
|
||||
ConnectionType connection_type_ : 2 {ConnectionType::V1};
|
||||
static_assert(SERVICES_DONE_RETRY_LIMIT < (1 << 5), "counter bitfield too narrow");
|
||||
uint8_t services_done_retries_ : 5 {0};
|
||||
uint8_t connection_index_ : 4 {0};
|
||||
ConnectionType connection_type_ : 2 {ConnectionType::V1};
|
||||
bool paired_ : 1 {false};
|
||||
bool services_discovered_ : 1 {false};
|
||||
};
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ static_assert(static_cast<uint32_t>(ble_device_base::ScannerState::STOPPED) ==
|
||||
|
||||
bool BluetoothProxy::send_bluetooth_scanner_state_(ble_device_base::ScannerState state) {
|
||||
if (this->api_connection_ == nullptr)
|
||||
return false;
|
||||
return true; // Nobody subscribed: nothing owed
|
||||
api::BluetoothScannerStateResponse resp;
|
||||
resp.state = static_cast<api::enums::BluetoothScannerState>(state);
|
||||
resp.mode = this->hub_->scan_active() ? api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE
|
||||
@@ -51,7 +51,12 @@ bool BluetoothProxy::send_bluetooth_scanner_state_(ble_device_base::ScannerState
|
||||
return this->api_connection_->send_message(resp);
|
||||
}
|
||||
|
||||
#ifndef USE_BLE_SCANNER_STATE_CALLBACK
|
||||
#ifdef USE_BLE_SCANNER_STATE_CALLBACK
|
||||
void BluetoothProxy::send_scanner_state_(ble_device_base::ScannerState state) {
|
||||
// False only on a refused frame, so the latch arms only when a retry is owed.
|
||||
this->scanner_state_pending_ = !this->send_bluetooth_scanner_state_(state);
|
||||
}
|
||||
#else
|
||||
void BluetoothProxy::send_polled_scanner_state_() {
|
||||
// One read feeds both the frame and the change detector; the detector only
|
||||
// advances if the frame was accepted, so a dropped send (WOULD_BLOCK on a
|
||||
@@ -62,7 +67,7 @@ void BluetoothProxy::send_polled_scanner_state_() {
|
||||
this->last_scan_running_ = running;
|
||||
}
|
||||
}
|
||||
#endif // !USE_BLE_SCANNER_STATE_CALLBACK
|
||||
#endif // USE_BLE_SCANNER_STATE_CALLBACK
|
||||
|
||||
void BluetoothProxy::setup() {
|
||||
// BLUETOOTH_PROXY_MAX_CONNECTIONS is 0 on an advertisement-only proxy.
|
||||
@@ -78,7 +83,7 @@ void BluetoothProxy::setup() {
|
||||
#ifdef USE_BLE_SCANNER_STATE_CALLBACK
|
||||
// Only push hubs compile the slot; elsewhere loop() polls scan_running().
|
||||
this->hub_->set_scanner_state_callback({this, [](void *self, ble_device_base::ScannerState state) {
|
||||
static_cast<BluetoothProxy *>(self)->send_bluetooth_scanner_state_(state);
|
||||
static_cast<BluetoothProxy *>(self)->send_scanner_state_(state);
|
||||
}});
|
||||
#endif
|
||||
}
|
||||
@@ -190,8 +195,48 @@ void BluetoothProxy::replace_allocated_slot_(uint64_t find_value, uint64_t set_v
|
||||
ESP_LOGW(TAG, "Connection slot accounting mismatch (find 0x%llx)", (unsigned long long) find_value);
|
||||
}
|
||||
|
||||
void BluetoothProxy::latch_pending_disconnection_(uint64_t address, conn_err_t error) {
|
||||
// Match before free entry so one address never occupies two pool slots.
|
||||
PendingDisconnect *free_entry = nullptr;
|
||||
for (uint8_t i = 0; i < this->connection_count_; i++) {
|
||||
auto &owed = this->pending_disconnections_[i];
|
||||
if (owed.matches(address)) {
|
||||
owed.set(address, error);
|
||||
return;
|
||||
}
|
||||
if (free_entry == nullptr && owed.empty()) {
|
||||
free_entry = &owed;
|
||||
}
|
||||
}
|
||||
if (free_entry != nullptr) {
|
||||
free_entry->set(address, error);
|
||||
return;
|
||||
}
|
||||
// Every entry is owed: evict the first so the newest loss is not silent too.
|
||||
ESP_LOGW(TAG, "Owed disconnect dropped (0x%llx), retry pool full",
|
||||
(unsigned long long) this->pending_disconnections_[0].address());
|
||||
this->pending_disconnections_[0].set(address, error);
|
||||
}
|
||||
|
||||
void BluetoothProxy::clear_pending_disconnection_(uint64_t address) {
|
||||
// A reconnect supersedes the owed disconnect; a late resend would shadow
|
||||
// the new connection.
|
||||
for (uint8_t i = 0; i < this->connection_count_; i++) {
|
||||
if (this->pending_disconnections_[i].matches(address)) {
|
||||
this->pending_disconnections_[i].clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BluetoothProxy::reset_connection_slot_(BluetoothConnection *connection, conn_err_t reason) {
|
||||
this->send_device_connection(connection->get_address(), false, 0, reason);
|
||||
if (!this->send_device_connection(connection->get_address(), false, 0, reason)) {
|
||||
// The client has no other way to learn of an unsolicited disconnect;
|
||||
// latch and let loop()'s paced drain deliver it. V by design: a louder
|
||||
// level would ride the same congested link this reports on.
|
||||
ESP_LOGV(TAG, "[%d] [%s] Disconnect notification deferred, TCP buffer full", connection->get_connection_index(),
|
||||
connection->address_str());
|
||||
this->latch_pending_disconnection_(connection->get_address(), reason);
|
||||
}
|
||||
connection->set_address(0);
|
||||
connection->send_service_ = INIT_SENDING_SERVICES;
|
||||
this->send_connections_free();
|
||||
@@ -206,14 +251,20 @@ BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool rese
|
||||
auto *connection = this->connections_[i];
|
||||
uint64_t conn_addr = connection->get_address();
|
||||
|
||||
if (conn_addr == address)
|
||||
if (conn_addr == address) {
|
||||
// A connect request supersedes an owed disconnect.
|
||||
if (reserve) {
|
||||
this->clear_pending_disconnection_(address);
|
||||
}
|
||||
return connection;
|
||||
}
|
||||
|
||||
if (free_slot == nullptr && conn_addr == 0)
|
||||
free_slot = connection;
|
||||
}
|
||||
if (!reserve || free_slot == nullptr)
|
||||
return nullptr;
|
||||
this->clear_pending_disconnection_(address);
|
||||
free_slot->send_service_ = INIT_SENDING_SERVICES;
|
||||
free_slot->set_address(address);
|
||||
// All connections must start at INIT
|
||||
@@ -387,7 +438,30 @@ void BluetoothProxy::bluetooth_gatt_send_services(const api::BluetoothGATTGetSer
|
||||
}
|
||||
if (!connection->has_gatt_services()) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] No GATT services found", connection->get_connection_index(), connection->address_str());
|
||||
this->send_gatt_services_done(msg.address);
|
||||
// Through the retrying sender: a drop must not leave discovery hanging.
|
||||
// Re-entry does not depend on the cursor - this branch is gated on
|
||||
// has_gatt_services() alone, so no restore is needed.
|
||||
connection->send_services_done_();
|
||||
return;
|
||||
}
|
||||
if (connection->send_service_ > 0) {
|
||||
// A request mid-stream restarts from the top so the requester always
|
||||
// gets the full list. No duplicate risk: the client accumulates batches
|
||||
// per request, and a same-session re-request only happens after the
|
||||
// previous request timed out and discarded its partial list.
|
||||
ESP_LOGD(TAG, "[%d] [%s] GetServices mid-stream, restarting", connection->get_connection_index(),
|
||||
connection->address_str());
|
||||
connection->send_service_ = 0;
|
||||
return;
|
||||
}
|
||||
if (connection->send_service_ == SERVICES_DONE_PENDING) {
|
||||
// A new request supersedes an owed done: the client accumulates batches
|
||||
// per request, so its fresh, empty accumulator plus a bare done would
|
||||
// cache as an empty database. The table is freed; the client's timeout
|
||||
// arbitrates.
|
||||
ESP_LOGW(TAG, "[%d] [%s] GetServices superseded an undelivered done; client timeout will retry",
|
||||
connection->get_connection_index(), connection->address_str());
|
||||
connection->send_service_ = DONE_SENDING_SERVICES;
|
||||
return;
|
||||
}
|
||||
if (connection->send_service_ == INIT_SENDING_SERVICES) // Start sending services if not started yet
|
||||
@@ -515,7 +589,27 @@ void BluetoothProxy::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
#ifndef USE_BLE_SCANNER_STATE_CALLBACK
|
||||
#ifdef BLUETOOTH_CONNECTION_HAS_GATT
|
||||
// Paced retries of owed per-slot notifications; subscriber swaps clear
|
||||
// stale latches before this runs.
|
||||
for (uint8_t i = 0; i < this->connection_count_; i++) {
|
||||
auto *connection = this->connections_[i];
|
||||
if (connection->send_service_ == SERVICES_DONE_PENDING) {
|
||||
connection->send_services_done_();
|
||||
}
|
||||
auto &owed = this->pending_disconnections_[i];
|
||||
if (!owed.empty() && this->send_device_connection(owed.address(), false, 0, owed.error())) {
|
||||
owed.clear();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_BLE_SCANNER_STATE_CALLBACK
|
||||
// Resend a dropped scanner-state push (see scanner_state_pending_).
|
||||
if (this->scanner_state_pending_) {
|
||||
this->send_scanner_state_(this->hub_->get_scanner_state());
|
||||
}
|
||||
#else
|
||||
// This hub doesn't push scanner-state transitions; poll and report on
|
||||
// change. A hub gaining push emits the define and drops this poll.
|
||||
if (this->hub_->scan_running() != this->last_scan_running_) {
|
||||
@@ -601,24 +695,35 @@ void BluetoothProxy::bluetooth_set_connection_params(const api::BluetoothSetConn
|
||||
#endif // !BLUETOOTH_CONNECTION_HAS_GATT
|
||||
|
||||
void BluetoothProxy::subscribe_api_connection(api::APIConnection *api_connection, uint32_t flags) {
|
||||
if (this->api_connection_ != nullptr && this->api_connection_ != api_connection) {
|
||||
// A previous subscriber still holds the slot. This is almost always a stale
|
||||
// connection from a client that dropped without a clean disconnect and has
|
||||
// not yet hit the keepalive timeout; rejecting the new subscriber would
|
||||
// silently starve it of advertisements until it reconnects, so the newest
|
||||
// subscriber wins instead.
|
||||
char old_peername[socket::SOCKADDR_STR_LEN];
|
||||
char new_peername[socket::SOCKADDR_STR_LEN];
|
||||
ESP_LOGW(TAG, "Subscription from %s (%s) replaces %s (%s)", api_connection->get_name(),
|
||||
api_connection->get_peername_to(new_peername), this->api_connection_->get_name(),
|
||||
this->api_connection_->get_peername_to(old_peername));
|
||||
if (api_connection != this->api_connection_) {
|
||||
if (this->api_connection_ != nullptr) {
|
||||
// A previous subscriber still holds the slot. This is almost always a
|
||||
// stale connection from a client that dropped without a clean disconnect
|
||||
// and has not yet hit the keepalive timeout; rejecting the new
|
||||
// subscriber would silently starve it of advertisements until it
|
||||
// reconnects, so the newest subscriber wins instead.
|
||||
char old_peername[socket::SOCKADDR_STR_LEN];
|
||||
char new_peername[socket::SOCKADDR_STR_LEN];
|
||||
ESP_LOGW(TAG, "Subscription from %s (%s) replaces %s (%s)", api_connection->get_name(),
|
||||
api_connection->get_peername_to(new_peername), this->api_connection_->get_name(),
|
||||
this->api_connection_->get_peername_to(old_peername));
|
||||
}
|
||||
// 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
|
||||
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_();
|
||||
}
|
||||
this->pending_disconnections_.fill({});
|
||||
#endif
|
||||
}
|
||||
// A stale retry latch belongs to the previous subscriber's session.
|
||||
this->connections_free_pending_ = false;
|
||||
this->api_connection_ = api_connection;
|
||||
#ifdef USE_BLE_SCANNER_STATE_CALLBACK
|
||||
// get_scanner_state() is part of the push-hub surface (see BLEHubContract).
|
||||
this->send_bluetooth_scanner_state_(this->hub_->get_scanner_state());
|
||||
this->send_scanner_state_(this->hub_->get_scanner_state());
|
||||
#else
|
||||
this->send_polled_scanner_state_();
|
||||
#endif
|
||||
@@ -631,20 +736,11 @@ void BluetoothProxy::unsubscribe_api_connection(api::APIConnection *api_connecti
|
||||
}
|
||||
this->api_connection_ = nullptr;
|
||||
this->connections_free_pending_ = false;
|
||||
#ifdef USE_BLE_SCANNER_STATE_CALLBACK
|
||||
this->scanner_state_pending_ = false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void BluetoothProxy::send_device_connection(uint64_t address, bool connected, uint16_t mtu, conn_err_t error) {
|
||||
if (this->api_connection_ == nullptr)
|
||||
return;
|
||||
api::BluetoothDeviceConnectionResponse call;
|
||||
call.address = address;
|
||||
call.connected = connected;
|
||||
call.mtu = mtu;
|
||||
call.error = error;
|
||||
// Fire and forget: a drop is covered by the client's own timeouts and the
|
||||
// retried connections-free state.
|
||||
this->api_connection_->send_message(call);
|
||||
}
|
||||
void BluetoothProxy::send_connections_free() {
|
||||
if (this->api_connection_ != nullptr) {
|
||||
this->send_connections_free(this->api_connection_);
|
||||
@@ -661,12 +757,23 @@ void BluetoothProxy::send_connections_free(api::APIConnection *api_connection) {
|
||||
}
|
||||
}
|
||||
|
||||
void BluetoothProxy::send_gatt_services_done(uint64_t address) {
|
||||
bool BluetoothProxy::send_device_connection(uint64_t address, bool connected, uint16_t mtu, conn_err_t error) {
|
||||
if (this->api_connection_ == nullptr)
|
||||
return;
|
||||
return true; // Nobody subscribed: nothing owed
|
||||
api::BluetoothDeviceConnectionResponse call;
|
||||
call.address = address;
|
||||
call.connected = connected;
|
||||
call.mtu = mtu;
|
||||
call.error = error;
|
||||
return this->api_connection_->send_message(call);
|
||||
}
|
||||
|
||||
bool BluetoothProxy::send_gatt_services_done(uint64_t address) {
|
||||
if (this->api_connection_ == nullptr)
|
||||
return true; // Nobody subscribed: nothing is owed, only a refused frame reports false
|
||||
api::BluetoothGATTGetServicesDoneResponse call;
|
||||
call.address = address;
|
||||
this->api_connection_->send_message(call);
|
||||
return this->api_connection_->send_message(call);
|
||||
}
|
||||
|
||||
void BluetoothProxy::send_gatt_error(uint64_t address, uint16_t handle, conn_err_t error) {
|
||||
|
||||
@@ -24,7 +24,9 @@ namespace esphome::bluetooth_proxy {
|
||||
using bluetooth_connection::CONN_OK;
|
||||
using bluetooth_connection::conn_err_t;
|
||||
using bluetooth_connection::GATT_NOT_CONNECTED;
|
||||
using bluetooth_connection::DONE_SENDING_SERVICES;
|
||||
using bluetooth_connection::INIT_SENDING_SERVICES;
|
||||
using bluetooth_connection::SERVICES_DONE_PENDING;
|
||||
|
||||
#ifdef BLUETOOTH_CONNECTION_HAS_GATT
|
||||
using BluetoothConnection = bluetooth_connection::BluetoothConnection;
|
||||
@@ -57,6 +59,43 @@ enum BluetoothProxySubscriptionFlag : uint32_t {
|
||||
SUBSCRIPTION_RAW_ADVERTISEMENTS = 1 << 0,
|
||||
};
|
||||
|
||||
#ifdef BLUETOOTH_CONNECTION_HAS_GATT
|
||||
/// One owed freed-slot connected=false notification in a single word: the
|
||||
/// 48-bit address in the low bits, the sign-extending 16-bit reason on top.
|
||||
/// Every reason that reaches the pool (esp_gatt_status_t,
|
||||
/// esp_gatt_conn_reason_t, generic ESP_ERR_*, -1) fits int16_t.
|
||||
class PendingDisconnect {
|
||||
public:
|
||||
constexpr void set(uint64_t address, conn_err_t error) {
|
||||
// Mask: the address originates from the client, and a stray high bit
|
||||
// must not corrupt the reason.
|
||||
this->word_ = (address & ADDRESS_MASK) | (static_cast<uint64_t>(static_cast<uint16_t>(error)) << 48);
|
||||
}
|
||||
constexpr void clear() { this->word_ = 0; }
|
||||
// Whole-word test: set() is only ever given a live (nonzero) address.
|
||||
constexpr bool empty() const { return this->word_ == 0; }
|
||||
// Masked like set(), so a stray high bit cannot defeat the pool lookups.
|
||||
constexpr bool matches(uint64_t address) const { return this->address() == (address & ADDRESS_MASK); }
|
||||
constexpr uint64_t address() const { return this->word_ & ADDRESS_MASK; }
|
||||
constexpr conn_err_t error() const { return static_cast<int16_t>(this->word_ >> 48); }
|
||||
|
||||
private:
|
||||
static constexpr uint64_t ADDRESS_MASK = 0x0000FFFFFFFFFFFFULL;
|
||||
uint64_t word_{0};
|
||||
};
|
||||
// Pin the packing at compile time: mask and sign round-trip for every
|
||||
// reachable shape (negative, GATT status, ESP_ERR_* range, stray high bit).
|
||||
constexpr bool pending_disconnect_round_trips(uint64_t address, uint64_t expected_address, conn_err_t error) {
|
||||
PendingDisconnect p;
|
||||
p.set(address, error);
|
||||
return p.address() == expected_address && p.error() == error && !p.empty() && p.matches(address);
|
||||
}
|
||||
static_assert(pending_disconnect_round_trips(0x0000112233445566ULL, 0x0000112233445566ULL, -1));
|
||||
static_assert(pending_disconnect_round_trips(0x0000FFFFFFFFFFFFULL, 0x0000FFFFFFFFFFFFULL, 0x8F));
|
||||
static_assert(pending_disconnect_round_trips(0xABCD112233445566ULL, 0x0000112233445566ULL, 0x110));
|
||||
static_assert(PendingDisconnect{}.empty());
|
||||
#endif
|
||||
|
||||
class BluetoothProxy final : public Component {
|
||||
#ifdef BLUETOOTH_CONNECTION_HAS_GATT
|
||||
// Allow the connection to update connections_free_response_
|
||||
@@ -97,10 +136,14 @@ class BluetoothProxy final : public Component {
|
||||
return this->api_connection_ != nullptr && this->api_connection_->client_supports_api_version(1, 12);
|
||||
}
|
||||
|
||||
void send_device_connection(uint64_t address, bool connected, uint16_t mtu = 0, conn_err_t error = CONN_OK);
|
||||
/// False only when a subscriber refused the frame; true = delivered or
|
||||
/// nobody subscribed. Request-answer callers ignore the result (client
|
||||
/// timeouts cover those); only reset_connection_slot_ latches for retry.
|
||||
bool send_device_connection(uint64_t address, bool connected, uint16_t mtu = 0, conn_err_t error = CONN_OK);
|
||||
void send_connections_free();
|
||||
void send_connections_free(api::APIConnection *api_connection);
|
||||
void send_gatt_services_done(uint64_t address);
|
||||
/// Same convention as send_device_connection: false only on a refused frame.
|
||||
bool send_gatt_services_done(uint64_t address);
|
||||
void send_gatt_error(uint64_t address, uint16_t handle, conn_err_t error);
|
||||
void send_device_pairing(uint64_t address, bool paired, conn_err_t error = CONN_OK);
|
||||
void send_device_unpairing(uint64_t address, bool success, conn_err_t error = CONN_OK);
|
||||
@@ -172,7 +215,9 @@ class BluetoothProxy final : public Component {
|
||||
|
||||
protected:
|
||||
bool send_bluetooth_scanner_state_(ble_device_base::ScannerState state);
|
||||
#ifndef USE_BLE_SCANNER_STATE_CALLBACK
|
||||
#ifdef USE_BLE_SCANNER_STATE_CALLBACK
|
||||
void send_scanner_state_(ble_device_base::ScannerState state);
|
||||
#else
|
||||
void send_polled_scanner_state_();
|
||||
#endif
|
||||
void on_raw_advertisement_(const ble_device_base::RawAdvertisement &raw);
|
||||
@@ -231,6 +276,10 @@ class BluetoothProxy final : public Component {
|
||||
/// a 30-second timeout (DEFAULT_BLE_TIMEOUT) to detect incomplete service
|
||||
/// discovery and retry, rather than being told a partial list is complete.
|
||||
void reset_connection_slot_(BluetoothConnection *connection, conn_err_t reason);
|
||||
/// Drop any owed freed-slot notification for this address (client reconnected).
|
||||
void clear_pending_disconnection_(uint64_t address);
|
||||
/// Pool a refused freed-slot notification for the paced drain.
|
||||
void latch_pending_disconnection_(uint64_t address, conn_err_t error);
|
||||
#endif
|
||||
|
||||
// Memory optimized layout for 32-bit systems
|
||||
@@ -240,6 +289,10 @@ class BluetoothProxy final : public Component {
|
||||
#ifdef BLUETOOTH_CONNECTION_HAS_GATT
|
||||
// Group 2: Fixed-size array of connection pointers
|
||||
std::array<BluetoothConnection *, BLUETOOTH_PROXY_MAX_CONNECTIONS> connections_{};
|
||||
// Address-keyed pool of owed freed-slot notifications; loop() resends.
|
||||
// Proxy-only state, kept off BluetoothConnection; entries are not tied to
|
||||
// slot indices.
|
||||
std::array<PendingDisconnect, BLUETOOTH_PROXY_MAX_CONNECTIONS> pending_disconnections_{};
|
||||
#endif
|
||||
ble_device_base::BLEHub *hub_{nullptr};
|
||||
// Group 3: 4-byte types; paired with hub_ so the 8-aligned messages below
|
||||
@@ -260,7 +313,11 @@ class BluetoothProxy final : public Component {
|
||||
bool connections_free_pending_{false};
|
||||
uint8_t connection_count_{0};
|
||||
bool configured_scan_active_{false}; // Configured scan mode from YAML
|
||||
#ifndef USE_BLE_SCANNER_STATE_CALLBACK
|
||||
#ifdef USE_BLE_SCANNER_STATE_CALLBACK
|
||||
// A dropped push (full TX buffer) is re-queried from the hub and resent
|
||||
// from loop(); the hub's current state is idempotent by construction.
|
||||
bool scanner_state_pending_{false};
|
||||
#else
|
||||
bool last_scan_running_{false}; // Last scanner state reported to the subscriber
|
||||
#endif
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user