1
0
mirror of https://github.com/esphome/esphome.git synced 2025-04-15 15:20:27 +01:00

Rework max connections for BLE to avoid exceeding the hard limit (#8303)

This commit is contained in:
J. Nick Koston 2025-04-06 14:48:12 -10:00 committed by GitHub
parent f3b1b11eba
commit 23e5cdb30e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 224 additions and 72 deletions

View File

@ -67,7 +67,7 @@ CONF_AUTO_CONNECT = "auto_connect"
MULTI_CONF = True MULTI_CONF = True
CONFIG_SCHEMA = ( CONFIG_SCHEMA = cv.All(
cv.Schema( cv.Schema(
{ {
cv.GenerateID(): cv.declare_id(BLEClient), cv.GenerateID(): cv.declare_id(BLEClient),
@ -114,7 +114,8 @@ CONFIG_SCHEMA = (
} }
) )
.extend(cv.COMPONENT_SCHEMA) .extend(cv.COMPONENT_SCHEMA)
.extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA),
esp32_ble_tracker.consume_connection_slots(1, "ble_client"),
) )
CONF_BLE_CLIENT_ID = "ble_client_id" CONF_BLE_CLIENT_ID = "ble_client_id"

View File

@ -8,9 +8,10 @@ AUTO_LOAD = ["esp32_ble_client", "esp32_ble_tracker"]
DEPENDENCIES = ["api", "esp32"] DEPENDENCIES = ["api", "esp32"]
CODEOWNERS = ["@jesserockz"] CODEOWNERS = ["@jesserockz"]
CONF_CONNECTION_SLOTS = "connection_slots"
CONF_CACHE_SERVICES = "cache_services" CONF_CACHE_SERVICES = "cache_services"
CONF_CONNECTIONS = "connections" CONF_CONNECTIONS = "connections"
MAX_CONNECTIONS = 3 DEFAULT_CONNECTION_SLOTS = 3
bluetooth_proxy_ns = cg.esphome_ns.namespace("bluetooth_proxy") bluetooth_proxy_ns = cg.esphome_ns.namespace("bluetooth_proxy")
@ -35,28 +36,42 @@ def validate_connections(config):
"Connections can only be used if the proxy is set to active" "Connections can only be used if the proxy is set to active"
) )
elif config[CONF_ACTIVE]: elif config[CONF_ACTIVE]:
conf = config.copy() connection_slots: int = config[CONF_CONNECTION_SLOTS]
conf[CONF_CONNECTIONS] = [CONNECTION_SCHEMA({}) for _ in range(MAX_CONNECTIONS)] esp32_ble_tracker.consume_connection_slots(connection_slots, "bluetooth_proxy")(
return conf config
)
return {
**config,
CONF_CONNECTIONS: [CONNECTION_SCHEMA({}) for _ in range(connection_slots)],
}
return config return config
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
cv.Schema( (
{ cv.Schema(
cv.GenerateID(): cv.declare_id(BluetoothProxy), {
cv.Optional(CONF_ACTIVE, default=False): cv.boolean, cv.GenerateID(): cv.declare_id(BluetoothProxy),
cv.SplitDefault(CONF_CACHE_SERVICES, esp32_idf=True): cv.All( cv.Optional(CONF_ACTIVE, default=False): cv.boolean,
cv.only_with_esp_idf, cv.boolean cv.SplitDefault(CONF_CACHE_SERVICES, esp32_idf=True): cv.All(
), cv.only_with_esp_idf, cv.boolean
cv.Optional(CONF_CONNECTIONS): cv.All( ),
cv.ensure_list(CONNECTION_SCHEMA), cv.Optional(
cv.Length(min=1, max=MAX_CONNECTIONS), CONF_CONNECTION_SLOTS,
), default=DEFAULT_CONNECTION_SLOTS,
} ): cv.All(
) cv.positive_int,
.extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) cv.Range(min=1, max=esp32_ble_tracker.max_connections()),
.extend(cv.COMPONENT_SCHEMA), ),
cv.Optional(CONF_CONNECTIONS): cv.All(
cv.ensure_list(CONNECTION_SCHEMA),
cv.Length(min=1, max=esp32_ble_tracker.max_connections()),
),
}
)
.extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
.extend(cv.COMPONENT_SCHEMA)
),
validate_connections, validate_connections,
) )

View File

