mirror of
https://github.com/esphome/esphome.git
synced 2025-09-06 05:12:21 +01:00
518 lines
20 KiB
C++
518 lines
20 KiB
C++
#include "bluetooth_proxy.h"
|
||
|
||
#include "esphome/core/log.h"
|
||
#include "esphome/core/macros.h"
|
||
#include "esphome/core/application.h"
|
||
#include <cstring>
|
||
|
||
#ifdef USE_ESP32
|
||
|
||
namespace esphome::bluetooth_proxy {
|
||
|
||
static const char *const TAG = "bluetooth_proxy";
|
||
|
||
// Batch size for BLE advertisements to maximize WiFi efficiency
|
||
// Each advertisement is up to 80 bytes when packaged (including protocol overhead)
|
||
// Most advertisements are 20-30 bytes, allowing even more to fit per packet
|
||
// 16 advertisements × 80 bytes (worst case) = 1280 bytes out of ~1320 bytes usable payload
|
||
// This achieves ~97% WiFi MTU utilization while staying under the limit
|
||
static constexpr size_t FLUSH_BATCH_SIZE = 16;
|
||
|
||
// Verify BLE advertisement data array size matches the BLE specification (31 bytes adv + 31 bytes scan response)
|
||
static_assert(sizeof(((api::BluetoothLERawAdvertisement *) nullptr)->data) == 62,
|
||
"BLE advertisement data array size mismatch");
|
||
|
||
BluetoothProxy::BluetoothProxy() { global_bluetooth_proxy = this; }
|
||
|
||
void BluetoothProxy::setup() {
|
||
// Pre-allocate response object
|
||
this->response_ = std::make_unique<api::BluetoothLERawAdvertisementsResponse>();
|
||
|
||
// Reserve capacity but start with size 0
|
||
// Reserve 50% since we'll grow naturally and flush at FLUSH_BATCH_SIZE
|
||
this->response_->advertisements.reserve(FLUSH_BATCH_SIZE / 2);
|
||
|
||
// Don't pre-allocate pool - let it grow only if needed in busy environments
|
||
// Many devices in quiet areas will never need the overflow pool
|
||
|
||
this->connections_free_response_.limit = this->connections_.size();
|
||
this->connections_free_response_.free = this->connections_.size();
|
||
|
||
this->parent_->add_scanner_state_callback([this](esp32_ble_tracker::ScannerState state) {
|
||
if (this->api_connection_ != nullptr) {
|
||
this->send_bluetooth_scanner_state_(state);
|
||
}
|
||
});
|
||
}
|
||
|
||
void BluetoothProxy::send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerState state) {
|
||
api::BluetoothScannerStateResponse resp;
|
||
resp.state = static_cast<api::enums::BluetoothScannerState>(state);
|
||
resp.mode = this->parent_->get_scan_active() ? api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE
|
||
: api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_PASSIVE;
|
||
this->api_connection_->send_message(resp, api::BluetoothScannerStateResponse::MESSAGE_TYPE);
|
||
}
|
||
|
||
#ifdef USE_ESP32_BLE_DEVICE
|
||
bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
|
||
// This method should never be called since bluetooth_proxy always uses raw advertisements
|
||
// but we need to provide an implementation to satisfy the virtual method requirement
|
||
return false;
|
||
}
|
||
#endif
|
||
|
||
bool BluetoothProxy::parse_devices(const esp32_ble::BLEScanResult *scan_results, size_t count) {
|
||
if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr)
|
||
return false;
|
||
|
||
auto &advertisements = this->response_->advertisements;
|
||
|
||
for (size_t i = 0; i < count; i++) {
|
||
auto &result = scan_results[i];
|
||
uint8_t length = result.adv_data_len + result.scan_rsp_len;
|
||
|
||
// Check if we need to expand the vector
|
||
if (this->advertisement_count_ >= advertisements.size()) {
|
||
if (this->advertisement_pool_.empty()) {
|
||
// No room in pool, need to allocate
|
||
advertisements.emplace_back();
|
||
} else {
|
||
// Pull from pool
|
||
advertisements.push_back(std::move(this->advertisement_pool_.back()));
|
||
this->advertisement_pool_.pop_back();
|
||
}
|
||
}
|
||
|
||
// Fill in the data directly at current position
|
||
auto &adv = advertisements[this->advertisement_count_];
|
||
adv.address = esp32_ble::ble_addr_to_uint64(result.bda);
|
||
adv.rssi = result.rssi;
|
||
adv.address_type = result.ble_addr_type;
|
||
adv.data_len = length;
|
||
std::memcpy(adv.data, result.ble_adv, length);
|
||
|
||
this->advertisement_count_++;
|
||
|
||
ESP_LOGV(TAG, "Queuing raw packet from %02X:%02X:%02X:%02X:%02X:%02X, length %d. RSSI: %d dB", result.bda[0],
|
||
result.bda[1], result.bda[2], result.bda[3], result.bda[4], result.bda[5], length, result.rssi);
|
||
|
||
// Flush if we have reached FLUSH_BATCH_SIZE
|
||
if (this->advertisement_count_ >= FLUSH_BATCH_SIZE) {
|
||
this->flush_pending_advertisements();
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
void BluetoothProxy::flush_pending_advertisements() {
|
||
if (this->advertisement_count_ == 0 || !api::global_api_server->is_connected() || this->api_connection_ == nullptr)
|
||
return;
|
||
|
||
auto &advertisements = this->response_->advertisements;
|
||
|
||
// Return any items beyond advertisement_count_ to the pool
|
||
if (advertisements.size() > this->advertisement_count_) {
|
||
// Move unused items back to pool
|
||
this->advertisement_pool_.insert(this->advertisement_pool_.end(),
|
||
std::make_move_iterator(advertisements.begin() + this->advertisement_count_),
|
||
std::make_move_iterator(advertisements.end()));
|
||
|
||
// Resize to actual count
|
||
advertisements.resize(this->advertisement_count_);
|
||
}
|
||
|
||
// Send the message
|
||
this->api_connection_->send_message(*this->response_, api::BluetoothLERawAdvertisementsResponse::MESSAGE_TYPE);
|
||
|
||
// Reset count - existing items will be overwritten in next batch
|
||
this->advertisement_count_ = 0;
|
||
}
|
||
|
||
void BluetoothProxy::dump_config() {
|
||
ESP_LOGCONFIG(TAG, "Bluetooth Proxy:");
|
||
ESP_LOGCONFIG(TAG,
|
||
" Active: %s\n"
|
||
" Connections: %d",
|
||
YESNO(this->active_), this->connections_.size());
|
||
}
|
||
|
||
void BluetoothProxy::loop() {
|
||
if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr) {
|
||
for (auto *connection : this->connections_) {
|
||
if (connection->get_address() != 0 && !connection->disconnect_pending()) {
|
||
connection->disconnect();
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Flush any pending BLE advertisements that have been accumulated but not yet sent
|
||
uint32_t now = App.get_loop_component_start_time();
|
||
|
||
// Flush accumulated advertisements every 100ms
|
||
if (now - this->last_advertisement_flush_time_ >= 100) {
|
||
this->flush_pending_advertisements();
|
||
this->last_advertisement_flush_time_ = now;
|
||
}
|
||
}
|
||
|
||
esp32_ble_tracker::AdvertisementParserType BluetoothProxy::get_advertisement_parser_type() {
|
||
return esp32_ble_tracker::AdvertisementParserType::RAW_ADVERTISEMENTS;
|
||
}
|
||
|
||
BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool reserve) {
|
||
for (auto *connection : this->connections_) {
|
||
if (connection->get_address() == address)
|
||
return connection;
|
||
}
|
||
|
||
if (!reserve)
|
||
return nullptr;
|
||
|
||
for (auto *connection : this->connections_) {
|
||
if (connection->get_address() == 0) {
|
||
connection->send_service_ = DONE_SENDING_SERVICES;
|
||
connection->set_address(address);
|
||
// All connections must start at INIT
|
||
// We only set the state if we allocate the connection
|
||
// to avoid a race where multiple connection attempts
|
||
// are made.
|
||
connection->set_state(espbt::ClientState::INIT);
|
||
return connection;
|
||
}
|
||
}
|
||
|
||
return nullptr;
|
||
}
|
||
|
||
void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest &msg) {
|
||
switch (msg.request_type) {
|
||
case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITH_CACHE:
|
||
case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE:
|
||
case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT: {
|
||
auto *connection = this->get_connection_(msg.address, true);
|
||
if (connection == nullptr) {
|
||
ESP_LOGW(TAG, "No free connections available");
|
||
this->send_device_connection(msg.address, false);
|
||
return;
|
||
}
|
||
if (connection->state() == espbt::ClientState::CONNECTED ||
|
||
connection->state() == espbt::ClientState::ESTABLISHED) {
|
||
ESP_LOGW(TAG, "[%d] [%s] Connection already established", connection->get_connection_index(),
|
||
connection->address_str().c_str());
|
||
this->send_device_connection(msg.address, true);
|
||
this->send_connections_free();
|
||
return;
|
||
} else if (connection->state() == espbt::ClientState::SEARCHING) {
|
||
ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, already searching for device",
|
||
connection->get_connection_index(), connection->address_str().c_str());
|
||
return;
|
||
} else if (connection->state() == espbt::ClientState::DISCOVERED) {
|
||
ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, device already discovered",
|
||
connection->get_connection_index(), connection->address_str().c_str());
|
||
return;
|
||
} else if (connection->state() == espbt::ClientState::READY_TO_CONNECT) {
|
||
ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, waiting in line to connect",
|
||
connection->get_connection_index(), connection->address_str().c_str());
|
||
return;
|
||
} else if (connection->state() == espbt::ClientState::CONNECTING) {
|
||
if (connection->disconnect_pending()) {
|
||
ESP_LOGW(TAG, "[%d] [%s] Connection request while pending disconnect, cancelling pending disconnect",
|
||
connection->get_connection_index(), connection->address_str().c_str());
|
||
connection->cancel_pending_disconnect();
|
||
return;
|
||
}
|
||
ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, already connecting", connection->get_connection_index(),
|
||
connection->address_str().c_str());
|
||
return;
|
||
} else if (connection->state() == espbt::ClientState::DISCONNECTING) {
|
||
ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, device is disconnecting",
|
||
connection->get_connection_index(), connection->address_str().c_str());
|
||
return;
|
||
} else if (connection->state() != espbt::ClientState::INIT) {
|
||
ESP_LOGW(TAG, "[%d] [%s] Connection already in progress", connection->get_connection_index(),
|
||
connection->address_str().c_str());
|
||
return;
|
||
}
|
||
if (msg.request_type == api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITH_CACHE) {
|
||
connection->set_connection_type(espbt::ConnectionType::V3_WITH_CACHE);
|
||
ESP_LOGI(TAG, "[%d] [%s] Connecting v3 with cache", connection->get_connection_index(),
|
||
connection->address_str().c_str());
|
||
} else if (msg.request_type == api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE) {
|
||
connection->set_connection_type(espbt::ConnectionType::V3_WITHOUT_CACHE);
|
||
ESP_LOGI(TAG, "[%d] [%s] Connecting v3 without cache", connection->get_connection_index(),
|
||
connection->address_str().c_str());
|
||
} else {
|
||
connection->set_connection_type(espbt::ConnectionType::V1);
|
||
ESP_LOGI(TAG, "[%d] [%s] Connecting v1", connection->get_connection_index(), connection->address_str().c_str());
|
||
}
|
||
if (msg.has_address_type) {
|
||
uint64_to_bd_addr(msg.address, connection->remote_bda_);
|
||
connection->set_remote_addr_type(static_cast<esp_ble_addr_type_t>(msg.address_type));
|
||
connection->set_state(espbt::ClientState::DISCOVERED);
|
||
} else {
|
||
connection->set_state(espbt::ClientState::SEARCHING);
|
||
}
|
||
this->send_connections_free();
|
||
break;
|
||
}
|
||
case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT: {
|
||
auto *connection = this->get_connection_(msg.address, false);
|
||
if (connection == nullptr) {
|
||
this->send_device_connection(msg.address, false);
|
||
this->send_connections_free();
|
||
return;
|
||
}
|
||
if (connection->state() != espbt::ClientState::IDLE) {
|
||
connection->disconnect();
|
||
} else {
|
||
// Manual disconnect for idle connection
|
||
connection->set_address(0);
|
||
this->send_device_connection(msg.address, false);
|
||
this->send_connections_free();
|
||
}
|
||
break;
|
||
}
|
||
case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_PAIR: {
|
||
auto *connection = this->get_connection_(msg.address, false);
|
||
if (connection != nullptr) {
|
||
if (!connection->is_paired()) {
|
||
auto err = connection->pair();
|
||
if (err != ESP_OK) {
|
||
this->send_device_pairing(msg.address, false, err);
|
||
}
|
||
} else {
|
||
this->send_device_pairing(msg.address, true);
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR: {
|
||
esp_bd_addr_t address;
|
||
uint64_to_bd_addr(msg.address, address);
|
||
esp_err_t ret = esp_ble_remove_bond_device(address);
|
||
this->send_device_pairing(msg.address, ret == ESP_OK, ret);
|
||
break;
|
||
}
|
||
case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE: {
|
||
esp_bd_addr_t address;
|
||
uint64_to_bd_addr(msg.address, address);
|
||
esp_err_t ret = esp_ble_gattc_cache_clean(address);
|
||
api::BluetoothDeviceClearCacheResponse call;
|
||
call.address = msg.address;
|
||
call.success = ret == ESP_OK;
|
||
call.error = ret;
|
||
|
||
this->api_connection_->send_message(call, api::BluetoothDeviceClearCacheResponse::MESSAGE_TYPE);
|
||
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
void BluetoothProxy::bluetooth_gatt_read(const api::BluetoothGATTReadRequest &msg) {
|
||
auto *connection = this->get_connection_(msg.address, false);
|
||
if (connection == nullptr) {
|
||
ESP_LOGW(TAG, "Cannot read GATT characteristic, not connected");
|
||
this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
|
||
return;
|
||
}
|
||
|
||
auto err = connection->read_characteristic(msg.handle);
|
||
if (err != ESP_OK) {
|
||
this->send_gatt_error(msg.address, msg.handle, err);
|
||
}
|
||
}
|
||
|
||
void BluetoothProxy::bluetooth_gatt_write(const api::BluetoothGATTWriteRequest &msg) {
|
||
auto *connection = this->get_connection_(msg.address, false);
|
||
if (connection == nullptr) {
|
||
ESP_LOGW(TAG, "Cannot write GATT characteristic, not connected");
|
||
this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
|
||
return;
|
||
}
|
||
|
||
auto err = connection->write_characteristic(msg.handle, msg.data, msg.response);
|
||
if (err != ESP_OK) {
|
||
this->send_gatt_error(msg.address, msg.handle, err);
|
||
}
|
||
}
|
||
|
||
void BluetoothProxy::bluetooth_gatt_read_descriptor(const api::BluetoothGATTReadDescriptorRequest &msg) {
|
||
auto *connection = this->get_connection_(msg.address, false);
|
||
if (connection == nullptr) {
|
||
ESP_LOGW(TAG, "Cannot read GATT descriptor, not connected");
|
||
this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
|
||
return;
|
||
}
|
||
|
||
auto err = connection->read_descriptor(msg.handle);
|
||
if (err != ESP_OK) {
|
||
this->send_gatt_error(msg.address, msg.handle, err);
|
||
}
|
||
}
|
||
|
||
void BluetoothProxy::bluetooth_gatt_write_descriptor(const api::BluetoothGATTWriteDescriptorRequest &msg) {
|
||
auto *connection = this->get_connection_(msg.address, false);
|
||
if (connection == nullptr) {
|
||
ESP_LOGW(TAG, "Cannot write GATT descriptor, not connected");
|
||
this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
|
||
return;
|
||
}
|
||
|
||
auto err = connection->write_descriptor(msg.handle, msg.data, true);
|
||
if (err != ESP_OK) {
|
||
this->send_gatt_error(msg.address, msg.handle, err);
|
||
}
|
||
}
|
||
|
||
void BluetoothProxy::bluetooth_gatt_send_services(const api::BluetoothGATTGetServicesRequest &msg) {
|
||
auto *connection = this->get_connection_(msg.address, false);
|
||
if (connection == nullptr || !connection->connected()) {
|
||
ESP_LOGW(TAG, "Cannot get GATT services, not connected");
|
||
this->send_gatt_error(msg.address, 0, ESP_GATT_NOT_CONNECTED);
|
||
return;
|
||
}
|
||
if (!connection->service_count_) {
|
||
ESP_LOGW(TAG, "[%d] [%s] No GATT services found", connection->connection_index_, connection->address_str().c_str());
|
||
this->send_gatt_services_done(msg.address);
|
||
return;
|
||
}
|
||
if (connection->send_service_ ==
|
||
DONE_SENDING_SERVICES) // Only start sending services if we're not already sending them
|
||
connection->send_service_ = 0;
|
||
}
|
||
|
||
void BluetoothProxy::bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest &msg) {
|
||
auto *connection = this->get_connection_(msg.address, false);
|
||
if (connection == nullptr) {
|
||
ESP_LOGW(TAG, "Cannot notify GATT characteristic, not connected");
|
||
this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
|
||
return;
|
||
}
|
||
|
||
auto err = connection->notify_characteristic(msg.handle, msg.enable);
|
||
if (err != ESP_OK) {
|
||
this->send_gatt_error(msg.address, msg.handle, err);
|
||
}
|
||
}
|
||
|
||
void BluetoothProxy::subscribe_api_connection(api::APIConnection *api_connection, uint32_t flags) {
|
||
if (this->api_connection_ != nullptr) {
|
||
ESP_LOGE(TAG, "Only one API subscription is allowed at a time");
|
||
return;
|
||
}
|
||
this->api_connection_ = api_connection;
|
||
this->parent_->recalculate_advertisement_parser_types();
|
||
|
||
this->send_bluetooth_scanner_state_(this->parent_->get_scanner_state());
|
||
}
|
||
|
||
void BluetoothProxy::unsubscribe_api_connection(api::APIConnection *api_connection) {
|
||
if (this->api_connection_ != api_connection) {
|
||
ESP_LOGV(TAG, "API connection is not subscribed");
|
||
return;
|
||
}
|
||
this->api_connection_ = nullptr;
|
||
this->parent_->recalculate_advertisement_parser_types();
|
||
}
|
||
|
||
void BluetoothProxy::send_device_connection(uint64_t address, bool connected, uint16_t mtu, esp_err_t error) {
|
||
if (this->api_connection_ == nullptr)
|
||
return;
|
||
api::BluetoothDeviceConnectionResponse call;
|
||
call.address = address;
|
||
call.connected = connected;
|
||
call.mtu = mtu;
|
||
call.error = error;
|
||
this->api_connection_->send_message(call, api::BluetoothDeviceConnectionResponse::MESSAGE_TYPE);
|
||
}
|
||
void BluetoothProxy::send_connections_free() {
|
||
if (this->api_connection_ == nullptr)
|
||
return;
|
||
|
||
this->api_connection_->send_message(this->connections_free_response_,
|
||
api::BluetoothConnectionsFreeResponse::MESSAGE_TYPE);
|
||
}
|
||
|
||
void BluetoothProxy::send_gatt_services_done(uint64_t address) {
|
||
if (this->api_connection_ == nullptr)
|
||
return;
|
||
api::BluetoothGATTGetServicesDoneResponse call;
|
||
call.address = address;
|
||
this->api_connection_->send_message(call, api::BluetoothGATTGetServicesDoneResponse::MESSAGE_TYPE);
|
||
}
|
||
|
||
void BluetoothProxy::send_gatt_error(uint64_t address, uint16_t handle, esp_err_t error) {
|
||
if (this->api_connection_ == nullptr)
|
||
return;
|
||
api::BluetoothGATTErrorResponse call;
|
||
call.address = address;
|
||
call.handle = handle;
|
||
call.error = error;
|
||
this->api_connection_->send_message(call, api::BluetoothGATTWriteResponse::MESSAGE_TYPE);
|
||
}
|
||
|
||
void BluetoothProxy::send_device_pairing(uint64_t address, bool paired, esp_err_t error) {
|
||
api::BluetoothDevicePairingResponse call;
|
||
call.address = address;
|
||
call.paired = paired;
|
||
call.error = error;
|
||
|
||
this->api_connection_->send_message(call, api::BluetoothDevicePairingResponse::MESSAGE_TYPE);
|
||
}
|
||
|
||
void BluetoothProxy::send_device_unpairing(uint64_t address, bool success, esp_err_t error) {
|
||
api::BluetoothDeviceUnpairingResponse call;
|
||
call.address = address;
|
||
call.success = success;
|
||
call.error = error;
|
||
|
||
this->api_connection_->send_message(call, api::BluetoothDeviceUnpairingResponse::MESSAGE_TYPE);
|
||
}
|
||
|
||
void BluetoothProxy::bluetooth_scanner_set_mode(bool active) {
|
||
if (this->parent_->get_scan_active() == active) {
|
||
return;
|
||
}
|
||
ESP_LOGD(TAG, "Setting scanner mode to %s", active ? "active" : "passive");
|
||
this->parent_->set_scan_active(active);
|
||
this->parent_->stop_scan();
|
||
this->parent_->set_scan_continuous(
|
||
true); // Set this to true to automatically start scanning again when it has cleaned up.
|
||
}
|
||
|
||
void BluetoothProxy::allocate_connection_(BluetoothConnection *connection, uint64_t address) {
|
||
// Update pre-allocated message directly
|
||
this->connections_free_response_.free--;
|
||
|
||
// Find first zero slot and set it
|
||
auto it = std::find(this->connections_free_response_.allocated.begin(),
|
||
this->connections_free_response_.allocated.end(), 0);
|
||
if (it != this->connections_free_response_.allocated.end()) {
|
||
*it = address;
|
||
}
|
||
}
|
||
|
||
void BluetoothProxy::free_connection_(uint64_t address) {
|
||
if (address == 0)
|
||
return; // Safety check
|
||
|
||
// Update pre-allocated message directly
|
||
this->connections_free_response_.free++;
|
||
|
||
// Find the address and set to 0
|
||
auto it = std::find(this->connections_free_response_.allocated.begin(),
|
||
this->connections_free_response_.allocated.end(), address);
|
||
if (it != this->connections_free_response_.allocated.end()) {
|
||
*it = 0;
|
||
}
|
||
}
|
||
|
||
BluetoothProxy *global_bluetooth_proxy = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||
|
||
} // namespace esphome::bluetooth_proxy
|
||
|
||
#endif // USE_ESP32
|