mirror of
https://github.com/esphome/esphome.git
synced 2025-10-06 03:43:49 +01:00
merge
This commit is contained in:
@@ -116,7 +116,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA),
|
||||
esp32_ble_tracker.consume_connection_slots(1, "ble_client"),
|
||||
esp32_ble.consume_connection_slots(1, "ble_client"),
|
||||
)
|
||||
|
||||
CONF_BLE_CLIENT_ID = "ble_client_id"
|
||||
|
@@ -42,9 +42,7 @@ def validate_connections(config):
|
||||
)
|
||||
elif config[CONF_ACTIVE]:
|
||||
connection_slots: int = config[CONF_CONNECTION_SLOTS]
|
||||
esp32_ble_tracker.consume_connection_slots(connection_slots, "bluetooth_proxy")(
|
||||
config
|
||||
)
|
||||
esp32_ble.consume_connection_slots(connection_slots, "bluetooth_proxy")(config)
|
||||
|
||||
return {
|
||||
**config,
|
||||
@@ -65,11 +63,11 @@ CONFIG_SCHEMA = cv.All(
|
||||
default=DEFAULT_CONNECTION_SLOTS,
|
||||
): cv.All(
|
||||
cv.positive_int,
|
||||
cv.Range(min=1, max=esp32_ble_tracker.IDF_MAX_CONNECTIONS),
|
||||
cv.Range(min=1, max=esp32_ble.IDF_MAX_CONNECTIONS),
|
||||
),
|
||||
cv.Optional(CONF_CONNECTIONS): cv.All(
|
||||
cv.ensure_list(CONNECTION_SCHEMA),
|
||||
cv.Length(min=1, max=esp32_ble_tracker.IDF_MAX_CONNECTIONS),
|
||||
cv.Length(min=1, max=esp32_ble.IDF_MAX_CONNECTIONS),
|
||||
),
|
||||
}
|
||||
)
|
||||
|
@@ -1,5 +1,8 @@
|
||||
from collections.abc import Callable, MutableMapping
|
||||
from enum import Enum
|
||||
import logging
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
@@ -9,16 +12,19 @@ from esphome.const import (
|
||||
CONF_ENABLE_ON_BOOT,
|
||||
CONF_ESPHOME,
|
||||
CONF_ID,
|
||||
CONF_MAX_CONNECTIONS,
|
||||
CONF_NAME,
|
||||
CONF_NAME_ADD_MAC_SUFFIX,
|
||||
)
|
||||
from esphome.core import TimePeriod
|
||||
from esphome.core import CORE, TimePeriod
|
||||
import esphome.final_validate as fv
|
||||
|
||||
DEPENDENCIES = ["esp32"]
|
||||
CODEOWNERS = ["@jesserockz", "@Rapsssito", "@bdraco"]
|
||||
DOMAIN = "esp32_ble"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BTLoggers(Enum):
|
||||
"""Bluetooth logger categories available in ESP-IDF.
|
||||
@@ -127,6 +133,28 @@ CONF_DISABLE_BT_LOGS = "disable_bt_logs"
|
||||
CONF_CONNECTION_TIMEOUT = "connection_timeout"
|
||||
CONF_MAX_NOTIFICATIONS = "max_notifications"
|
||||
|
||||
# BLE connection limits
|
||||
# ESP-IDF CONFIG_BT_ACL_CONNECTIONS has range 1-9, default 4
|
||||
# Total instances: 10 (ADV + SCAN + connections)
|
||||
# - ADV only: up to 9 connections
|
||||
# - SCAN only: up to 9 connections
|
||||
# - ADV + SCAN: up to 8 connections
|
||||
DEFAULT_MAX_CONNECTIONS = 3
|
||||
IDF_MAX_CONNECTIONS = 9
|
||||
|
||||
# Connection slot tracking keys
|
||||
KEY_ESP32_BLE = "esp32_ble"
|
||||
KEY_USED_CONNECTION_SLOTS = "used_connection_slots"
|
||||
|
||||
# Export for use by other components (bluetooth_proxy, etc.)
|
||||
__all__ = [
|
||||
"DEFAULT_MAX_CONNECTIONS",
|
||||
"IDF_MAX_CONNECTIONS",
|
||||
"KEY_ESP32_BLE",
|
||||
"KEY_USED_CONNECTION_SLOTS",
|
||||
"consume_connection_slots",
|
||||
]
|
||||
|
||||
NO_BLUETOOTH_VARIANTS = [const.VARIANT_ESP32S2]
|
||||
|
||||
esp32_ble_ns = cg.esphome_ns.namespace("esp32_ble")
|
||||
@@ -183,6 +211,9 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
cv.positive_int,
|
||||
cv.Range(min=1, max=64),
|
||||
),
|
||||
cv.Optional(CONF_MAX_CONNECTIONS, default=DEFAULT_MAX_CONNECTIONS): cv.All(
|
||||
cv.positive_int, cv.Range(min=1, max=IDF_MAX_CONNECTIONS)
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
@@ -230,6 +261,56 @@ def validate_variant(_):
|
||||
raise cv.Invalid(f"{variant} does not support Bluetooth")
|
||||
|
||||
|
||||
def consume_connection_slots(
|
||||
value: int, consumer: str
|
||||
) -> Callable[[MutableMapping], MutableMapping]:
|
||||
"""Reserve BLE connection slots for a component.
|
||||
|
||||
Args:
|
||||
value: Number of connection slots to reserve
|
||||
consumer: Name of the component consuming the slots
|
||||
|
||||
Returns:
|
||||
A validator function that records the slot usage
|
||||
"""
|
||||
|
||||
def _consume_connection_slots(config: MutableMapping) -> MutableMapping:
|
||||
data: dict[str, Any] = CORE.data.setdefault(KEY_ESP32_BLE, {})
|
||||
slots: list[str] = data.setdefault(KEY_USED_CONNECTION_SLOTS, [])
|
||||
slots.extend([consumer] * value)
|
||||
return config
|
||||
|
||||
return _consume_connection_slots
|
||||
|
||||
|
||||
def validate_connection_slots(max_connections: int) -> None:
|
||||
"""Validate that BLE connection slots don't exceed the configured maximum."""
|
||||
ble_data = CORE.data.get(KEY_ESP32_BLE, {})
|
||||
used_slots = ble_data.get(KEY_USED_CONNECTION_SLOTS, [])
|
||||
num_used = len(used_slots)
|
||||
|
||||
if num_used <= max_connections:
|
||||
return
|
||||
|
||||
slot_users = ", ".join(used_slots)
|
||||
|
||||
if num_used > IDF_MAX_CONNECTIONS:
|
||||
raise cv.Invalid(
|
||||
f"BLE components require {num_used} connection slots but maximum is {IDF_MAX_CONNECTIONS}. "
|
||||
f"Reduce the number of BLE clients. Components: {slot_users}"
|
||||
)
|
||||
|
||||
_LOGGER.warning(
|
||||
"BLE components require %d connection slot(s) but only %d configured. "
|
||||
"Please set 'max_connections: %d' in the 'esp32_ble' component. "
|
||||
"Components: %s",
|
||||
num_used,
|
||||
max_connections,
|
||||
num_used,
|
||||
slot_users,
|
||||
)
|
||||
|
||||
|
||||
def final_validation(config):
|
||||
validate_variant(config)
|
||||
if (name := config.get(CONF_NAME)) is not None:
|
||||
@@ -245,6 +326,10 @@ def final_validation(config):
|
||||
# Set GATT Client/Server sdkconfig options based on which components are loaded
|
||||
full_config = fv.full_config.get()
|
||||
|
||||
# Validate connection slots usage
|
||||
max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS)
|
||||
validate_connection_slots(max_connections)
|
||||
|
||||
# Check if BLE Server is needed
|
||||
has_ble_server = "esp32_ble_server" in full_config
|
||||
add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server)
|
||||
@@ -255,6 +340,26 @@ def final_validation(config):
|
||||
)
|
||||
add_idf_sdkconfig_option("CONFIG_BT_GATTC_ENABLE", has_ble_client)
|
||||
|
||||
# Handle max_connections: check for deprecated location in esp32_ble_tracker
|
||||
max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS)
|
||||
|
||||
# Use value from tracker if esp32_ble doesn't have it explicitly set (backward compat)
|
||||
if "esp32_ble_tracker" in full_config:
|
||||
tracker_config = full_config["esp32_ble_tracker"]
|
||||
if "max_connections" in tracker_config and CONF_MAX_CONNECTIONS not in config:
|
||||
max_connections = tracker_config["max_connections"]
|
||||
|
||||
# Set CONFIG_BT_ACL_CONNECTIONS to the maximum connections needed + 1 for ADV/SCAN
|
||||
# This is the Bluedroid host stack total instance limit (range 1-9, default 4)
|
||||
# Total instances = ADV/SCAN (1) + connection slots (max_connections)
|
||||
# Shared between client (tracker/ble_client) and server
|
||||
add_idf_sdkconfig_option("CONFIG_BT_ACL_CONNECTIONS", max_connections + 1)
|
||||
|
||||
# Set controller-specific max connections for ESP32 (classic)
|
||||
# CONFIG_BTDM_CTRL_BLE_MAX_CONN is ESP32-specific controller limit (just connections, not ADV/SCAN)
|
||||
# For newer chips (C3/S3/etc), different configs are used automatically
|
||||
add_idf_sdkconfig_option("CONFIG_BTDM_CTRL_BLE_MAX_CONN", max_connections)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
@@ -270,6 +375,10 @@ async def to_code(config):
|
||||
cg.add(var.set_name(name))
|
||||
await cg.register_component(var, config)
|
||||
|
||||
# Define max connections for use in C++ code (e.g., ble_server.h)
|
||||
max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS)
|
||||
cg.add_define("USE_ESP32_BLE_MAX_CONNECTIONS", max_connections)
|
||||
|
||||
add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True)
|
||||
add_idf_sdkconfig_option("CONFIG_BT_BLE_42_FEATURES_SUPPORTED", True)
|
||||
|
||||
|
@@ -49,7 +49,11 @@ void BLECharacteristic::notify() {
|
||||
this->service_->get_server()->get_connected_client_count() == 0)
|
||||
return;
|
||||
|
||||
for (auto &client : this->service_->get_server()->get_clients()) {
|
||||
const uint16_t *clients = this->service_->get_server()->get_clients();
|
||||
uint8_t client_count = this->service_->get_server()->get_client_count();
|
||||
|
||||
for (uint8_t i = 0; i < client_count; i++) {
|
||||
uint16_t client = clients[i];
|
||||
size_t length = this->value_.size();
|
||||
// Find the client in the list of clients to notify
|
||||
auto *entry = this->find_client_in_notify_list_(client);
|
||||
|
@@ -185,9 +185,35 @@ void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t ga
|
||||
}
|
||||
}
|
||||
|
||||
int8_t BLEServer::find_client_index_(uint16_t conn_id) const {
|
||||
for (uint8_t i = 0; i < this->client_count_; i++) {
|
||||
if (this->clients_[i] == conn_id)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void BLEServer::add_client_(uint16_t conn_id) {
|
||||
// Check if already in list
|
||||
if (this->find_client_index_(conn_id) >= 0)
|
||||
return;
|
||||
// Add if there's space
|
||||
if (this->client_count_ < USE_ESP32_BLE_MAX_CONNECTIONS) {
|
||||
this->clients_[this->client_count_++] = conn_id;
|
||||
}
|
||||
}
|
||||
|
||||
void BLEServer::remove_client_(uint16_t conn_id) {
|
||||
int8_t index = this->find_client_index_(conn_id);
|
||||
if (index >= 0) {
|
||||
// Replace with last element and decrement count
|
||||
this->clients_[index] = this->clients_[--this->client_count_];
|
||||
}
|
||||
}
|
||||
|
||||
void BLEServer::ble_before_disabled_event_handler() {
|
||||
// Delete all clients
|
||||
this->clients_.clear();
|
||||
this->client_count_ = 0;
|
||||
// Delete all services
|
||||
for (auto &entry : this->services_) {
|
||||
entry.service->do_delete();
|
||||
|
@@ -12,7 +12,6 @@
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <functional>
|
||||
|
||||
#ifdef USE_ESP32
|
||||
@@ -47,8 +46,9 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv
|
||||
void set_device_information_service(BLEService *service) { this->device_information_service_ = service; }
|
||||
|
||||
esp_gatt_if_t get_gatts_if() { return this->gatts_if_; }
|
||||
uint32_t get_connected_client_count() { return this->clients_.size(); }
|
||||
const std::unordered_set<uint16_t> &get_clients() { return this->clients_; }
|
||||
uint32_t get_connected_client_count() { return this->client_count_; }
|
||||
const uint16_t *get_clients() const { return this->clients_; }
|
||||
uint8_t get_client_count() const { return this->client_count_; }
|
||||
|
||||
void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
|
||||
esp_ble_gatts_cb_param_t *param) override;
|
||||
@@ -82,8 +82,9 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv
|
||||
|
||||
void restart_advertising_();
|
||||
|
||||
void add_client_(uint16_t conn_id) { this->clients_.insert(conn_id); }
|
||||
void remove_client_(uint16_t conn_id) { this->clients_.erase(conn_id); }
|
||||
int8_t find_client_index_(uint16_t conn_id) const;
|
||||
void add_client_(uint16_t conn_id);
|
||||
void remove_client_(uint16_t conn_id);
|
||||
void dispatch_callbacks_(CallbackType type, uint16_t conn_id);
|
||||
|
||||
std::vector<CallbackEntry> callbacks_;
|
||||
@@ -92,7 +93,8 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv
|
||||
esp_gatt_if_t gatts_if_{0};
|
||||
bool registered_{false};
|
||||
|
||||
std::unordered_set<uint16_t> clients_;
|
||||
uint16_t clients_[USE_ESP32_BLE_MAX_CONNECTIONS]{};
|
||||
uint8_t client_count_{0};
|
||||
std::vector<ServiceEntry> services_{};
|
||||
std::vector<BLEService *> services_to_start_{};
|
||||
BLEService *device_information_service_{};
|
||||
|
@@ -1,14 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, MutableMapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32_ble
|
||||
from esphome.components.esp32 import add_idf_sdkconfig_option
|
||||
from esphome.components.esp32_ble import (
|
||||
DEFAULT_MAX_CONNECTIONS,
|
||||
IDF_MAX_CONNECTIONS,
|
||||
BTLoggers,
|
||||
bt_uuid,
|
||||
bt_uuid16_format,
|
||||
@@ -39,18 +39,12 @@ AUTO_LOAD = ["esp32_ble"]
|
||||
DEPENDENCIES = ["esp32"]
|
||||
CODEOWNERS = ["@bdraco"]
|
||||
|
||||
KEY_ESP32_BLE_TRACKER = "esp32_ble_tracker"
|
||||
KEY_USED_CONNECTION_SLOTS = "used_connection_slots"
|
||||
|
||||
CONF_ESP32_BLE_ID = "esp32_ble_id"
|
||||
CONF_SCAN_PARAMETERS = "scan_parameters"
|
||||
CONF_WINDOW = "window"
|
||||
CONF_ON_SCAN_END = "on_scan_end"
|
||||
CONF_SOFTWARE_COEXISTENCE = "software_coexistence"
|
||||
|
||||
DEFAULT_MAX_CONNECTIONS = 3
|
||||
IDF_MAX_CONNECTIONS = 9
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -128,6 +122,15 @@ def validate_scan_parameters(config):
|
||||
return config
|
||||
|
||||
|
||||
def validate_max_connections_deprecated(config: ConfigType) -> ConfigType:
|
||||
if CONF_MAX_CONNECTIONS in config:
|
||||
_LOGGER.warning(
|
||||
"The 'max_connections' option in 'esp32_ble_tracker' is deprecated. "
|
||||
"Please move it to the 'esp32_ble' component instead."
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
def as_hex(value):
|
||||
return cg.RawExpression(f"0x{value}ULL")
|
||||
|
||||
@@ -150,18 +153,6 @@ def as_reversed_hex_array(value):
|
||||
)
|
||||
|
||||
|
||||
def consume_connection_slots(
|
||||
value: int, consumer: str
|
||||
) -> Callable[[MutableMapping], MutableMapping]:
|
||||
def _consume_connection_slots(config: MutableMapping) -> MutableMapping:
|
||||
data: dict[str, Any] = CORE.data.setdefault(KEY_ESP32_BLE_TRACKER, {})
|
||||
slots: list[str] = data.setdefault(KEY_USED_CONNECTION_SLOTS, [])
|
||||
slots.extend([consumer] * value)
|
||||
return config
|
||||
|
||||
return _consume_connection_slots
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
@@ -224,48 +215,11 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.OnlyWith(CONF_SOFTWARE_COEXISTENCE, "wifi", default=True): bool,
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
validate_max_connections_deprecated,
|
||||
)
|
||||
|
||||
|
||||
def validate_remaining_connections(config):
|
||||
data: dict[str, Any] = CORE.data.get(KEY_ESP32_BLE_TRACKER, {})
|
||||
slots: list[str] = data.get(KEY_USED_CONNECTION_SLOTS, [])
|
||||
used_slots = len(slots)
|
||||
if used_slots <= config[CONF_MAX_CONNECTIONS]:
|
||||
return config
|
||||
slot_users = ", ".join(slots)
|
||||
|
||||
if used_slots < IDF_MAX_CONNECTIONS:
|
||||
_LOGGER.warning(
|
||||
"esp32_ble_tracker exceeded `%s`: components attempted to consume %d "
|
||||
"connection slot(s) out of available configured maximum %d connection "
|
||||
"slot(s); The system automatically increased `%s` to %d to match the "
|
||||
"number of used connection slot(s) by components: %s.",
|
||||
CONF_MAX_CONNECTIONS,
|
||||
used_slots,
|
||||
config[CONF_MAX_CONNECTIONS],
|
||||
CONF_MAX_CONNECTIONS,
|
||||
used_slots,
|
||||
slot_users,
|
||||
)
|
||||
config[CONF_MAX_CONNECTIONS] = used_slots
|
||||
return config
|
||||
|
||||
msg = (
|
||||
f"esp32_ble_tracker exceeded `{CONF_MAX_CONNECTIONS}`: "
|
||||
f"components attempted to consume {used_slots} connection slot(s) "
|
||||
f"out of available configured maximum {config[CONF_MAX_CONNECTIONS]} "
|
||||
f"connection slot(s); Decrease the number of BLE clients ({slot_users})"
|
||||
)
|
||||
if config[CONF_MAX_CONNECTIONS] < IDF_MAX_CONNECTIONS:
|
||||
msg += f" or increase {CONF_MAX_CONNECTIONS}` to {used_slots}"
|
||||
msg += f" to stay under the {IDF_MAX_CONNECTIONS} connection slot(s) limit."
|
||||
raise cv.Invalid(msg)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = cv.All(
|
||||
validate_remaining_connections, esp32_ble.validate_variant
|
||||
)
|
||||
FINAL_VALIDATE_SCHEMA = esp32_ble.validate_variant
|
||||
|
||||
ESP_BLE_DEVICE_SCHEMA = cv.Schema(
|
||||
{
|
||||
@@ -345,10 +299,8 @@ async def to_code(config):
|
||||
# Match arduino CONFIG_BTU_TASK_STACK_SIZE
|
||||
# https://github.com/espressif/arduino-esp32/blob/fd72cf46ad6fc1a6de99c1d83ba8eba17d80a4ee/tools/sdk/esp32/sdkconfig#L1866
|
||||
add_idf_sdkconfig_option("CONFIG_BT_BTU_TASK_STACK_SIZE", 8192)
|
||||
add_idf_sdkconfig_option("CONFIG_BT_ACL_CONNECTIONS", 9)
|
||||
add_idf_sdkconfig_option(
|
||||
"CONFIG_BTDM_CTRL_BLE_MAX_CONN", config[CONF_MAX_CONNECTIONS]
|
||||
)
|
||||
# Note: CONFIG_BT_ACL_CONNECTIONS and CONFIG_BTDM_CTRL_BLE_MAX_CONN are now
|
||||
# configured in esp32_ble component based on max_connections setting
|
||||
|
||||
cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts
|
||||
cg.add_define("USE_ESP32_BLE_CLIENT")
|
||||
|
@@ -159,6 +159,7 @@
|
||||
#define BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE 16
|
||||
#define USE_CAPTIVE_PORTAL
|
||||
#define USE_ESP32_BLE
|
||||
#define USE_ESP32_BLE_MAX_CONNECTIONS 3
|
||||
#define USE_ESP32_BLE_CLIENT
|
||||
#define USE_ESP32_BLE_DEVICE
|
||||
#define USE_ESP32_BLE_SERVER
|
||||
|
Reference in New Issue
Block a user