@ -1,3 +1,9 @@
from __future__ import annotations
from collections.abc import MutableMapping
import logging
from typing import Any, Callable
from esphome import automation from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import esp32_ble from esphome.components import esp32_ble
@ -29,11 +35,21 @@ from esphome.core import CORE
AUTO_LOAD = ["esp32_ble"] AUTO_LOAD = ["esp32_ble"]
DEPENDENCIES = ["esp32"] DEPENDENCIES = ["esp32"]
KEY_ESP32_BLE_TRACKER = "esp32_ble_tracker"
KEY_USED_CONNECTION_SLOTS = "used_connection_slots"
CONF_MAX_CONNECTIONS = "max_connections"
CONF_ESP32_BLE_ID = "esp32_ble_id" CONF_ESP32_BLE_ID = "esp32_ble_id"
CONF_SCAN_PARAMETERS = "scan_parameters" CONF_SCAN_PARAMETERS = "scan_parameters"
CONF_WINDOW = "window" CONF_WINDOW = "window"
CONF_CONTINUOUS = "continuous" CONF_CONTINUOUS = "continuous"
CONF_ON_SCAN_END = "on_scan_end" CONF_ON_SCAN_END = "on_scan_end"
DEFAULT_MAX_CONNECTIONS = 3
IDF_MAX_CONNECTIONS = 9
_LOGGER = logging.getLogger(__name__)
esp32_ble_tracker_ns = cg.esphome_ns.namespace("esp32_ble_tracker") esp32_ble_tracker_ns = cg.esphome_ns.namespace("esp32_ble_tracker")
ESP32BLETracker = esp32_ble_tracker_ns.class_( ESP32BLETracker = esp32_ble_tracker_ns.class_(
"ESP32BLETracker", "ESP32BLETracker",
@ -112,61 +128,126 @@ def as_reversed_hex_array(value):
) )
CONFIG_SCHEMA = cv.Schema( def max_connections() -> int:
{ return IDF_MAX_CONNECTIONS if CORE.using_esp_idf else DEFAULT_MAX_CONNECTIONS
cv.GenerateID(): cv.declare_id(ESP32BLETracker),
cv.GenerateID(esp32_ble.CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE),
cv.Optional(CONF_SCAN_PARAMETERS, default={}): cv.All( def consume_connection_slots(
cv.Schema( 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(
{
cv.GenerateID(): cv.declare_id(ESP32BLETracker),
cv.GenerateID(esp32_ble.CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE),
cv.Optional(CONF_MAX_CONNECTIONS, default=DEFAULT_MAX_CONNECTIONS): cv.All(
cv.positive_int, cv.Range(min=0, max=max_connections())
),
cv.Optional(CONF_SCAN_PARAMETERS, default={}): cv.All(
cv.Schema(
{
cv.Optional(
CONF_DURATION, default="5min"
): cv.positive_time_period_seconds,
cv.Optional(
CONF_INTERVAL, default="320ms"
): cv.positive_time_period_milliseconds,
cv.Optional(
CONF_WINDOW, default="30ms"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_ACTIVE, default=True): cv.boolean,
cv.Optional(CONF_CONTINUOUS, default=True): cv.boolean,
}
),
validate_scan_parameters,
),
cv.Optional(CONF_ON_BLE_ADVERTISE): automation.validate_automation(
{ {
cv.Optional( cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
CONF_DURATION, default="5min" ESPBTAdvertiseTrigger
): cv.positive_time_period_seconds, ),
cv.Optional( cv.Optional(CONF_MAC_ADDRESS): cv.ensure_list(cv.mac_address),
CONF_INTERVAL, default="320ms"
): cv.positive_time_period_milliseconds,
cv.Optional(
CONF_WINDOW, default="30ms"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_ACTIVE, default=True): cv.boolean,
cv.Optional(CONF_CONTINUOUS, default=True): cv.boolean,
} }
), ),
validate_scan_parameters, cv.Optional(
), CONF_ON_BLE_SERVICE_DATA_ADVERTISE
cv.Optional(CONF_ON_BLE_ADVERTISE): automation.validate_automation( ): automation.validate_automation(
{ {
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESPBTAdvertiseTrigger), cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
cv.Optional(CONF_MAC_ADDRESS): cv.ensure_list(cv.mac_address), BLEServiceDataAdvertiseTrigger
} ),
), cv.Optional(CONF_MAC_ADDRESS): cv.mac_address,
cv.Optional(CONF_ON_BLE_SERVICE_DATA_ADVERTISE): automation.validate_automation( cv.Required(CONF_SERVICE_UUID): bt_uuid,
{ }
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( ),
BLEServiceDataAdvertiseTrigger cv.Optional(
), CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE
cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, ): automation.validate_automation(
cv.Required(CONF_SERVICE_UUID): bt_uuid, {
} cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
), BLEManufacturerDataAdvertiseTrigger
cv.Optional( ),
CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE cv.Optional(CONF_MAC_ADDRESS): cv.mac_address,
): automation.validate_automation( cv.Required(CONF_MANUFACTURER_ID): bt_uuid,
{ }
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( ),
BLEManufacturerDataAdvertiseTrigger cv.Optional(CONF_ON_SCAN_END): automation.validate_automation(
), {cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(BLEEndOfScanTrigger)}
cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, ),
cv.Required(CONF_MANUFACTURER_ID): bt_uuid, }
} ).extend(cv.COMPONENT_SCHEMA),
), )
cv.Optional(CONF_ON_SCAN_END): automation.validate_automation(
{cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(BLEEndOfScanTrigger)}
),
}
).extend(cv.COMPONENT_SCHEMA)
FINAL_VALIDATE_SCHEMA = esp32_ble.validate_variant
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)
hard_limit = max_connections()
if used_slots < hard_limit:
_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] < hard_limit:
msg += f" or increase {CONF_MAX_CONNECTIONS}` to {used_slots}"
msg += f" to stay under the {hard_limit} connection slot(s) limit."
raise cv.Invalid(msg)
FINAL_VALIDATE_SCHEMA = cv.All(
validate_remaining_connections, esp32_ble.validate_variant
)
ESP_BLE_DEVICE_SCHEMA = cv.Schema( ESP_BLE_DEVICE_SCHEMA = cv.Schema(
{ {
@ -238,6 +319,9 @@ async def to_code(config):
else: else:
add_idf_sdkconfig_option("CONFIG_BTU_TASK_STACK_SIZE", 8192) add_idf_sdkconfig_option("CONFIG_BTU_TASK_STACK_SIZE", 8192)
add_idf_sdkconfig_option("CONFIG_BT_ACL_CONNECTIONS", 9) add_idf_sdkconfig_option("CONFIG_BT_ACL_CONNECTIONS", 9)
add_idf_sdkconfig_option(
"CONFIG_BTDM_CTRL_BLE_MAX_CONN", config[CONF_MAX_CONNECTIONS]
)
# CONFIG_BT_GATTC_NOTIF_REG_MAX controls the number of # CONFIG_BT_GATTC_NOTIF_REG_MAX controls the number of
# max notifications in 5.x, setting CONFIG_BT_ACL_CONNECTIONS # max notifications in 5.x, setting CONFIG_BT_ACL_CONNECTIONS
# is enough in 4.x # is enough in 4.x

View File

@ -0,0 +1,8 @@
wifi:
ssid: MySSID
password: password1
ota:
- platform: esphome
api:

View File

@ -0,0 +1,8 @@
<<: !include common.yaml
esp32_ble_tracker:
max_connections: 3
bluetooth_proxy:
active: true
connection_slots: 2

View File

@ -0,0 +1,8 @@
<<: !include common.yaml
esp32_ble_tracker:
max_connections: 3
bluetooth_proxy:
active: true
connection_slots: 2

View File

@ -0,0 +1,8 @@
<<: !include common.yaml
esp32_ble_tracker:
max_connections: 9
bluetooth_proxy:
active: true
connection_slots: 9

View File

@ -0,0 +1,8 @@
<<: !include common.yaml
esp32_ble_tracker:
max_connections: 9
bluetooth_proxy:
active: true
connection_slots: 9

View File

@ -1 +1,4 @@
<<: !include common.yaml <<: !include common.yaml
esp32_ble_tracker:
max_connections: 3

View File

@ -1 +1,4 @@
<<: !include common.yaml <<: !include common.yaml
esp32_ble_tracker:
max_connections: 3

View File

@ -1 +1,4 @@
<<: !include common.yaml <<: !include common.yaml
esp32_ble_tracker:
max_connections: 9

View File

@ -1 +1,4 @@
<<: !include common.yaml <<: !include common.yaml
esp32_ble_tracker:
max_connections: 9