Compare commits

..

2 Commits

Author SHA1 Message Date
J. Nick Koston
b3b55c6b6b cover 2025-08-02 22:28:34 -10:00
clydebarrow
52204b2439 [config] Fix reversion of excessive yaml output after error 2025-08-03 13:24:42 +10:00
114 changed files with 1061 additions and 3750 deletions

View File

@@ -168,8 +168,6 @@ This document provides essential context for AI models interacting with this pro
* `platformio.ini`: Configures the PlatformIO build environments for different microcontrollers.
* `.pre-commit-config.yaml`: Configures the pre-commit hooks for linting and formatting.
* **CI/CD Pipeline:** Defined in `.github/workflows`.
* **Static Analysis & Development:**
* `esphome/core/defines.h`: A comprehensive header file containing all `#define` directives that can be added by components using `cg.add_define()` in Python. This file is used exclusively for development, static analysis tools, and CI testing - it is not used during runtime compilation. When developing components that add new defines, they must be added to this file to ensure proper IDE support and static analysis coverage. The file includes feature flags, build configurations, and platform-specific defines that help static analyzers understand the complete codebase without needing to compile for specific platforms.
## 6. Development & Testing Workflow

View File

@@ -102,12 +102,12 @@ jobs:
uses: docker/setup-buildx-action@v3.11.1
- name: Log in to docker hub
uses: docker/login-action@v3.5.0
uses: docker/login-action@v3.4.0
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry
uses: docker/login-action@v3.5.0
uses: docker/login-action@v3.4.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -182,13 +182,13 @@ jobs:
- name: Log in to docker hub
if: matrix.registry == 'dockerhub'
uses: docker/login-action@v3.5.0
uses: docker/login-action@v3.4.0
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry
if: matrix.registry == 'ghcr'
uses: docker/login-action@v3.5.0
uses: docker/login-action@v3.4.0
with:
registry: ghcr.io
username: ${{ github.actor }}

View File

@@ -40,11 +40,11 @@ esphome/components/analog_threshold/* @ianchi
esphome/components/animation/* @syndlex
esphome/components/anova/* @buxtronix
esphome/components/apds9306/* @aodrenah
esphome/components/api/* @esphome/core
esphome/components/api/* @OttoWinter
esphome/components/as5600/* @ammmze
esphome/components/as5600/sensor/* @ammmze
esphome/components/as7341/* @mrgnr
esphome/components/async_tcp/* @esphome/core
esphome/components/async_tcp/* @OttoWinter
esphome/components/at581x/* @X-Ryl669
esphome/components/atc_mithermometer/* @ahpohl
esphome/components/atm90e26/* @danieltwagner
@@ -69,7 +69,7 @@ esphome/components/bl0939/* @ziceva
esphome/components/bl0940/* @tobias-
esphome/components/bl0942/* @dbuezas @dwmw2
esphome/components/ble_client/* @buxtronix @clydebarrow
esphome/components/bluetooth_proxy/* @bdraco @jesserockz
esphome/components/bluetooth_proxy/* @jesserockz
esphome/components/bme280_base/* @esphome/core
esphome/components/bme280_spi/* @apbodrov
esphome/components/bme680_bsec/* @trvrnrth
@@ -91,7 +91,7 @@ esphome/components/bytebuffer/* @clydebarrow
esphome/components/camera/* @DT-art1 @bdraco
esphome/components/canbus/* @danielschramm @mvturnho
esphome/components/cap1188/* @mreditor97
esphome/components/captive_portal/* @esphome/core
esphome/components/captive_portal/* @OttoWinter
esphome/components/ccs811/* @habbie
esphome/components/cd74hc4067/* @asoehlke
esphome/components/ch422g/* @clydebarrow @jesterret
@@ -118,7 +118,7 @@ esphome/components/dallas_temp/* @ssieb
esphome/components/daly_bms/* @s1lvi0
esphome/components/dashboard_import/* @esphome/core
esphome/components/datetime/* @jesserockz @rfdarter
esphome/components/debug/* @esphome/core
esphome/components/debug/* @OttoWinter
esphome/components/delonghi/* @grob6000
esphome/components/dfplayer/* @glmnet
esphome/components/dfrobot_sen0395/* @niklasweber
@@ -144,10 +144,9 @@ esphome/components/es8156/* @kbx81
esphome/components/es8311/* @kahrendt @kroimon
esphome/components/es8388/* @P4uLT
esphome/components/esp32/* @esphome/core
esphome/components/esp32_ble/* @Rapsssito @bdraco @jesserockz
esphome/components/esp32_ble_client/* @bdraco @jesserockz
esphome/components/esp32_ble/* @Rapsssito @jesserockz
esphome/components/esp32_ble_client/* @jesserockz
esphome/components/esp32_ble_server/* @Rapsssito @clydebarrow @jesserockz
esphome/components/esp32_ble_tracker/* @bdraco
esphome/components/esp32_camera_web_server/* @ayufan
esphome/components/esp32_can/* @Sympatron
esphome/components/esp32_hosted/* @swoboda1337
@@ -238,7 +237,7 @@ esphome/components/integration/* @OttoWinter
esphome/components/internal_temperature/* @Mat931
esphome/components/interval/* @esphome/core
esphome/components/jsn_sr04t/* @Mafus1
esphome/components/json/* @esphome/core
esphome/components/json/* @OttoWinter
esphome/components/kamstrup_kmp/* @cfeenstra1024
esphome/components/key_collector/* @ssieb
esphome/components/key_provider/* @ssieb
@@ -468,7 +467,7 @@ esphome/components/template/event/* @nohat
esphome/components/template/fan/* @ssieb
esphome/components/text/* @mauritskorse
esphome/components/thermostat/* @kbx81
esphome/components/time/* @esphome/core
esphome/components/time/* @OttoWinter
esphome/components/tlc5947/* @rnauber
esphome/components/tlc5971/* @IJIJI
esphome/components/tm1621/* @Philippe12
@@ -512,7 +511,7 @@ esphome/components/wake_on_lan/* @clydebarrow @willwill2will54
esphome/components/watchdog/* @oarcher
esphome/components/waveshare_epaper/* @clydebarrow
esphome/components/web_server/ota/* @esphome/core
esphome/components/web_server_base/* @esphome/core
esphome/components/web_server_base/* @OttoWinter
esphome/components/web_server_idf/* @dentra
esphome/components/weikai/* @DrCoolZic
esphome/components/weikai_i2c/* @DrCoolZic

View File

@@ -9,7 +9,6 @@ import os
import re
import sys
import time
from typing import Protocol
import argcomplete
@@ -45,7 +44,6 @@ from esphome.const import (
from esphome.core import CORE, EsphomeError, coroutine
from esphome.helpers import get_bool_env, indent, is_ip_address
from esphome.log import AnsiFore, color, setup_log
from esphome.types import ConfigType
from esphome.util import (
get_serial_ports,
list_yaml_files,
@@ -57,23 +55,6 @@ from esphome.util import (
_LOGGER = logging.getLogger(__name__)
class ArgsProtocol(Protocol):
device: list[str] | None
reset: bool
username: str | None
password: str | None
client_id: str | None
topic: str | None
file: str | None
no_logs: bool
only_generate: bool
show_secrets: bool
dashboard: bool
configuration: str
name: str
upload_speed: str | None
def choose_prompt(options, purpose: str = None):
if not options:
raise EsphomeError(
@@ -107,54 +88,30 @@ def choose_prompt(options, purpose: str = None):
def choose_upload_log_host(
default: list[str] | str | None,
check_default: str | None,
show_ota: bool,
show_mqtt: bool,
show_api: bool,
purpose: str | None = None,
) -> list[str]:
# Convert to list for uniform handling
defaults = [default] if isinstance(default, str) else default or []
# If devices specified, resolve them
if defaults:
resolved: list[str] = []
for device in defaults:
if device == "SERIAL":
serial_ports = get_serial_ports()
if not serial_ports:
_LOGGER.warning("No serial ports found, skipping SERIAL device")
continue
options = [
(f"{port.path} ({port.description})", port.path)
for port in serial_ports
]
resolved.append(choose_prompt(options, purpose=purpose))
elif device == "OTA":
if (show_ota and "ota" in CORE.config) or (
show_api and "api" in CORE.config
):
resolved.append(CORE.address)
elif show_mqtt and has_mqtt_logging():
resolved.append("MQTT")
else:
resolved.append(device)
return resolved
# No devices specified, show interactive chooser
default, check_default, show_ota, show_mqtt, show_api, purpose: str = None
):
options = [
(f"{port.path} ({port.description})", port.path) for port in get_serial_ports()
]
if default == "SERIAL":
return choose_prompt(options, purpose=purpose)
if (show_ota and "ota" in CORE.config) or (show_api and "api" in CORE.config):
options.append((f"Over The Air ({CORE.address})", CORE.address))
if show_mqtt and has_mqtt_logging():
mqtt_config = CORE.config[CONF_MQTT]
if default == "OTA":
return CORE.address
if (
show_mqtt
and (mqtt_config := CORE.config.get(CONF_MQTT))
and mqtt_logging_enabled(mqtt_config)
):
options.append((f"MQTT ({mqtt_config[CONF_BROKER]})", "MQTT"))
if default == "OTA":
return "MQTT"
if default is not None:
return default
if check_default is not None and check_default in [opt[1] for opt in options]:
return [check_default]
return [choose_prompt(options, purpose=purpose)]
return check_default
return choose_prompt(options, purpose=purpose)
def mqtt_logging_enabled(mqtt_config):
@@ -166,14 +123,7 @@ def mqtt_logging_enabled(mqtt_config):
return log_topic.get(CONF_LEVEL, None) != "NONE"
def has_mqtt_logging() -> bool:
"""Check if MQTT logging is available."""
return (mqtt_config := CORE.config.get(CONF_MQTT)) and mqtt_logging_enabled(
mqtt_config
)
def get_port_type(port: str) -> str:
def get_port_type(port):
if port.startswith("/") or port.startswith("COM"):
return "SERIAL"
if port == "MQTT":
@@ -181,7 +131,7 @@ def get_port_type(port: str) -> str:
return "NETWORK"
def run_miniterm(config: ConfigType, port: str, args) -> int:
def run_miniterm(config, port, args):
from aioesphomeapi import LogParser
import serial
@@ -258,7 +208,7 @@ def wrap_to_code(name, comp):
return wrapped
def write_cpp(config: ConfigType) -> int:
def write_cpp(config):
if not get_bool_env(ENV_NOGITIGNORE):
writer.write_gitignore()
@@ -266,7 +216,7 @@ def write_cpp(config: ConfigType) -> int:
return write_cpp_file()
def generate_cpp_contents(config: ConfigType) -> None:
def generate_cpp_contents(config):
_LOGGER.info("Generating C++ source...")
for name, component, conf in iter_component_configs(CORE.config):
@@ -277,7 +227,7 @@ def generate_cpp_contents(config: ConfigType) -> None:
CORE.flush_tasks()
def write_cpp_file() -> int:
def write_cpp_file():
code_s = indent(CORE.cpp_main_section)
writer.write_cpp(code_s)
@@ -288,7 +238,7 @@ def write_cpp_file() -> int:
return 0
def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
def compile_program(args, config):
from esphome import platformio_api
_LOGGER.info("Compiling app...")
@@ -299,9 +249,7 @@ def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
return 0 if idedata is not None else 1
def upload_using_esptool(
config: ConfigType, port: str, file: str, speed: int
) -> str | int:
def upload_using_esptool(config, port, file, speed):
from esphome import platformio_api
first_baudrate = speed or config[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS].get(
@@ -329,20 +277,20 @@ def upload_using_esptool(
def run_esptool(baud_rate):
cmd = [
"esptool",
"esptool.py",
"--before",
"default-reset",
"default_reset",
"--after",
"hard-reset",
"hard_reset",
"--baud",
str(baud_rate),
"--port",
port,
"--chip",
mcu,
"write-flash",
"write_flash",
"-z",
"--flash-size",
"--flash_size",
"detect",
]
for img in flash_images:
@@ -366,7 +314,7 @@ def upload_using_esptool(
return run_esptool(115200)
def upload_using_platformio(config: ConfigType, port: str):
def upload_using_platformio(config, port):
from esphome import platformio_api
upload_args = ["-t", "upload", "-t", "nobuild"]
@@ -375,7 +323,7 @@ def upload_using_platformio(config: ConfigType, port: str):
return platformio_api.run_platformio_cli_run(config, CORE.verbose, *upload_args)
def check_permissions(port: str):
def check_permissions(port):
if os.name == "posix" and get_port_type(port) == "SERIAL":
# Check if we can open selected serial port
if not os.access(port, os.F_OK):
@@ -393,7 +341,7 @@ def check_permissions(port: str):
)
def upload_program(config: ConfigType, args: ArgsProtocol, host: str) -> int | str:
def upload_program(config, args, host):
try:
module = importlib.import_module("esphome.components." + CORE.target_platform)
if getattr(module, "upload_program")(config, args, host):
@@ -408,7 +356,7 @@ def upload_program(config: ConfigType, args: ArgsProtocol, host: str) -> int | s
return upload_using_esptool(config, host, file, args.upload_speed)
if CORE.target_platform in (PLATFORM_RP2040):
return upload_using_platformio(config, host)
return upload_using_platformio(config, args.device)
if CORE.is_libretiny:
return upload_using_platformio(config, host)
@@ -431,12 +379,9 @@ def upload_program(config: ConfigType, args: ArgsProtocol, host: str) -> int | s
remote_port = int(ota_conf[CONF_PORT])
password = ota_conf.get(CONF_PASSWORD, "")
# Check if we should use MQTT for address resolution
# This happens when no device was specified, or the current host is "MQTT"/"OTA"
devices: list[str] = args.device or []
if (
CONF_MQTT in config # pylint: disable=too-many-boolean-expressions
and (not devices or host in ("MQTT", "OTA"))
and (not args.device or args.device in ("MQTT", "OTA"))
and (
((config[CONF_MDNS][CONF_DISABLED]) and not is_ip_address(CORE.address))
or get_port_type(host) == "MQTT"
@@ -454,28 +399,23 @@ def upload_program(config: ConfigType, args: ArgsProtocol, host: str) -> int | s
return espota2.run_ota(host, remote_port, password, CORE.firmware_bin)
def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int | None:
def show_logs(config, args, port):
if "logger" not in config:
raise EsphomeError("Logger is not configured!")
port = devices[0]
if get_port_type(port) == "SERIAL":
check_permissions(port)
return run_miniterm(config, port, args)
if get_port_type(port) == "NETWORK" and "api" in config:
addresses_to_use = devices
if config[CONF_MDNS][CONF_DISABLED] and CONF_MQTT in config:
from esphome import mqtt
mqtt_address = mqtt.get_esphome_device_ip(
port = mqtt.get_esphome_device_ip(
config, args.username, args.password, args.client_id
)[0]
addresses_to_use = [mqtt_address]
from esphome.components.api.client import run_logs
return run_logs(config, addresses_to_use)
return run_logs(config, port)
if get_port_type(port) == "MQTT" and "mqtt" in config:
from esphome import mqtt
@@ -486,7 +426,7 @@ def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int
raise EsphomeError("No remote or local logging method configured (api/mqtt/logger)")
def clean_mqtt(config: ConfigType, args: ArgsProtocol) -> int | None:
def clean_mqtt(config, args):
from esphome import mqtt
return mqtt.clear_topic(
@@ -494,13 +434,13 @@ def clean_mqtt(config: ConfigType, args: ArgsProtocol) -> int | None:
)
def command_wizard(args: ArgsProtocol) -> int | None:
def command_wizard(args):
from esphome import wizard
return wizard.wizard(args.configuration)
def command_config(args: ArgsProtocol, config: ConfigType) -> int | None:
def command_config(args, config):
if not CORE.verbose:
config = strip_default_ids(config)
output = yaml_util.dump(config, args.show_secrets)
@@ -515,7 +455,7 @@ def command_config(args: ArgsProtocol, config: ConfigType) -> int | None:
return 0
def command_vscode(args: ArgsProtocol) -> int | None:
def command_vscode(args):
from esphome import vscode
logging.disable(logging.INFO)
@@ -523,14 +463,7 @@ def command_vscode(args: ArgsProtocol) -> int | None:
vscode.read_config(args)
def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
# Set memory analysis options in config
if args.analyze_memory:
config.setdefault(CONF_ESPHOME, {})["analyze_memory"] = True
if args.memory_report:
config.setdefault(CONF_ESPHOME, {})["memory_report_file"] = args.memory_report
def command_compile(args, config):
exit_code = write_cpp(config)
if exit_code != 0:
return exit_code
@@ -544,9 +477,8 @@ def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
return 0
def command_upload(args: ArgsProtocol, config: ConfigType) -> int | None:
# Get devices, resolving special identifiers like OTA
devices = choose_upload_log_host(
def command_upload(args, config):
port = choose_upload_log_host(
default=args.device,
check_default=None,
show_ota=True,
@@ -554,22 +486,14 @@ def command_upload(args: ArgsProtocol, config: ConfigType) -> int | None:
show_api=False,
purpose="uploading",
)
# Try each device until one succeeds
exit_code = 1
for device in devices:
_LOGGER.info("Uploading to %s", device)
exit_code = upload_program(config, args, device)
if exit_code == 0:
_LOGGER.info("Successfully uploaded program.")
return 0
if len(devices) > 1:
_LOGGER.warning("Failed to upload to %s", device)
return exit_code
exit_code = upload_program(config, args, port)
if exit_code != 0:
return exit_code
_LOGGER.info("Successfully uploaded program.")
return 0
def command_discover(args: ArgsProtocol, config: ConfigType) -> int | None:
def command_discover(args, config):
if "mqtt" in config:
from esphome import mqtt
@@ -578,9 +502,8 @@ def command_discover(args: ArgsProtocol, config: ConfigType) -> int | None:
raise EsphomeError("No discover method configured (mqtt)")
def command_logs(args: ArgsProtocol, config: ConfigType) -> int | None:
# Get devices, resolving special identifiers like OTA
devices = choose_upload_log_host(
def command_logs(args, config):
port = choose_upload_log_host(
default=args.device,
check_default=None,
show_ota=False,
@@ -588,10 +511,10 @@ def command_logs(args: ArgsProtocol, config: ConfigType) -> int | None:
show_api=True,
purpose="logging",
)
return show_logs(config, args, devices)
return show_logs(config, args, port)
def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
def command_run(args, config):
exit_code = write_cpp(config)
if exit_code != 0:
return exit_code
@@ -608,8 +531,7 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
program_path = idedata.raw["prog_path"]
return run_external_process(program_path)
# Get devices, resolving special identifiers like OTA
devices = choose_upload_log_host(
port = choose_upload_log_host(
default=args.device,
check_default=None,
show_ota=True,
@@ -617,53 +539,39 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
show_api=True,
purpose="uploading",
)
# Try each device for upload until one succeeds
successful_device: str | None = None
for device in devices:
_LOGGER.info("Uploading to %s", device)
exit_code = upload_program(config, args, device)
if exit_code == 0:
_LOGGER.info("Successfully uploaded program.")
successful_device = device
break
if len(devices) > 1:
_LOGGER.warning("Failed to upload to %s", device)
if successful_device is None:
exit_code = upload_program(config, args, port)
if exit_code != 0:
return exit_code
_LOGGER.info("Successfully uploaded program.")
if args.no_logs:
return 0
# For logs, prefer the device we successfully uploaded to
devices = choose_upload_log_host(
default=successful_device,
check_default=successful_device,
port = choose_upload_log_host(
default=args.device,
check_default=port,
show_ota=False,
show_mqtt=True,
show_api=True,
purpose="logging",
)
return show_logs(config, args, devices)
return show_logs(config, args, port)
def command_clean_mqtt(args: ArgsProtocol, config: ConfigType) -> int | None:
def command_clean_mqtt(args, config):
return clean_mqtt(config, args)
def command_mqtt_fingerprint(args: ArgsProtocol, config: ConfigType) -> int | None:
def command_mqtt_fingerprint(args, config):
from esphome import mqtt
return mqtt.get_fingerprint(config)
def command_version(args: ArgsProtocol) -> int | None:
def command_version(args):
safe_print(f"Version: {const.__version__}")
return 0
def command_clean(args: ArgsProtocol, config: ConfigType) -> int | None:
def command_clean(args, config):
try:
writer.clean_build()
except OSError as err:
@@ -673,13 +581,13 @@ def command_clean(args: ArgsProtocol, config: ConfigType) -> int | None:
return 0
def command_dashboard(args: ArgsProtocol) -> int | None:
def command_dashboard(args):
from esphome.dashboard import dashboard
return dashboard.start_dashboard(args)
def command_update_all(args: ArgsProtocol) -> int | None:
def command_update_all(args):
import click
success = {}
@@ -726,7 +634,7 @@ def command_update_all(args: ArgsProtocol) -> int | None:
return failed
def command_idedata(args: ArgsProtocol, config: ConfigType) -> int:
def command_idedata(args, config):
import json
from esphome import platformio_api
@@ -742,7 +650,7 @@ def command_idedata(args: ArgsProtocol, config: ConfigType) -> int:
return 0
def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None:
def command_rename(args, config):
for c in args.name:
if c not in ALLOWED_NAME_CHARS:
print(
@@ -859,12 +767,6 @@ POST_CONFIG_ACTIONS = {
"discover": command_discover,
}
SIMPLE_CONFIG_ACTIONS = [
"clean",
"clean-mqtt",
"config",
]
def parse_args(argv):
options_parser = argparse.ArgumentParser(add_help=False)
@@ -941,17 +843,6 @@ def parse_args(argv):
help="Only generate source code, do not compile.",
action="store_true",
)
parser_compile.add_argument(
"--analyze-memory",
help="Analyze and display memory usage by component after compilation.",
action="store_true",
)
parser_compile.add_argument(
"--memory-report",
help="Save memory analysis report to a file (supports .json or .txt).",
type=str,
metavar="FILE",
)
parser_upload = subparsers.add_parser(
"upload",
@@ -963,8 +854,7 @@ def parse_args(argv):
)
parser_upload.add_argument(
"--device",
action="append",
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.",
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.",
)
parser_upload.add_argument(
"--upload_speed",
@@ -986,8 +876,7 @@ def parse_args(argv):
)
parser_logs.add_argument(
"--device",
action="append",
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.",
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.",
)
parser_logs.add_argument(
"--reset",
@@ -1016,8 +905,7 @@ def parse_args(argv):
)
parser_run.add_argument(
"--device",
action="append",
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.",
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.",
)
parser_run.add_argument(
"--upload_speed",
@@ -1144,13 +1032,6 @@ def parse_args(argv):
arguments = argv[1:]
argcomplete.autocomplete(parser)
if len(arguments) > 0 and arguments[0] in SIMPLE_CONFIG_ACTIONS:
args, unknown_args = parser.parse_known_args(arguments)
if unknown_args:
_LOGGER.warning("Ignored unrecognized arguments: %s", unknown_args)
return args
return parser.parse_args(arguments)

File diff suppressed because it is too large Load Diff

View File

@@ -29,7 +29,7 @@ from esphome.core import CORE, coroutine_with_priority
DOMAIN = "api"
DEPENDENCIES = ["network"]
AUTO_LOAD = ["socket"]
CODEOWNERS = ["@esphome/core"]
CODEOWNERS = ["@OttoWinter"]
api_ns = cg.esphome_ns.namespace("api")
APIServer = api_ns.class_("APIServer", cg.Component, cg.Controller)

View File

@@ -250,8 +250,8 @@ message DeviceInfoResponse {
// Supports receiving and saving api encryption key
bool api_encryption_supported = 19 [(field_ifdef) = "USE_API_NOISE"];
repeated DeviceInfo devices = 20 [(field_ifdef) = "USE_DEVICES", (fixed_array_size_define) = "ESPHOME_DEVICE_COUNT"];
repeated AreaInfo areas = 21 [(field_ifdef) = "USE_AREAS", (fixed_array_size_define) = "ESPHOME_AREA_COUNT"];
repeated DeviceInfo devices = 20 [(field_ifdef) = "USE_DEVICES"];
repeated AreaInfo areas = 21 [(field_ifdef) = "USE_AREAS"];
// Top-level area info to phase out suggested_area
AreaInfo area = 22 [(field_ifdef) = "USE_AREAS"];

View File

@@ -1462,22 +1462,18 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
resp.api_encryption_supported = true;
#endif
#ifdef USE_DEVICES
size_t device_index = 0;
for (auto const &device : App.get_devices()) {
if (device_index >= ESPHOME_DEVICE_COUNT)
break;
auto &device_info = resp.devices[device_index++];
resp.devices.emplace_back();
auto &device_info = resp.devices.back();
device_info.device_id = device->get_device_id();
device_info.set_name(StringRef(device->get_name()));
device_info.area_id = device->get_area_id();
}
#endif
#ifdef USE_AREAS
size_t area_index = 0;
for (auto const &area : App.get_areas()) {
if (area_index >= ESPHOME_AREA_COUNT)
break;
auto &area_info = resp.areas[area_index++];
resp.areas.emplace_back();
auto &area_info = resp.areas.back();
area_info.area_id = area->get_area_id();
area_info.set_name(StringRef(area->get_name()));
}

View File

@@ -703,16 +703,10 @@ class APIConnection : public APIServerConnection {
bool send_message_smart_(EntityBase *entity, MessageCreatorPtr creator, uint8_t message_type,
uint8_t estimated_size) {
// Try to send immediately if:
// 1. It's an UpdateStateResponse (always send immediately to handle cases where
// the main loop is blocked, e.g., during OTA updates)
// 2. OR: We should try to send immediately (should_try_send_immediately = true)
// AND Batch delay is 0 (user has opted in to immediate sending)
// 3. AND: Buffer has space available
if ((
#ifdef USE_UPDATE
message_type == UpdateStateResponse::MESSAGE_TYPE ||
#endif
(this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0)) &&
// 1. We should try to send immediately (should_try_send_immediately = true)
// 2. Batch delay is 0 (user has opted in to immediate sending)
// 3. Buffer has space available
if (this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0 &&
this->helper_->can_write_without_blocking()) {
// Now actually encode and send
if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true) &&

View File

@@ -115,12 +115,12 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_bool(19, this->api_encryption_supported);
#endif
#ifdef USE_DEVICES
for (const auto &it : this->devices) {
for (auto &it : this->devices) {
buffer.encode_message(20, it, true);
}
#endif
#ifdef USE_AREAS
for (const auto &it : this->areas) {
for (auto &it : this->areas) {
buffer.encode_message(21, it, true);
}
#endif
@@ -167,14 +167,10 @@ void DeviceInfoResponse::calculate_size(ProtoSize &size) const {
size.add_bool(2, this->api_encryption_supported);
#endif
#ifdef USE_DEVICES
for (const auto &it : this->devices) {
size.add_message_object_force(2, it);
}
size.add_repeated_message(2, this->devices);
#endif
#ifdef USE_AREAS
for (const auto &it : this->areas) {
size.add_message_object_force(2, it);
}
size.add_repeated_message(2, this->areas);
#endif
#ifdef USE_AREAS
size.add_message_object(2, this->area);

View File

@@ -490,7 +490,7 @@ class DeviceInfo : public ProtoMessage {
class DeviceInfoResponse : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 10;
static constexpr uint8_t ESTIMATED_SIZE = 247;
static constexpr uint8_t ESTIMATED_SIZE = 211;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "device_info_response"; }
#endif
@@ -543,10 +543,10 @@ class DeviceInfoResponse : public ProtoMessage {
bool api_encryption_supported{false};
#endif
#ifdef USE_DEVICES
std::array<DeviceInfo, ESPHOME_DEVICE_COUNT> devices{};
std::vector<DeviceInfo> devices{};
#endif
#ifdef USE_AREAS
std::array<AreaInfo, ESPHOME_AREA_COUNT> areas{};
std::vector<AreaInfo> areas{};
#endif
#ifdef USE_AREAS
AreaInfo area{};

View File

@@ -30,7 +30,7 @@ if TYPE_CHECKING:
_LOGGER = logging.getLogger(__name__)
async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None:
async def async_run_logs(config: dict[str, Any], address: str) -> None:
"""Run the logs command in the event loop."""
conf = config["api"]
name = config["esphome"]["name"]
@@ -39,21 +39,13 @@ async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None:
noise_psk: str | None = None
if (encryption := conf.get(CONF_ENCRYPTION)) and (key := encryption.get(CONF_KEY)):
noise_psk = key
if len(addresses) == 1:
_LOGGER.info("Starting log output from %s using esphome API", addresses[0])
else:
_LOGGER.info(
"Starting log output from %s using esphome API", " or ".join(addresses)
)
_LOGGER.info("Starting log output from %s using esphome API", address)
cli = APIClient(
addresses[0], # Primary address for compatibility
address,
port,
password,
client_info=f"ESPHome Logs {__version__}",
noise_psk=noise_psk,
addresses=addresses, # Pass all addresses for automatic retry
)
dashboard = CORE.dashboard
@@ -74,7 +66,7 @@ async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None:
await stop()
def run_logs(config: dict[str, Any], addresses: list[str]) -> None:
def run_logs(config: dict[str, Any], address: str) -> None:
"""Run the logs command."""
with contextlib.suppress(KeyboardInterrupt):
asyncio.run(async_run_logs(config, addresses))
asyncio.run(async_run_logs(config, address))

View File

@@ -56,14 +56,6 @@ class CustomAPIDevice {
auto *service = new CustomAPIDeviceService<T, Ts...>(name, arg_names, (T *) this, callback); // NOLINT
global_api_server->register_user_service(service);
}
#else
template<typename T, typename... Ts>
void register_service(void (T::*callback)(Ts...), const std::string &name,
const std::array<std::string, sizeof...(Ts)> &arg_names) {
static_assert(
sizeof(T) == 0,
"register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration");
}
#endif
/** Register a custom native API service that will show up in Home Assistant.
@@ -89,12 +81,6 @@ class CustomAPIDevice {
auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback); // NOLINT
global_api_server->register_user_service(service);
}
#else
template<typename T> void register_service(void (T::*callback)(), const std::string &name) {
static_assert(
sizeof(T) == 0,
"register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration");
}
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
@@ -149,22 +135,6 @@ class CustomAPIDevice {
auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1);
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), f);
}
#else
template<typename T>
void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id,
const std::string &attribute = "") {
static_assert(sizeof(T) == 0,
"subscribe_homeassistant_state() requires 'homeassistant_states: true' in the 'api:' section "
"of your YAML configuration");
}
template<typename T>
void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id,
const std::string &attribute = "") {
static_assert(sizeof(T) == 0,
"subscribe_homeassistant_state() requires 'homeassistant_states: true' in the 'api:' section "
"of your YAML configuration");
}
#endif
#ifdef USE_API_HOMEASSISTANT_SERVICES
@@ -252,28 +222,6 @@ class CustomAPIDevice {
}
global_api_server->send_homeassistant_service_call(resp);
}
#else
template<typename T = void> void call_homeassistant_service(const std::string &service_name) {
static_assert(sizeof(T) == 0, "call_homeassistant_service() requires 'homeassistant_services: true' in the 'api:' "
"section of your YAML configuration");
}
template<typename T = void>
void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) {
static_assert(sizeof(T) == 0, "call_homeassistant_service() requires 'homeassistant_services: true' in the 'api:' "
"section of your YAML configuration");
}
template<typename T = void> void fire_homeassistant_event(const std::string &event_name) {
static_assert(sizeof(T) == 0, "fire_homeassistant_event() requires 'homeassistant_services: true' in the 'api:' "
"section of your YAML configuration");
}
template<typename T = void>
void fire_homeassistant_event(const std::string &service_name, const std::map<std::string, std::string> &data) {
static_assert(sizeof(T) == 0, "fire_homeassistant_event() requires 'homeassistant_services: true' in the 'api:' "
"section of your YAML configuration");
}
#endif
};

View File

@@ -10,7 +10,7 @@ from esphome.const import (
)
from esphome.core import CORE, coroutine_with_priority
CODEOWNERS = ["@esphome/core"]
CODEOWNERS = ["@OttoWinter"]
CONFIG_SCHEMA = cv.All(
cv.Schema({}),

View File

@@ -1,19 +1,13 @@
import logging
import esphome.codegen as cg
from esphome.components import esp32_ble, esp32_ble_client, esp32_ble_tracker
from esphome.components.esp32 import add_idf_sdkconfig_option
from esphome.components.esp32_ble import BTLoggers
import esphome.config_validation as cv
from esphome.const import CONF_ACTIVE, CONF_ID
from esphome.core import CORE
from esphome.log import AnsiFore, color
AUTO_LOAD = ["esp32_ble_client", "esp32_ble_tracker"]
DEPENDENCIES = ["api", "esp32"]
CODEOWNERS = ["@jesserockz", "@bdraco"]
_LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@jesserockz"]
CONF_CONNECTION_SLOTS = "connection_slots"
CONF_CACHE_SERVICES = "cache_services"
@@ -47,27 +41,6 @@ def validate_connections(config):
esp32_ble_tracker.consume_connection_slots(connection_slots, "bluetooth_proxy")(
config
)
# Warn about connection slot waste when using Arduino framework
if CORE.using_arduino and connection_slots:
_LOGGER.warning(
"Bluetooth Proxy with active connections on Arduino framework has suboptimal performance.\n"
"If BLE connections fail, they can waste connection slots for 10 seconds because\n"
"Arduino doesn't allow configuring the BLE connection timeout (fixed at 30s).\n"
"ESP-IDF framework allows setting it to 20s to match client timeouts.\n"
"\n"
"To switch to ESP-IDF, add this to your YAML:\n"
" esp32:\n"
" framework:\n"
" type: esp-idf\n"
"\n"
"For detailed migration instructions, see:\n"
"%s",
color(
AnsiFore.BLUE, "https://esphome.io/guides/esp32_arduino_to_idf.html"
),
)
return {
**config,
CONF_CONNECTIONS: [CONNECTION_SCHEMA({}) for _ in range(connection_slots)],

View File

@@ -80,11 +80,9 @@ void BluetoothConnection::dump_config() {
void BluetoothConnection::update_allocated_slot_(uint64_t find_value, uint64_t set_value) {
auto &allocated = this->proxy_->connections_free_response_.allocated;
for (auto &slot : allocated) {
if (slot == find_value) {
slot = set_value;
return;
}
auto *it = std::find(allocated.begin(), allocated.end(), find_value);
if (it != allocated.end()) {
*it = set_value;
}
}
@@ -187,7 +185,8 @@ void BluetoothConnection::send_service_for_discovery_() {
service_result.start_handle, service_result.end_handle, 0, &total_char_count);
if (char_count_status != ESP_GATT_OK) {
this->log_connection_error_("esp_ble_gattc_get_attr_count", char_count_status);
ESP_LOGE(TAG, "[%d] [%s] Error getting characteristic count, status=%d", this->connection_index_,
this->address_str().c_str(), char_count_status);
this->send_service_ = DONE_SENDING_SERVICES;
return;
}
@@ -221,7 +220,8 @@ void BluetoothConnection::send_service_for_discovery_() {
break;
}
if (char_status != ESP_GATT_OK) {
this->log_connection_error_("esp_ble_gattc_get_all_char", char_status);
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", this->connection_index_,
this->address_str().c_str(), char_status);
this->send_service_ = DONE_SENDING_SERVICES;
return;
}
@@ -244,7 +244,8 @@ void BluetoothConnection::send_service_for_discovery_() {
this->gattc_if_, this->conn_id_, ESP_GATT_DB_DESCRIPTOR, 0, 0, char_result.char_handle, &total_desc_count);
if (desc_count_status != ESP_GATT_OK) {
this->log_connection_error_("esp_ble_gattc_get_attr_count", desc_count_status);
ESP_LOGE(TAG, "[%d] [%s] Error getting descriptor count for char handle %d, status=%d",
this->connection_index_, this->address_str().c_str(), char_result.char_handle, desc_count_status);
this->send_service_ = DONE_SENDING_SERVICES;
return;
}
@@ -265,7 +266,8 @@ void BluetoothConnection::send_service_for_discovery_() {
break;
}
if (desc_status != ESP_GATT_OK) {
this->log_connection_error_("esp_ble_gattc_get_all_descr", desc_status);
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_descr error, status=%d", this->connection_index_,
this->address_str().c_str(), desc_status);
this->send_service_ = DONE_SENDING_SERVICES;
return;
}
@@ -319,33 +321,6 @@ void BluetoothConnection::send_service_for_discovery_() {
api_conn->send_message(resp, api::BluetoothGATTGetServicesResponse::MESSAGE_TYPE);
}
void BluetoothConnection::log_connection_error_(const char *operation, esp_gatt_status_t status) {
ESP_LOGE(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str().c_str(), operation,
status);
}
void BluetoothConnection::log_connection_warning_(const char *operation, esp_err_t err) {
ESP_LOGW(TAG, "[%d] [%s] %s failed, err=%d", this->connection_index_, this->address_str().c_str(), operation, err);
}
void BluetoothConnection::log_gatt_not_connected_(const char *action, const char *type) {
ESP_LOGW(TAG, "[%d] [%s] Cannot %s GATT %s, not connected.", this->connection_index_, this->address_str().c_str(),
action, type);
}
void BluetoothConnection::log_gatt_operation_error_(const char *operation, uint16_t handle, esp_gatt_status_t status) {
ESP_LOGW(TAG, "[%d] [%s] Error %s for handle 0x%2X, status=%d", this->connection_index_, this->address_str().c_str(),
operation, handle, status);
}
esp_err_t BluetoothConnection::check_and_log_error_(const char *operation, esp_err_t err) {
if (err != ESP_OK) {
this->log_connection_warning_(operation, err);
return err;
}
return ESP_OK;
}
bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) {
if (!BLEClientBase::gattc_event_handler(event, gattc_if, param))
@@ -386,7 +361,8 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
case ESP_GATTC_READ_DESCR_EVT:
case ESP_GATTC_READ_CHAR_EVT: {
if (param->read.status != ESP_GATT_OK) {
this->log_gatt_operation_error_("reading char/descriptor", param->read.handle, param->read.status);
ESP_LOGW(TAG, "[%d] [%s] Error reading char/descriptor at handle 0x%2X, status=%d", this->connection_index_,
this->address_str_.c_str(), param->read.handle, param->read.status);
this->proxy_->send_gatt_error(this->address_, param->read.handle, param->read.status);
break;
}
@@ -400,7 +376,8 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
case ESP_GATTC_WRITE_CHAR_EVT:
case ESP_GATTC_WRITE_DESCR_EVT: {
if (param->write.status != ESP_GATT_OK) {
this->log_gatt_operation_error_("writing char/descriptor", param->write.handle, param->write.status);
ESP_LOGW(TAG, "[%d] [%s] Error writing char/descriptor at handle 0x%2X, status=%d", this->connection_index_,
this->address_str_.c_str(), param->write.handle, param->write.status);
this->proxy_->send_gatt_error(this->address_, param->write.handle, param->write.status);
break;
}
@@ -412,8 +389,9 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
}
case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: {
if (param->unreg_for_notify.status != ESP_GATT_OK) {
this->log_gatt_operation_error_("unregistering notifications", param->unreg_for_notify.handle,
param->unreg_for_notify.status);
ESP_LOGW(TAG, "[%d] [%s] Error unregistering notifications for handle 0x%2X, status=%d",
this->connection_index_, this->address_str_.c_str(), param->unreg_for_notify.handle,
param->unreg_for_notify.status);
this->proxy_->send_gatt_error(this->address_, param->unreg_for_notify.handle, param->unreg_for_notify.status);
break;
}
@@ -425,8 +403,8 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
}
case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
if (param->reg_for_notify.status != ESP_GATT_OK) {
this->log_gatt_operation_error_("registering notifications", param->reg_for_notify.handle,
param->reg_for_notify.status);
ESP_LOGW(TAG, "[%d] [%s] Error registering notifications for handle 0x%2X, status=%d", this->connection_index_,
this->address_str_.c_str(), param->reg_for_notify.handle, param->reg_for_notify.status);
this->proxy_->send_gatt_error(this->address_, param->reg_for_notify.handle, param->reg_for_notify.status);
break;
}
@@ -472,7 +450,8 @@ void BluetoothConnection::gap_event_handler(esp_gap_ble_cb_event_t event, esp_bl
esp_err_t BluetoothConnection::read_characteristic(uint16_t handle) {
if (!this->connected()) {
this->log_gatt_not_connected_("read", "characteristic");
ESP_LOGW(TAG, "[%d] [%s] Cannot read GATT characteristic, not connected.", this->connection_index_,
this->address_str_.c_str());
return ESP_GATT_NOT_CONNECTED;
}
@@ -480,12 +459,18 @@ esp_err_t BluetoothConnection::read_characteristic(uint16_t handle) {
handle);
esp_err_t err = esp_ble_gattc_read_char(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE);
return this->check_and_log_error_("esp_ble_gattc_read_char", err);
if (err != ERR_OK) {
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_read_char error, err=%d", this->connection_index_,
this->address_str_.c_str(), err);
return err;
}
return ESP_OK;
}
esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const std::string &data, bool response) {
if (!this->connected()) {
this->log_gatt_not_connected_("write", "characteristic");
ESP_LOGW(TAG, "[%d] [%s] Cannot write GATT characteristic, not connected.", this->connection_index_,
this->address_str_.c_str());
return ESP_GATT_NOT_CONNECTED;
}
ESP_LOGV(TAG, "[%d] [%s] Writing GATT characteristic handle %d", this->connection_index_, this->address_str_.c_str(),
@@ -494,24 +479,36 @@ esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const std::
esp_err_t err =
esp_ble_gattc_write_char(this->gattc_if_, this->conn_id_, handle, data.size(), (uint8_t *) data.data(),
response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
return this->check_and_log_error_("esp_ble_gattc_write_char", err);
if (err != ERR_OK) {
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_write_char error, err=%d", this->connection_index_,
this->address_str_.c_str(), err);
return err;
}
return ESP_OK;
}
esp_err_t BluetoothConnection::read_descriptor(uint16_t handle) {
if (!this->connected()) {
this->log_gatt_not_connected_("read", "descriptor");
ESP_LOGW(TAG, "[%d] [%s] Cannot read GATT descriptor, not connected.", this->connection_index_,
this->address_str_.c_str());
return ESP_GATT_NOT_CONNECTED;
}
ESP_LOGV(TAG, "[%d] [%s] Reading GATT descriptor handle %d", this->connection_index_, this->address_str_.c_str(),
handle);
esp_err_t err = esp_ble_gattc_read_char_descr(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE);
return this->check_and_log_error_("esp_ble_gattc_read_char_descr", err);
if (err != ERR_OK) {
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_read_char_descr error, err=%d", this->connection_index_,
this->address_str_.c_str(), err);
return err;
}
return ESP_OK;
}
esp_err_t BluetoothConnection::write_descriptor(uint16_t handle, const std::string &data, bool response) {
if (!this->connected()) {
this->log_gatt_not_connected_("write", "descriptor");
ESP_LOGW(TAG, "[%d] [%s] Cannot write GATT descriptor, not connected.", this->connection_index_,
this->address_str_.c_str());
return ESP_GATT_NOT_CONNECTED;
}
ESP_LOGV(TAG, "[%d] [%s] Writing GATT descriptor handle %d", this->connection_index_, this->address_str_.c_str(),
@@ -520,12 +517,18 @@ esp_err_t BluetoothConnection::write_descriptor(uint16_t handle, const std::stri
esp_err_t err = esp_ble_gattc_write_char_descr(
this->gattc_if_, this->conn_id_, handle, data.size(), (uint8_t *) data.data(),
response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
return this->check_and_log_error_("esp_ble_gattc_write_char_descr", err);
if (err != ERR_OK) {
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_write_char_descr error, err=%d", this->connection_index_,
this->address_str_.c_str(), err);
return err;
}
return ESP_OK;
}
esp_err_t BluetoothConnection::notify_characteristic(uint16_t handle, bool enable) {
if (!this->connected()) {
this->log_gatt_not_connected_("notify", "characteristic");
ESP_LOGW(TAG, "[%d] [%s] Cannot notify GATT characteristic, not connected.", this->connection_index_,
this->address_str_.c_str());
return ESP_GATT_NOT_CONNECTED;
}
@@ -533,13 +536,22 @@ esp_err_t BluetoothConnection::notify_characteristic(uint16_t handle, bool enabl
ESP_LOGV(TAG, "[%d] [%s] Registering for GATT characteristic notifications handle %d", this->connection_index_,
this->address_str_.c_str(), handle);
esp_err_t err = esp_ble_gattc_register_for_notify(this->gattc_if_, this->remote_bda_, handle);
return this->check_and_log_error_("esp_ble_gattc_register_for_notify", err);
if (err != ESP_OK) {
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_register_for_notify failed, err=%d", this->connection_index_,
this->address_str_.c_str(), err);
return err;
}
} else {
ESP_LOGV(TAG, "[%d] [%s] Unregistering for GATT characteristic notifications handle %d", this->connection_index_,
this->address_str_.c_str(), handle);
esp_err_t err = esp_ble_gattc_unregister_for_notify(this->gattc_if_, this->remote_bda_, handle);
if (err != ESP_OK) {
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_unregister_for_notify failed, err=%d", this->connection_index_,
this->address_str_.c_str(), err);
return err;
}
}
ESP_LOGV(TAG, "[%d] [%s] Unregistering for GATT characteristic notifications handle %d", this->connection_index_,
this->address_str_.c_str(), handle);
esp_err_t err = esp_ble_gattc_unregister_for_notify(this->gattc_if_, this->remote_bda_, handle);
return this->check_and_log_error_("esp_ble_gattc_unregister_for_notify", err);
return ESP_OK;
}
esp32_ble_tracker::AdvertisementParserType BluetoothConnection::get_advertisement_parser_type() {

View File

@@ -33,11 +33,6 @@ class BluetoothConnection : public esp32_ble_client::BLEClientBase {
void send_service_for_discovery_();
void reset_connection_(esp_err_t reason);
void update_allocated_slot_(uint64_t find_value, uint64_t set_value);
void log_connection_error_(const char *operation, esp_gatt_status_t status);
void log_connection_warning_(const char *operation, esp_err_t err);
void log_gatt_not_connected_(const char *action, const char *type);
void log_gatt_operation_error_(const char *operation, uint16_t handle, esp_gatt_status_t status);
esp_err_t check_and_log_error_(const char *operation, esp_err_t err);
// Memory optimized layout for 32-bit systems
// Group 1: Pointers (4 bytes each, naturally aligned)

View File

@@ -35,8 +35,8 @@ void BluetoothProxy::setup() {
// 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 = BLUETOOTH_PROXY_MAX_CONNECTIONS;
this->connections_free_response_.free = BLUETOOTH_PROXY_MAX_CONNECTIONS;
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) {
@@ -53,26 +53,6 @@ void BluetoothProxy::send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerSta
this->api_connection_->send_message(resp, api::BluetoothScannerStateResponse::MESSAGE_TYPE);
}
void BluetoothProxy::log_connection_request_ignored_(BluetoothConnection *connection, espbt::ClientState state) {
ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, state: %s", connection->get_connection_index(),
connection->address_str().c_str(), espbt::client_state_to_string(state));
}
void BluetoothProxy::log_connection_info_(BluetoothConnection *connection, const char *message) {
ESP_LOGI(TAG, "[%d] [%s] Connecting %s", connection->get_connection_index(), connection->address_str().c_str(),
message);
}
void BluetoothProxy::log_not_connected_gatt_(const char *action, const char *type) {
ESP_LOGW(TAG, "Cannot %s GATT %s, not connected", action, type);
}
void BluetoothProxy::handle_gatt_not_connected_(uint64_t address, uint16_t handle, const char *action,
const char *type) {
this->log_not_connected_gatt_(action, type);
this->send_gatt_error(address, handle, ESP_GATT_NOT_CONNECTED);
}
#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
@@ -154,13 +134,12 @@ void BluetoothProxy::dump_config() {
ESP_LOGCONFIG(TAG,
" Active: %s\n"
" Connections: %d",
YESNO(this->active_), this->connection_count_);
YESNO(this->active_), this->connections_.size());
}
void BluetoothProxy::loop() {
if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr) {
for (uint8_t i = 0; i < this->connection_count_; i++) {
auto *connection = this->connections_[i];
for (auto *connection : this->connections_) {
if (connection->get_address() != 0 && !connection->disconnect_pending()) {
connection->disconnect();
}
@@ -183,8 +162,7 @@ esp32_ble_tracker::AdvertisementParserType BluetoothProxy::get_advertisement_par
}
BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool reserve) {
for (uint8_t i = 0; i < this->connection_count_; i++) {
auto *connection = this->connections_[i];
for (auto *connection : this->connections_) {
if (connection->get_address() == address)
return connection;
}
@@ -192,8 +170,7 @@ BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool rese
if (!reserve)
return nullptr;
for (uint8_t i = 0; i < this->connection_count_; i++) {
auto *connection = this->connections_[i];
for (auto *connection : this->connections_) {
if (connection->get_address() == 0) {
connection->send_service_ = DONE_SENDING_SERVICES;
connection->set_address(address);
@@ -222,10 +199,23 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
}
if (connection->state() == espbt::ClientState::CONNECTED ||
connection->state() == espbt::ClientState::ESTABLISHED) {
this->log_connection_request_ignored_(connection, connection->state());
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",
@@ -233,21 +223,29 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
connection->cancel_pending_disconnect();
return;
}
this->log_connection_request_ignored_(connection, connection->state());
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) {
this->log_connection_request_ignored_(connection, connection->state());
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);
this->log_connection_info_(connection, "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);
this->log_connection_info_(connection, "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);
this->log_connection_info_(connection, "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_);
@@ -315,7 +313,8 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
void BluetoothProxy::bluetooth_gatt_read(const api::BluetoothGATTReadRequest &msg) {
auto *connection = this->get_connection_(msg.address, false);
if (connection == nullptr) {
this->handle_gatt_not_connected_(msg.address, msg.handle, "read", "characteristic");
ESP_LOGW(TAG, "Cannot read GATT characteristic, not connected");
this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
return;
}
@@ -328,7 +327,8 @@ void BluetoothProxy::bluetooth_gatt_read(const api::BluetoothGATTReadRequest &ms
void BluetoothProxy::bluetooth_gatt_write(const api::BluetoothGATTWriteRequest &msg) {
auto *connection = this->get_connection_(msg.address, false);
if (connection == nullptr) {
this->handle_gatt_not_connected_(msg.address, msg.handle, "write", "characteristic");
ESP_LOGW(TAG, "Cannot write GATT characteristic, not connected");
this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
return;
}
@@ -341,7 +341,8 @@ void BluetoothProxy::bluetooth_gatt_write(const api::BluetoothGATTWriteRequest &
void BluetoothProxy::bluetooth_gatt_read_descriptor(const api::BluetoothGATTReadDescriptorRequest &msg) {
auto *connection = this->get_connection_(msg.address, false);
if (connection == nullptr) {
this->handle_gatt_not_connected_(msg.address, msg.handle, "read", "descriptor");
ESP_LOGW(TAG, "Cannot read GATT descriptor, not connected");
this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
return;
}
@@ -354,7 +355,8 @@ void BluetoothProxy::bluetooth_gatt_read_descriptor(const api::BluetoothGATTRead
void BluetoothProxy::bluetooth_gatt_write_descriptor(const api::BluetoothGATTWriteDescriptorRequest &msg) {
auto *connection = this->get_connection_(msg.address, false);
if (connection == nullptr) {
this->handle_gatt_not_connected_(msg.address, msg.handle, "write", "descriptor");
ESP_LOGW(TAG, "Cannot write GATT descriptor, not connected");
this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
return;
}
@@ -367,7 +369,8 @@ void BluetoothProxy::bluetooth_gatt_write_descriptor(const api::BluetoothGATTWri
void BluetoothProxy::bluetooth_gatt_send_services(const api::BluetoothGATTGetServicesRequest &msg) {
auto *connection = this->get_connection_(msg.address, false);
if (connection == nullptr || !connection->connected()) {
this->handle_gatt_not_connected_(msg.address, 0, "get", "services");
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_) {
@@ -383,7 +386,8 @@ void BluetoothProxy::bluetooth_gatt_send_services(const api::BluetoothGATTGetSer
void BluetoothProxy::bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest &msg) {
auto *connection = this->get_connection_(msg.address, false);
if (connection == nullptr) {
this->handle_gatt_not_connected_(msg.address, msg.handle, "notify", "characteristic");
ESP_LOGW(TAG, "Cannot notify GATT characteristic, not connected");
this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
return;
}

View File

@@ -2,7 +2,6 @@
#ifdef USE_ESP32
#include <array>
#include <map>
#include <vector>
@@ -64,10 +63,8 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override;
void register_connection(BluetoothConnection *connection) {
if (this->connection_count_ < BLUETOOTH_PROXY_MAX_CONNECTIONS) {
this->connections_[this->connection_count_++] = connection;
connection->proxy_ = this;
}
this->connections_.push_back(connection);
connection->proxy_ = this;
}
void bluetooth_device_request(const api::BluetoothDeviceRequest &msg);
@@ -136,17 +133,13 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
void send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerState state);
BluetoothConnection *get_connection_(uint64_t address, bool reserve);
void log_connection_request_ignored_(BluetoothConnection *connection, espbt::ClientState state);
void log_connection_info_(BluetoothConnection *connection, const char *message);
void log_not_connected_gatt_(const char *action, const char *type);
void handle_gatt_not_connected_(uint64_t address, uint16_t handle, const char *action, const char *type);
// Memory optimized layout for 32-bit systems
// Group 1: Pointers (4 bytes each, naturally aligned)
api::APIConnection *api_connection_{nullptr};
// Group 2: Fixed-size array of connection pointers
std::array<BluetoothConnection *, BLUETOOTH_PROXY_MAX_CONNECTIONS> connections_{};
// Group 2: Container types (typically 12 bytes on 32-bit)
std::vector<BluetoothConnection *> connections_{};
// BLE advertisement batching
std::vector<api::BluetoothLERawAdvertisement> advertisement_pool_;
@@ -161,8 +154,7 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
// Group 4: 1-byte types grouped together
bool active_;
uint8_t advertisement_count_{0};
uint8_t connection_count_{0};
// 3 bytes used, 1 byte padding
// 2 bytes used, 2 bytes padding
};
extern BluetoothProxy *global_bluetooth_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)

View File

@@ -28,7 +28,7 @@ const float BME680_GAS_LOOKUP_TABLE_1[16] PROGMEM = {0.0, 0.0, 0.0, 0.0, 0.0,
const float BME680_GAS_LOOKUP_TABLE_2[16] PROGMEM = {0.0, 0.0, 0.0, 0.0, 0.1, 0.7, 0.0, -0.8,
-0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0};
[[maybe_unused]] static const char *oversampling_to_str(BME680Oversampling oversampling) {
static const char *oversampling_to_str(BME680Oversampling oversampling) {
switch (oversampling) {
case BME680_OVERSAMPLING_NONE:
return "None";
@@ -47,7 +47,7 @@ const float BME680_GAS_LOOKUP_TABLE_2[16] PROGMEM = {0.0, 0.0, 0.0, 0.0, 0.1, 0
}
}
[[maybe_unused]] static const char *iir_filter_to_str(BME680IIRFilter filter) {
static const char *iir_filter_to_str(BME680IIRFilter filter) {
switch (filter) {
case BME680_IIR_FILTER_OFF:
return "OFF";

View File

@@ -14,7 +14,7 @@ from esphome.core import CORE, coroutine_with_priority
AUTO_LOAD = ["web_server_base", "ota.web_server"]
DEPENDENCIES = ["wifi"]
CODEOWNERS = ["@esphome/core"]
CODEOWNERS = ["@OttoWinter"]
captive_portal_ns = cg.esphome_ns.namespace("captive_portal")
CaptivePortal = captive_portal_ns.class_("CaptivePortal", cg.Component)

View File

@@ -13,7 +13,7 @@ from esphome.const import (
)
from esphome.core import CORE
CODEOWNERS = ["@esphome/core"]
CODEOWNERS = ["@OttoWinter"]
DEPENDENCIES = ["logger"]
CONF_DEBUG_ID = "debug_id"

View File

@@ -1,5 +1,4 @@
#ifdef USE_ESP32
#include "driver/gpio.h"
#include "deep_sleep_component.h"
#include "esphome/core/log.h"
@@ -75,20 +74,11 @@ void DeepSleepComponent::deep_sleep_() {
if (this->sleep_duration_.has_value())
esp_sleep_enable_timer_wakeup(*this->sleep_duration_);
if (this->wakeup_pin_ != nullptr) {
const auto gpio_pin = gpio_num_t(this->wakeup_pin_->get_pin());
if (this->wakeup_pin_->get_flags() & gpio::FLAG_PULLUP) {
gpio_sleep_set_pull_mode(gpio_pin, GPIO_PULLUP_ONLY);
} else if (this->wakeup_pin_->get_flags() & gpio::FLAG_PULLDOWN) {
gpio_sleep_set_pull_mode(gpio_pin, GPIO_PULLDOWN_ONLY);
}
gpio_sleep_set_direction(gpio_pin, GPIO_MODE_INPUT);
gpio_hold_en(gpio_pin);
gpio_deep_sleep_hold_en();
bool level = !this->wakeup_pin_->is_inverted();
if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) {
level = !level;
}
esp_sleep_enable_ext0_wakeup(gpio_pin, level);
esp_sleep_enable_ext0_wakeup(gpio_num_t(this->wakeup_pin_->get_pin()), level);
}
if (this->ext1_wakeup_.has_value()) {
esp_sleep_enable_ext1_wakeup(this->ext1_wakeup_->mask, this->ext1_wakeup_->wakeup_mode);
@@ -112,15 +102,6 @@ void DeepSleepComponent::deep_sleep_() {
if (this->sleep_duration_.has_value())
esp_sleep_enable_timer_wakeup(*this->sleep_duration_);
if (this->wakeup_pin_ != nullptr) {
const auto gpio_pin = gpio_num_t(this->wakeup_pin_->get_pin());
if (this->wakeup_pin_->get_flags() && gpio::FLAG_PULLUP) {
gpio_sleep_set_pull_mode(gpio_pin, GPIO_PULLUP_ONLY);
} else if (this->wakeup_pin_->get_flags() && gpio::FLAG_PULLDOWN) {
gpio_sleep_set_pull_mode(gpio_pin, GPIO_PULLDOWN_ONLY);
}
gpio_sleep_set_direction(gpio_pin, GPIO_MODE_INPUT);
gpio_hold_en(gpio_pin);
gpio_deep_sleep_hold_en();
bool level = !this->wakeup_pin_->is_inverted();
if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) {
level = !level;

View File

@@ -680,64 +680,6 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All(
)
class _FrameworkMigrationWarning:
shown = False
def _show_framework_migration_message(name: str, variant: str) -> None:
"""Show a friendly message about framework migration when defaulting to Arduino."""
if _FrameworkMigrationWarning.shown:
return
_FrameworkMigrationWarning.shown = True
from esphome.log import AnsiFore, color
message = (
color(
AnsiFore.BOLD_CYAN,
f"💡 IMPORTANT: {name} doesn't have a framework specified!",
)
+ "\n\n"
+ f"Currently, {variant} defaults to the Arduino framework.\n"
+ color(AnsiFore.YELLOW, "This will change to ESP-IDF in ESPHome 2026.1.0.\n")
+ "\n"
+ "Note: Newer ESP32 variants (C6, H2, P4, etc.) already use ESP-IDF by default.\n"
+ "\n"
+ "Why change? ESP-IDF offers:\n"
+ color(AnsiFore.GREEN, " ✨ Up to 40% smaller binaries\n")
+ color(AnsiFore.GREEN, " 🚀 Better performance and optimization\n")
+ color(AnsiFore.GREEN, " 📦 Custom-built firmware for your exact needs\n")
+ color(
AnsiFore.GREEN,
" 🔧 Active development and testing by ESPHome developers\n",
)
+ "\n"
+ "Trade-offs:\n"
+ color(AnsiFore.YELLOW, " ⏱️ Compile times are ~25% longer\n")
+ color(AnsiFore.YELLOW, " 🔄 Some components need migration\n")
+ "\n"
+ "What should I do?\n"
+ color(AnsiFore.CYAN, " Option 1")
+ ": Migrate to ESP-IDF (recommended)\n"
+ " Add this to your YAML under 'esp32:':\n"
+ color(AnsiFore.WHITE, " framework:\n")
+ color(AnsiFore.WHITE, " type: esp-idf\n")
+ "\n"
+ color(AnsiFore.CYAN, " Option 2")
+ ": Keep using Arduino (still supported)\n"
+ " Add this to your YAML under 'esp32:':\n"
+ color(AnsiFore.WHITE, " framework:\n")
+ color(AnsiFore.WHITE, " type: arduino\n")
+ "\n"
+ "Need help? Check out the migration guide:\n"
+ color(
AnsiFore.BLUE,
"https://esphome.io/guides/esp32_arduino_to_idf.html",
)
)
_LOGGER.warning(message)
def _set_default_framework(config):
if CONF_FRAMEWORK not in config:
config = config.copy()
@@ -746,10 +688,6 @@ def _set_default_framework(config):
if variant in ARDUINO_ALLOWED_VARIANTS:
config[CONF_FRAMEWORK] = ARDUINO_FRAMEWORK_SCHEMA({})
config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ARDUINO
# Show the migration message
_show_framework_migration_message(
config.get(CONF_NAME, "This device"), variant
)
else:
config[CONF_FRAMEWORK] = ESP_IDF_FRAMEWORK_SCHEMA({})
config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ESP_IDF

View File

@@ -93,8 +93,8 @@ def merge_factory_bin(source, target, env):
"esptool",
"--chip",
chip,
"merge-bin",
"--flash-size",
"merge_bin",
"--flash_size",
flash_size,
"--output",
str(output_path),
@@ -110,7 +110,7 @@ def merge_factory_bin(source, target, env):
if result == 0:
print(f"Successfully created {output_path}")
else:
print(f"Error: esptool merge-bin failed with code {result}")
print(f"Error: esptool merge_bin failed with code {result}")
def esp32_copy_ota_bin(source, target, env):

View File

@@ -6,13 +6,12 @@ import esphome.codegen as cg
from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant
import esphome.config_validation as cv
from esphome.const import CONF_ENABLE_ON_BOOT, CONF_ESPHOME, CONF_ID, CONF_NAME
from esphome.core import CORE, TimePeriod
from esphome.core import CORE
from esphome.core.config import CONF_NAME_ADD_MAC_SUFFIX
import esphome.final_validate as fv
DEPENDENCIES = ["esp32"]
CODEOWNERS = ["@jesserockz", "@Rapsssito", "@bdraco"]
DOMAIN = "esp32_ble"
CODEOWNERS = ["@jesserockz", "@Rapsssito"]
class BTLoggers(Enum):
@@ -116,11 +115,8 @@ def register_bt_logger(*loggers: BTLoggers) -> None:
CONF_BLE_ID = "ble_id"
CONF_IO_CAPABILITY = "io_capability"
CONF_ADVERTISING = "advertising"
CONF_ADVERTISING_CYCLE_TIME = "advertising_cycle_time"
CONF_DISABLE_BT_LOGS = "disable_bt_logs"
CONF_CONNECTION_TIMEOUT = "connection_timeout"
CONF_MAX_NOTIFICATIONS = "max_notifications"
NO_BLUETOOTH_VARIANTS = [const.VARIANT_ESP32S2]
@@ -165,23 +161,12 @@ CONFIG_SCHEMA = cv.Schema(
IO_CAPABILITY, lower=True
),
cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean,
cv.Optional(CONF_ADVERTISING, default=False): cv.boolean,
cv.Optional(
CONF_ADVERTISING_CYCLE_TIME, default="10s"
): cv.positive_time_period_milliseconds,
cv.SplitDefault(CONF_DISABLE_BT_LOGS, esp32_idf=True): cv.All(
cv.only_with_esp_idf, cv.boolean
),
cv.SplitDefault(CONF_CONNECTION_TIMEOUT, esp32_idf="20s"): cv.All(
cv.only_with_esp_idf,
cv.positive_time_period_seconds,
cv.Range(min=TimePeriod(seconds=10), max=TimePeriod(seconds=180)),
),
cv.SplitDefault(CONF_MAX_NOTIFICATIONS, esp32_idf=12): cv.All(
cv.only_with_esp_idf,
cv.positive_int,
cv.Range(min=1, max=64),
),
}
).extend(cv.COMPONENT_SCHEMA)
@@ -270,31 +255,8 @@ async def to_code(config):
if logger not in _required_loggers:
add_idf_sdkconfig_option(f"{logger.value}_NONE", True)
# Set BLE connection establishment timeout to match aioesphomeapi/bleak-retry-connector
# Default is 20 seconds instead of ESP-IDF's 30 seconds. Because there is no way to
# cancel a BLE connection in progress, when aioesphomeapi times out at 20 seconds,
# the connection slot remains occupied for the remaining time, preventing new connection
# attempts and wasting valuable connection slots.
if CONF_CONNECTION_TIMEOUT in config:
timeout_seconds = int(config[CONF_CONNECTION_TIMEOUT].total_seconds)
add_idf_sdkconfig_option(
"CONFIG_BT_BLE_ESTAB_LINK_CONN_TOUT", timeout_seconds
)
# Set the maximum number of notification registrations
# This controls how many BLE characteristics can have notifications enabled
# across all connections for a single GATT client interface
# https://github.com/esphome/issues/issues/6808
if CONF_MAX_NOTIFICATIONS in config:
add_idf_sdkconfig_option(
"CONFIG_BT_GATTC_NOTIF_REG_MAX", config[CONF_MAX_NOTIFICATIONS]
)
cg.add_define("USE_ESP32_BLE")
if config[CONF_ADVERTISING]:
cg.add_define("USE_ESP32_BLE_ADVERTISING")
@automation.register_condition("ble.enabled", BLEEnabledCondition, cv.Schema({}))
async def ble_enabled_to_code(config, condition_id, template_arg, args):

View File

@@ -53,7 +53,6 @@ void ESP32BLE::disable() {
bool ESP32BLE::is_active() { return this->state_ == BLE_COMPONENT_STATE_ACTIVE; }
#ifdef USE_ESP32_BLE_ADVERTISING
void ESP32BLE::advertising_start() {
this->advertising_init_();
if (!this->is_active())
@@ -89,7 +88,6 @@ void ESP32BLE::advertising_remove_service_uuid(ESPBTUUID uuid) {
this->advertising_->remove_service_uuid(uuid);
this->advertising_start();
}
#endif
bool ESP32BLE::ble_pre_setup_() {
esp_err_t err = nvs_flash_init();
@@ -100,7 +98,6 @@ bool ESP32BLE::ble_pre_setup_() {
return true;
}
#ifdef USE_ESP32_BLE_ADVERTISING
void ESP32BLE::advertising_init_() {
if (this->advertising_ != nullptr)
return;
@@ -110,7 +107,6 @@ void ESP32BLE::advertising_init_() {
this->advertising_->set_min_preferred_interval(0x06);
this->advertising_->set_appearance(this->appearance_);
}
#endif
bool ESP32BLE::ble_setup_() {
esp_err_t err;
@@ -398,11 +394,9 @@ void ESP32BLE::loop() {
this->ble_event_pool_.release(ble_event);
ble_event = this->ble_events_.pop();
}
#ifdef USE_ESP32_BLE_ADVERTISING
if (this->advertising_ != nullptr) {
this->advertising_->loop();
}
#endif
// Log dropped events periodically
uint16_t dropped = this->ble_events_.get_and_reset_dropped_count();

View File

@@ -1,17 +1,14 @@
#pragma once
#include "esphome/core/defines.h" // Must be included before conditional includes
#include "ble_advertising.h"
#include "ble_uuid.h"
#include "ble_scan_result.h"
#ifdef USE_ESP32_BLE_ADVERTISING
#include "ble_advertising.h"
#endif
#include <functional>
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "ble_event.h"
@@ -26,14 +23,21 @@
namespace esphome::esp32_ble {
// Maximum size of the BLE event queue
// Increased to absorb the ring buffer capacity from esp32_ble_tracker
// Maximum number of BLE scan results to buffer
// Sized to handle bursts of advertisements while allowing for processing delays
// With 16 advertisements per batch and some safety margin:
// - Without PSRAM: 24 entries (1.5× batch size)
// - With PSRAM: 36 entries (2.25× batch size)
// The reduced structure size (~80 bytes vs ~400 bytes) allows for larger buffers
#ifdef USE_PSRAM
static constexpr uint8_t MAX_BLE_QUEUE_SIZE = 100; // 64 + 36 (ring buffer size with PSRAM)
static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 36;
#else
static constexpr uint8_t MAX_BLE_QUEUE_SIZE = 88; // 64 + 24 (ring buffer size without PSRAM)
static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 24;
#endif
// Maximum size of the BLE event queue - must be power of 2 for lock-free queue
static constexpr size_t MAX_BLE_QUEUE_SIZE = 64;
uint64_t ble_addr_to_uint64(const esp_bd_addr_t address);
// NOLINTNEXTLINE(modernize-use-using)
@@ -109,7 +113,6 @@ class ESP32BLE : public Component {
float get_setup_priority() const override;
void set_name(const std::string &name) { this->name_ = name; }
#ifdef USE_ESP32_BLE_ADVERTISING
void advertising_start();
void advertising_set_service_data(const std::vector<uint8_t> &data);
void advertising_set_manufacturer_data(const std::vector<uint8_t> &data);
@@ -117,7 +120,6 @@ class ESP32BLE : public Component {
void advertising_add_service_uuid(ESPBTUUID uuid);
void advertising_remove_service_uuid(ESPBTUUID uuid);
void advertising_register_raw_advertisement_callback(std::function<void(bool)> &&callback);
#endif
void register_gap_event_handler(GAPEventHandler *handler) { this->gap_event_handlers_.push_back(handler); }
void register_gap_scan_event_handler(GAPScanEventHandler *handler) {
@@ -138,9 +140,7 @@ class ESP32BLE : public Component {
bool ble_setup_();
bool ble_dismantle_();
bool ble_pre_setup_();
#ifdef USE_ESP32_BLE_ADVERTISING
void advertising_init_();
#endif
private:
template<typename... Args> friend void enqueue_ble_event(Args... args);
@@ -160,9 +160,7 @@ class ESP32BLE : public Component {
optional<std::string> name_;
// 4-byte aligned members
#ifdef USE_ESP32_BLE_ADVERTISING
BLEAdvertising *advertising_{}; // 4 bytes (pointer)
#endif
BLEAdvertising *advertising_{}; // 4 bytes (pointer)
esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; // 4 bytes (enum)
uint32_t advertising_cycle_time_{}; // 4 bytes

View File

@@ -1,7 +1,6 @@
#include "ble_advertising.h"
#ifdef USE_ESP32
#ifdef USE_ESP32_BLE_ADVERTISING
#include <cstdio>
#include <cstring>
@@ -162,5 +161,4 @@ void BLEAdvertising::register_raw_advertisement_callback(std::function<void(bool
} // namespace esphome::esp32_ble
#endif // USE_ESP32_BLE_ADVERTISING
#endif // USE_ESP32
#endif

View File

@@ -5,7 +5,6 @@
#include <vector>
#ifdef USE_ESP32
#ifdef USE_ESP32_BLE_ADVERTISING
#include <esp_bt.h>
#include <esp_gap_ble_api.h>
@@ -57,5 +56,4 @@ class BLEAdvertising {
} // namespace esphome::esp32_ble
#endif // USE_ESP32_BLE_ADVERTISING
#endif // USE_ESP32
#endif

View File

@@ -82,8 +82,6 @@ async def to_code(config):
cg.add(var.set_measured_power(config[CONF_MEASURED_POWER]))
cg.add(var.set_tx_power(config[CONF_TX_POWER]))
cg.add_define("USE_ESP32_BLE_ADVERTISING")
if CORE.using_esp_idf:
add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True)
add_idf_sdkconfig_option("CONFIG_BT_BLE_42_FEATURES_SUPPORTED", True)

View File

@@ -2,7 +2,7 @@ import esphome.codegen as cg
from esphome.components import esp32_ble_tracker
AUTO_LOAD = ["esp32_ble_tracker"]
CODEOWNERS = ["@jesserockz", "@bdraco"]
CODEOWNERS = ["@jesserockz"]
DEPENDENCIES = ["esp32"]
esp32_ble_client_ns = cg.esphome_ns.namespace("esp32_ble_client")

View File

@@ -16,8 +16,8 @@ static const char *const TAG = "esp32_ble_client";
// Intermediate connection parameters for standard operation
// ESP-IDF defaults (12.5-15ms) are too slow for stable connections through WiFi-based BLE proxies,
// causing disconnections. These medium parameters balance responsiveness with bandwidth usage.
static const uint16_t MEDIUM_MIN_CONN_INTERVAL = 0x07; // 7 * 1.25ms = 8.75ms
static const uint16_t MEDIUM_MAX_CONN_INTERVAL = 0x09; // 9 * 1.25ms = 11.25ms
static const uint16_t MEDIUM_MIN_CONN_INTERVAL = 0x08; // 8 * 1.25ms = 10ms
static const uint16_t MEDIUM_MAX_CONN_INTERVAL = 0x0A; // 10 * 1.25ms = 12.5ms
// The timeout value was increased from 6s to 8s to address stability issues observed
// in certain BLE devices when operating through WiFi-based BLE proxies. The longer
// timeout reduces the likelihood of disconnections during periods of high latency.
@@ -45,10 +45,8 @@ void BLEClientBase::set_state(espbt::ClientState st) {
ESPBTClient::set_state(st);
if (st == espbt::ClientState::READY_TO_CONNECT) {
// Enable loop for state processing
// Enable loop when we need to connect
this->enable_loop();
// Connect immediately instead of waiting for next loop
this->connect();
}
}
@@ -65,6 +63,11 @@ void BLEClientBase::loop() {
}
this->set_state(espbt::ClientState::IDLE);
}
// READY_TO_CONNECT means we have discovered the device
// and the scanner has been stopped by the tracker.
else if (this->state_ == espbt::ClientState::READY_TO_CONNECT) {
this->connect();
}
// If its idle, we can disable the loop as set_state
// will enable it again when we need to connect.
else if (this->state_ == espbt::ClientState::IDLE) {
@@ -79,7 +82,40 @@ void BLEClientBase::dump_config() {
" Address: %s\n"
" Auto-Connect: %s",
this->address_str().c_str(), TRUEFALSE(this->auto_connect_));
ESP_LOGCONFIG(TAG, " State: %s", espbt::client_state_to_string(this->state()));
std::string state_name;
switch (this->state()) {
case espbt::ClientState::INIT:
state_name = "INIT";
break;
case espbt::ClientState::DISCONNECTING:
state_name = "DISCONNECTING";
break;
case espbt::ClientState::IDLE:
state_name = "IDLE";
break;
case espbt::ClientState::SEARCHING:
state_name = "SEARCHING";
break;
case espbt::ClientState::DISCOVERED:
state_name = "DISCOVERED";
break;
case espbt::ClientState::READY_TO_CONNECT:
state_name = "READY_TO_CONNECT";
break;
case espbt::ClientState::CONNECTING:
state_name = "CONNECTING";
break;
case espbt::ClientState::CONNECTED:
state_name = "CONNECTED";
break;
case espbt::ClientState::ESTABLISHED:
state_name = "ESTABLISHED";
break;
default:
state_name = "UNKNOWN_STATE";
break;
}
ESP_LOGCONFIG(TAG, " State: %s", state_name.c_str());
if (this->status_ == ESP_GATT_NO_RESOURCES) {
ESP_LOGE(TAG, " Failed due to no resources. Try to reduce number of BLE clients in config.");
} else if (this->status_ != ESP_GATT_OK) {
@@ -112,42 +148,41 @@ void BLEClientBase::connect() {
this->remote_addr_type_);
this->paired_ = false;
// Set preferred connection parameters before connecting
// Use FAST for all V3 connections (better latency and reliability)
// Use MEDIUM for V1/legacy connections (balanced performance)
uint16_t min_interval, max_interval, timeout;
const char *param_type;
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE ||
this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
min_interval = FAST_MIN_CONN_INTERVAL;
max_interval = FAST_MAX_CONN_INTERVAL;
timeout = FAST_CONN_TIMEOUT;
param_type = "fast";
} else {
min_interval = MEDIUM_MIN_CONN_INTERVAL;
max_interval = MEDIUM_MAX_CONN_INTERVAL;
timeout = MEDIUM_CONN_TIMEOUT;
param_type = "medium";
}
auto param_ret = esp_ble_gap_set_prefer_conn_params(this->remote_bda_, min_interval, max_interval,
0, // latency: 0
timeout);
if (param_ret != ESP_OK) {
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gap_set_prefer_conn_params failed: %d", this->connection_index_,
this->address_str_.c_str(), param_ret);
} else {
ESP_LOGD(TAG, "[%d] [%s] Set %s conn params", this->connection_index_, this->address_str_.c_str(), param_type);
}
// Now open the connection
auto ret = esp_ble_gattc_open(this->gattc_if_, this->remote_bda_, this->remote_addr_type_, true);
if (ret) {
this->log_gattc_warning_("esp_ble_gattc_open", ret);
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_open error, status=%d", this->connection_index_, this->address_str_.c_str(),
ret);
this->set_state(espbt::ClientState::IDLE);
} else {
this->set_state(espbt::ClientState::CONNECTING);
// Always set connection parameters to ensure stable operation
// Use FAST for V3_WITHOUT_CACHE (devices that need lowest latency)
// Use MEDIUM for all other connections (balanced performance)
uint16_t min_interval, max_interval, timeout;
const char *param_type;
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
min_interval = FAST_MIN_CONN_INTERVAL;
max_interval = FAST_MAX_CONN_INTERVAL;
timeout = FAST_CONN_TIMEOUT;
param_type = "fast";
} else {
min_interval = MEDIUM_MIN_CONN_INTERVAL;
max_interval = MEDIUM_MAX_CONN_INTERVAL;
timeout = MEDIUM_CONN_TIMEOUT;
param_type = "medium";
}
auto param_ret = esp_ble_gap_set_prefer_conn_params(this->remote_bda_, min_interval, max_interval,
0, // latency: 0
timeout);
if (param_ret != ESP_OK) {
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gap_set_prefer_conn_params failed: %d", this->connection_index_,
this->address_str_.c_str(), param_ret);
} else {
ESP_LOGD(TAG, "[%d] [%s] Set %s conn params", this->connection_index_, this->address_str_.c_str(), param_type);
}
}
}
@@ -222,32 +257,6 @@ void BLEClientBase::log_event_(const char *name) {
ESP_LOGD(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_.c_str(), name);
}
void BLEClientBase::log_gattc_event_(const char *name) {
ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_%s_EVT", this->connection_index_, this->address_str_.c_str(), name);
}
void BLEClientBase::log_gattc_warning_(const char *operation, esp_gatt_status_t status) {
ESP_LOGW(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str_.c_str(), operation,
status);
}
void BLEClientBase::log_gattc_warning_(const char *operation, esp_err_t err) {
ESP_LOGW(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str_.c_str(), operation, err);
}
void BLEClientBase::restore_medium_conn_params_() {
// Restore to medium connection parameters after initial connection phase
// This balances performance with bandwidth usage for normal operation
esp_ble_conn_update_params_t conn_params = {{0}};
memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t));
conn_params.min_int = MEDIUM_MIN_CONN_INTERVAL;
conn_params.max_int = MEDIUM_MAX_CONN_INTERVAL;
conn_params.latency = 0;
conn_params.timeout = MEDIUM_CONN_TIMEOUT;
ESP_LOGD(TAG, "[%d] [%s] Restoring medium conn params", this->connection_index_, this->address_str_.c_str());
esp_ble_gap_update_conn_params(&conn_params);
}
bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if,
esp_ble_gattc_cb_param_t *param) {
if (event == ESP_GATTC_REG_EVT && this->app_id != param->reg.app_id)
@@ -275,18 +284,30 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
case ESP_GATTC_OPEN_EVT: {
if (!this->check_addr(param->open.remote_bda))
return false;
this->log_gattc_event_("OPEN");
// conn_id was already set in ESP_GATTC_CONNECT_EVT
this->log_event_("ESP_GATTC_OPEN_EVT");
this->conn_id_ = param->open.conn_id;
this->service_count_ = 0;
if (this->state_ != espbt::ClientState::CONNECTING) {
// This should not happen but lets log it in case it does
// because it means we have a bad assumption about how the
// ESP BT stack works.
ESP_LOGE(TAG, "[%d] [%s] Got ESP_GATTC_OPEN_EVT while in %s state, status=%d", this->connection_index_,
this->address_str_.c_str(), espbt::client_state_to_string(this->state_), param->open.status);
if (this->state_ == espbt::ClientState::CONNECTED) {
ESP_LOGE(TAG, "[%d] [%s] Got ESP_GATTC_OPEN_EVT while already connected, status=%d", this->connection_index_,
this->address_str_.c_str(), param->open.status);
} else if (this->state_ == espbt::ClientState::ESTABLISHED) {
ESP_LOGE(TAG, "[%d] [%s] Got ESP_GATTC_OPEN_EVT while already established, status=%d",
this->connection_index_, this->address_str_.c_str(), param->open.status);
} else if (this->state_ == espbt::ClientState::DISCONNECTING) {
ESP_LOGE(TAG, "[%d] [%s] Got ESP_GATTC_OPEN_EVT while disconnecting, status=%d", this->connection_index_,
this->address_str_.c_str(), param->open.status);
} else {
ESP_LOGE(TAG, "[%d] [%s] Got ESP_GATTC_OPEN_EVT while not in connecting state, status=%d",
this->connection_index_, this->address_str_.c_str(), param->open.status);
}
}
if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) {
this->log_gattc_warning_("Connection open", param->open.status);
ESP_LOGW(TAG, "[%d] [%s] Connection failed, status=%d", this->connection_index_, this->address_str_.c_str(),
param->open.status);
this->set_state(espbt::ClientState::IDLE);
break;
}
@@ -298,15 +319,15 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
this->conn_id_ = UNSET_CONN_ID;
break;
}
// MTU negotiation already started in ESP_GATTC_CONNECT_EVT
auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if_, param->open.conn_id);
if (ret) {
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_send_mtu_req failed, status=%x", this->connection_index_,
this->address_str_.c_str(), ret);
}
this->set_state(espbt::ClientState::CONNECTED);
ESP_LOGI(TAG, "[%d] [%s] Connection open", this->connection_index_, this->address_str_.c_str());
if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
ESP_LOGI(TAG, "[%d] [%s] Using cached services", this->connection_index_, this->address_str_.c_str());
// Restore to medium connection parameters for cached connections too
this->restore_medium_conn_params_();
// only set our state, subclients might have more stuff to do yet.
this->state_ = espbt::ClientState::ESTABLISHED;
break;
@@ -318,17 +339,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
case ESP_GATTC_CONNECT_EVT: {
if (!this->check_addr(param->connect.remote_bda))
return false;
this->log_gattc_event_("CONNECT");
this->conn_id_ = param->connect.conn_id;
// Start MTU negotiation immediately as recommended by ESP-IDF examples
// (gatt_client, ble_throughput) which call esp_ble_gattc_send_mtu_req in
// ESP_GATTC_CONNECT_EVT instead of waiting for ESP_GATTC_OPEN_EVT.
// This saves ~3ms in the connection process.
auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if_, param->connect.conn_id);
if (ret) {
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_send_mtu_req failed, status=%x", this->connection_index_,
this->address_str_.c_str(), ret);
}
this->log_event_("ESP_GATTC_CONNECT_EVT");
break;
}
case ESP_GATTC_DISCONNECT_EVT: {
@@ -365,7 +376,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
case ESP_GATTC_CLOSE_EVT: {
if (this->conn_id_ != param->close.conn_id)
return false;
this->log_gattc_event_("CLOSE");
this->log_event_("ESP_GATTC_CLOSE_EVT");
this->release_services();
this->set_state(espbt::ClientState::IDLE);
this->conn_id_ = UNSET_CONN_ID;
@@ -391,7 +402,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
case ESP_GATTC_SEARCH_CMPL_EVT: {
if (this->conn_id_ != param->search_cmpl.conn_id)
return false;
this->log_gattc_event_("SEARCH_CMPL");
this->log_event_("ESP_GATTC_SEARCH_CMPL_EVT");
for (auto &svc : this->services_) {
ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_.c_str(),
svc->uuid.to_string().c_str());
@@ -400,11 +411,18 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
}
ESP_LOGI(TAG, "[%d] [%s] Service discovery complete", this->connection_index_, this->address_str_.c_str());
// For V3 connections, restore to medium connection parameters after service discovery
// For non-cached connections, restore to medium connection parameters after service discovery
// This balances performance with bandwidth usage after the critical discovery phase
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE ||
this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
this->restore_medium_conn_params_();
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
esp_ble_conn_update_params_t conn_params = {{0}};
memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t));
conn_params.min_int = MEDIUM_MIN_CONN_INTERVAL;
conn_params.max_int = MEDIUM_MAX_CONN_INTERVAL;
conn_params.latency = 0;
conn_params.timeout = MEDIUM_CONN_TIMEOUT;
ESP_LOGD(TAG, "[%d] [%s] Restored medium conn params after service discovery", this->connection_index_,
this->address_str_.c_str());
esp_ble_gap_update_conn_params(&conn_params);
}
this->state_ = espbt::ClientState::ESTABLISHED;
@@ -413,35 +431,35 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
case ESP_GATTC_READ_DESCR_EVT: {
if (this->conn_id_ != param->write.conn_id)
return false;
this->log_gattc_event_("READ_DESCR");
this->log_event_("ESP_GATTC_READ_DESCR_EVT");
break;
}
case ESP_GATTC_WRITE_DESCR_EVT: {
if (this->conn_id_ != param->write.conn_id)
return false;
this->log_gattc_event_("WRITE_DESCR");
this->log_event_("ESP_GATTC_WRITE_DESCR_EVT");
break;
}
case ESP_GATTC_WRITE_CHAR_EVT: {
if (this->conn_id_ != param->write.conn_id)
return false;
this->log_gattc_event_("WRITE_CHAR");
this->log_event_("ESP_GATTC_WRITE_CHAR_EVT");
break;
}
case ESP_GATTC_READ_CHAR_EVT: {
if (this->conn_id_ != param->read.conn_id)
return false;
this->log_gattc_event_("READ_CHAR");
this->log_event_("ESP_GATTC_READ_CHAR_EVT");
break;
}
case ESP_GATTC_NOTIFY_EVT: {
if (this->conn_id_ != param->notify.conn_id)
return false;
this->log_gattc_event_("NOTIFY");
this->log_event_("ESP_GATTC_NOTIFY_EVT");
break;
}
case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
this->log_gattc_event_("REG_FOR_NOTIFY");
this->log_event_("ESP_GATTC_REG_FOR_NOTIFY_EVT");
if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
// Client is responsible for flipping the descriptor value
@@ -453,7 +471,8 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
esp_gatt_status_t descr_status = esp_ble_gattc_get_descr_by_char_handle(
this->gattc_if_, this->conn_id_, param->reg_for_notify.handle, NOTIFY_DESC_UUID, &desc_result, &count);
if (descr_status != ESP_GATT_OK) {
this->log_gattc_warning_("esp_ble_gattc_get_descr_by_char_handle", descr_status);
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_descr_by_char_handle error, status=%d", this->connection_index_,
this->address_str_.c_str(), descr_status);
break;
}
esp_gattc_char_elem_t char_result;
@@ -461,7 +480,8 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, param->reg_for_notify.handle,
param->reg_for_notify.handle, &char_result, &count, 0);
if (char_status != ESP_GATT_OK) {
this->log_gattc_warning_("esp_ble_gattc_get_all_char", char_status);
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", this->connection_index_,
this->address_str_.c_str(), char_status);
break;
}
@@ -475,7 +495,8 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
(uint8_t *) &notify_en, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE);
ESP_LOGD(TAG, "Wrote notify descriptor %d, properties=%d", notify_en, char_result.properties);
if (status) {
this->log_gattc_warning_("esp_ble_gattc_write_char_descr", status);
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_write_char_descr error, status=%d", this->connection_index_,
this->address_str_.c_str(), status);
}
break;
}

View File

@@ -66,7 +66,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
(uint8_t) (this->address_ >> 0) & 0xff);
}
}
const std::string &address_str() const { return this->address_str_; }
std::string address_str() const { return this->address_str_; }
BLEService *get_service(espbt::ESPBTUUID uuid);
BLEService *get_service(uint16_t uuid);
@@ -127,10 +127,6 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
// 6 bytes used, 2 bytes padding
void log_event_(const char *name);
void log_gattc_event_(const char *name);
void restore_medium_conn_params_();
void log_gattc_warning_(const char *operation, esp_gatt_status_t status);
void log_gattc_warning_(const char *operation, esp_err_t err);
};
} // namespace esp32_ble_client

View File

@@ -571,7 +571,6 @@ async def to_code(config):
config[CONF_ON_DISCONNECT],
)
cg.add_define("USE_ESP32_BLE_SERVER")
cg.add_define("USE_ESP32_BLE_ADVERTISING")
if CORE.using_esp_idf:
add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True)

View File

@@ -36,7 +36,6 @@ from esphome.types import ConfigType
AUTO_LOAD = ["esp32_ble"]
DEPENDENCIES = ["esp32"]
CODEOWNERS = ["@bdraco"]
KEY_ESP32_BLE_TRACKER = "esp32_ble_tracker"
KEY_USED_CONNECTION_SLOTS = "used_connection_slots"
@@ -355,6 +354,11 @@ async def to_code(config):
add_idf_sdkconfig_option(
"CONFIG_BTDM_CTRL_BLE_MAX_CONN", config[CONF_MAX_CONNECTIONS]
)
# CONFIG_BT_GATTC_NOTIF_REG_MAX controls the number of
# max notifications in 5.x, setting CONFIG_BT_ACL_CONNECTIONS
# is enough in 4.x
# https://github.com/esphome/issues/issues/6808
add_idf_sdkconfig_option("CONFIG_BT_GATTC_NOTIF_REG_MAX", 9)
cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts
cg.add_define("USE_ESP32_BLE_CLIENT")

View File

@@ -41,31 +41,6 @@ static const char *const TAG = "esp32_ble_tracker";
ESP32BLETracker *global_esp32_ble_tracker = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
const char *client_state_to_string(ClientState state) {
switch (state) {
case ClientState::INIT:
return "INIT";
case ClientState::DISCONNECTING:
return "DISCONNECTING";
case ClientState::IDLE:
return "IDLE";
case ClientState::SEARCHING:
return "SEARCHING";
case ClientState::DISCOVERED:
return "DISCOVERED";
case ClientState::READY_TO_CONNECT:
return "READY_TO_CONNECT";
case ClientState::CONNECTING:
return "CONNECTING";
case ClientState::CONNECTED:
return "CONNECTED";
case ClientState::ESTABLISHED:
return "ESTABLISHED";
default:
return "UNKNOWN";
}
}
float ESP32BLETracker::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; }
void ESP32BLETracker::setup() {
@@ -74,6 +49,13 @@ void ESP32BLETracker::setup() {
ESP_LOGE(TAG, "BLE Tracker was marked failed by ESP32BLE");
return;
}
RAMAllocator<BLEScanResult> allocator;
this->scan_ring_buffer_ = allocator.allocate(SCAN_RESULT_BUFFER_SIZE);
if (this->scan_ring_buffer_ == nullptr) {
ESP_LOGE(TAG, "Could not allocate ring buffer for BLE Tracker!");
this->mark_failed();
}
global_esp32_ble_tracker = this;
@@ -101,17 +83,127 @@ void ESP32BLETracker::loop() {
this->start_scan();
}
}
ClientStateCounts counts = this->count_client_states_();
if (counts != this->client_state_counts_) {
this->client_state_counts_ = counts;
ESP_LOGD(TAG, "connecting: %d, discovered: %d, searching: %d, disconnecting: %d",
this->client_state_counts_.connecting, this->client_state_counts_.discovered,
this->client_state_counts_.searching, this->client_state_counts_.disconnecting);
int connecting = 0;
int discovered = 0;
int searching = 0;
int disconnecting = 0;
for (auto *client : this->clients_) {
switch (client->state()) {
case ClientState::DISCONNECTING:
disconnecting++;
break;
case ClientState::DISCOVERED:
discovered++;
break;
case ClientState::SEARCHING:
searching++;
break;
case ClientState::CONNECTING:
case ClientState::READY_TO_CONNECT:
connecting++;
break;
default:
break;
}
}
if (connecting != connecting_ || discovered != discovered_ || searching != searching_ ||
disconnecting != disconnecting_) {
connecting_ = connecting;
discovered_ = discovered;
searching_ = searching;
disconnecting_ = disconnecting;
ESP_LOGD(TAG, "connecting: %d, discovered: %d, searching: %d, disconnecting: %d", connecting_, discovered_,
searching_, disconnecting_);
}
bool promote_to_connecting = discovered && !searching && !connecting;
// Process scan results from lock-free SPSC ring buffer
// Consumer side: This runs in the main loop thread
if (this->scanner_state_ == ScannerState::RUNNING) {
// Load our own index with relaxed ordering (we're the only writer)
uint8_t read_idx = this->ring_read_index_.load(std::memory_order_relaxed);
// Load producer's index with acquire to see their latest writes
uint8_t write_idx = this->ring_write_index_.load(std::memory_order_acquire);
while (read_idx != write_idx) {
// Calculate how many contiguous results we can process in one batch
// If write > read: process all results from read to write
// If write <= read (wraparound): process from read to end of buffer first
size_t batch_size = (write_idx > read_idx) ? (write_idx - read_idx) : (SCAN_RESULT_BUFFER_SIZE - read_idx);
// Process the batch for raw advertisements
if (this->raw_advertisements_) {
for (auto *listener : this->listeners_) {
listener->parse_devices(&this->scan_ring_buffer_[read_idx], batch_size);
}
for (auto *client : this->clients_) {
client->parse_devices(&this->scan_ring_buffer_[read_idx], batch_size);
}
}
// Process individual results for parsed advertisements
if (this->parse_advertisements_) {
#ifdef USE_ESP32_BLE_DEVICE
for (size_t i = 0; i < batch_size; i++) {
BLEScanResult &scan_result = this->scan_ring_buffer_[read_idx + i];
ESPBTDevice device;
device.parse_scan_rst(scan_result);
bool found = false;
for (auto *listener : this->listeners_) {
if (listener->parse_device(device))
found = true;
}
for (auto *client : this->clients_) {
if (client->parse_device(device)) {
found = true;
if (!connecting && client->state() == ClientState::DISCOVERED) {
promote_to_connecting = true;
}
}
}
if (!found && !this->scan_continuous_) {
this->print_bt_device_info(device);
}
}
#endif // USE_ESP32_BLE_DEVICE
}
// Update read index for entire batch
read_idx = (read_idx + batch_size) % SCAN_RESULT_BUFFER_SIZE;
// Store with release to ensure reads complete before index update
this->ring_read_index_.store(read_idx, std::memory_order_release);
}
// Log dropped results periodically
size_t dropped = this->scan_results_dropped_.exchange(0, std::memory_order_relaxed);
if (dropped > 0) {
ESP_LOGW(TAG, "Dropped %zu BLE scan results due to buffer overflow", dropped);
}
}
if (this->scanner_state_ == ScannerState::STOPPED) {
this->end_of_scan_(); // Change state to IDLE
}
if (this->scanner_state_ == ScannerState::FAILED ||
(this->scan_set_param_failed_ && this->scanner_state_ == ScannerState::RUNNING)) {
this->handle_scanner_failure_();
this->stop_scan_();
if (this->scan_start_fail_count_ == std::numeric_limits<uint8_t>::max()) {
ESP_LOGE(TAG, "Scan could not restart after %d attempts, rebooting to restore stack (IDF)",
std::numeric_limits<uint8_t>::max());
App.reboot();
}
if (this->scan_start_failed_) {
ESP_LOGE(TAG, "Scan start failed: %d", this->scan_start_failed_);
this->scan_start_failed_ = ESP_BT_STATUS_SUCCESS;
}
if (this->scan_set_param_failed_) {
ESP_LOGE(TAG, "Scan set param failed: %d", this->scan_set_param_failed_);
this->scan_set_param_failed_ = ESP_BT_STATUS_SUCCESS;
}
}
/*
@@ -126,12 +218,13 @@ void ESP32BLETracker::loop() {
https://github.com/espressif/esp-idf/issues/6688
*/
bool promote_to_connecting = counts.discovered && !counts.searching && !counts.connecting;
if (this->scanner_state_ == ScannerState::IDLE && !counts.connecting && !counts.disconnecting &&
!promote_to_connecting) {
if (this->scanner_state_ == ScannerState::IDLE && !connecting && !disconnecting && !promote_to_connecting) {
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
this->update_coex_preference_(false);
if (this->coex_prefer_ble_) {
this->coex_prefer_ble_ = false;
ESP_LOGD(TAG, "Setting coexistence preference to balanced.");
esp_coex_preference_set(ESP_COEX_PREFER_BALANCE); // Reset to default
}
#endif
if (this->scan_continuous_) {
this->start_scan_(false); // first = false
@@ -139,13 +232,31 @@ void ESP32BLETracker::loop() {
}
// If there is a discovered client and no connecting
// clients and no clients using the scanner to search for
// devices, then promote the discovered client to ready to connect.
// We check both RUNNING and IDLE states because:
// - RUNNING: gap_scan_event_handler initiates stop_scan_() but promotion can happen immediately
// - IDLE: Scanner has already stopped (naturally or by gap_scan_event_handler)
// devices, then stop scanning and promote the discovered
// client to ready to connect.
if (promote_to_connecting &&
(this->scanner_state_ == ScannerState::RUNNING || this->scanner_state_ == ScannerState::IDLE)) {
this->try_promote_discovered_clients_();
for (auto *client : this->clients_) {
if (client->state() == ClientState::DISCOVERED) {
if (this->scanner_state_ == ScannerState::RUNNING) {
ESP_LOGD(TAG, "Stopping scan to make connection");
this->stop_scan_();
} else if (this->scanner_state_ == ScannerState::IDLE) {
ESP_LOGD(TAG, "Promoting client to connect");
// We only want to promote one client at a time.
// once the scanner is fully stopped.
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
ESP_LOGD(TAG, "Setting coexistence to Bluetooth to make connection.");
if (!this->coex_prefer_ble_) {
this->coex_prefer_ble_ = true;
esp_coex_preference_set(ESP_COEX_PREFER_BT); // Prioritize Bluetooth
}
#endif
client->set_state(ClientState::READY_TO_CONNECT);
}
break;
}
}
}
}
@@ -161,7 +272,15 @@ void ESP32BLETracker::ble_before_disabled_event_handler() { this->stop_scan_();
void ESP32BLETracker::stop_scan_() {
if (this->scanner_state_ != ScannerState::RUNNING && this->scanner_state_ != ScannerState::FAILED) {
ESP_LOGE(TAG, "Cannot stop scan: %s", this->scanner_state_to_string_(this->scanner_state_));
if (this->scanner_state_ == ScannerState::IDLE) {
ESP_LOGE(TAG, "Scan is already stopped while trying to stop.");
} else if (this->scanner_state_ == ScannerState::STARTING) {
ESP_LOGE(TAG, "Scan is starting while trying to stop.");
} else if (this->scanner_state_ == ScannerState::STOPPING) {
ESP_LOGE(TAG, "Scan is already stopping while trying to stop.");
} else if (this->scanner_state_ == ScannerState::STOPPED) {
ESP_LOGE(TAG, "Scan is already stopped while trying to stop.");
}
return;
}
this->cancel_timeout("scan");
@@ -179,7 +298,17 @@ void ESP32BLETracker::start_scan_(bool first) {
return;
}
if (this->scanner_state_ != ScannerState::IDLE) {
this->log_unexpected_state_("start scan", ScannerState::IDLE);
if (this->scanner_state_ == ScannerState::STARTING) {
ESP_LOGE(TAG, "Cannot start scan while already starting.");
} else if (this->scanner_state_ == ScannerState::RUNNING) {
ESP_LOGE(TAG, "Cannot start scan while already running.");
} else if (this->scanner_state_ == ScannerState::STOPPING) {
ESP_LOGE(TAG, "Cannot start scan while already stopping.");
} else if (this->scanner_state_ == ScannerState::FAILED) {
ESP_LOGE(TAG, "Cannot start scan while already failed.");
} else if (this->scanner_state_ == ScannerState::STOPPED) {
ESP_LOGE(TAG, "Cannot start scan while already stopped.");
}
return;
}
this->set_scanner_state_(ScannerState::STARTING);
@@ -188,9 +317,7 @@ void ESP32BLETracker::start_scan_(bool first) {
for (auto *listener : this->listeners_)
listener->on_scan_end();
}
#ifdef USE_ESP32_BLE_DEVICE
this->already_discovered_.clear();
#endif
this->scan_params_.scan_type = this->scan_active_ ? BLE_SCAN_TYPE_ACTIVE : BLE_SCAN_TYPE_PASSIVE;
this->scan_params_.own_addr_type = BLE_ADDR_TYPE_PUBLIC;
this->scan_params_.scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL;
@@ -215,6 +342,21 @@ void ESP32BLETracker::start_scan_(bool first) {
}
}
void ESP32BLETracker::end_of_scan_() {
// The lock must be held when calling this function.
if (this->scanner_state_ != ScannerState::STOPPED) {
ESP_LOGE(TAG, "end_of_scan_ called while scanner is not stopped.");
return;
}
ESP_LOGD(TAG, "End of scan, set scanner state to IDLE.");
this->already_discovered_.clear();
this->cancel_timeout("scan");
for (auto *listener : this->listeners_)
listener->on_scan_end();
this->set_scanner_state_(ScannerState::IDLE);
}
void ESP32BLETracker::register_client(ESPBTClient *client) {
client->app_id = ++this->app_id_;
this->clients_.push_back(client);
@@ -247,8 +389,6 @@ void ESP32BLETracker::recalculate_advertisement_parser_types() {
}
void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
// Note: This handler is called from the main loop context, not directly from the BT task.
// The esp32_ble component queues events via enqueue_ble_event() and processes them in loop().
switch (event) {
case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
this->gap_scan_set_param_complete_(param->scan_param_cmpl);
@@ -269,32 +409,51 @@ void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_ga
}
void ESP32BLETracker::gap_scan_event_handler(const BLEScanResult &scan_result) {
// Note: This handler is called from the main loop context via esp32_ble's event queue.
// We process advertisements immediately instead of buffering them.
ESP_LOGV(TAG, "gap_scan_result - event %d", scan_result.search_evt);
if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) {
// Process the scan result immediately
bool found_discovered_client = this->process_scan_result_(scan_result);
// Lock-free SPSC ring buffer write (Producer side)
// This runs in the ESP-IDF Bluetooth stack callback thread
// IMPORTANT: Only this thread writes to ring_write_index_
// If we found a discovered client that needs promotion, stop scanning
// This replaces the promote_to_connecting logic from loop()
if (found_discovered_client && this->scanner_state_ == ScannerState::RUNNING) {
ESP_LOGD(TAG, "Found discovered client, stopping scan for connection");
this->stop_scan_();
// Load our own index with relaxed ordering (we're the only writer)
uint8_t write_idx = this->ring_write_index_.load(std::memory_order_relaxed);
uint8_t next_write_idx = (write_idx + 1) % SCAN_RESULT_BUFFER_SIZE;
// Load consumer's index with acquire to see their latest updates
uint8_t read_idx = this->ring_read_index_.load(std::memory_order_acquire);
// Check if buffer is full
if (next_write_idx != read_idx) {
// Write to ring buffer
this->scan_ring_buffer_[write_idx] = scan_result;
// Store with release to ensure the write is visible before index update
this->ring_write_index_.store(next_write_idx, std::memory_order_release);
} else {
// Buffer full, track dropped results
this->scan_results_dropped_.fetch_add(1, std::memory_order_relaxed);
}
} else if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_CMPL_EVT) {
// Scan finished on its own
if (this->scanner_state_ != ScannerState::RUNNING) {
this->log_unexpected_state_("scan complete", ScannerState::RUNNING);
if (this->scanner_state_ == ScannerState::STOPPING) {
ESP_LOGE(TAG, "Scan was not running when scan completed.");
} else if (this->scanner_state_ == ScannerState::STARTING) {
ESP_LOGE(TAG, "Scan was not started when scan completed.");
} else if (this->scanner_state_ == ScannerState::FAILED) {
ESP_LOGE(TAG, "Scan was in failed state when scan completed.");
} else if (this->scanner_state_ == ScannerState::IDLE) {
ESP_LOGE(TAG, "Scan was idle when scan completed.");
} else if (this->scanner_state_ == ScannerState::STOPPED) {
ESP_LOGE(TAG, "Scan was stopped when scan completed.");
}
}
// Scan completed naturally, perform cleanup and transition to IDLE
this->cleanup_scan_state_(false);
this->set_scanner_state_(ScannerState::STOPPED);
}
}
void ESP32BLETracker::gap_scan_set_param_complete_(const esp_ble_gap_cb_param_t::ble_scan_param_cmpl_evt_param &param) {
// Called from main loop context via gap_event_handler after being queued from BT task
ESP_LOGV(TAG, "gap_scan_set_param_complete - status %d", param.status);
if (param.status == ESP_BT_STATUS_DONE) {
this->scan_set_param_failed_ = ESP_BT_STATUS_SUCCESS;
@@ -304,11 +463,20 @@ void ESP32BLETracker::gap_scan_set_param_complete_(const esp_ble_gap_cb_param_t:
}
void ESP32BLETracker::gap_scan_start_complete_(const esp_ble_gap_cb_param_t::ble_scan_start_cmpl_evt_param &param) {
// Called from main loop context via gap_event_handler after being queued from BT task
ESP_LOGV(TAG, "gap_scan_start_complete - status %d", param.status);
this->scan_start_failed_ = param.status;
if (this->scanner_state_ != ScannerState::STARTING) {
this->log_unexpected_state_("start complete", ScannerState::STARTING);
if (this->scanner_state_ == ScannerState::RUNNING) {
ESP_LOGE(TAG, "Scan was already running when start complete.");
} else if (this->scanner_state_ == ScannerState::STOPPING) {
ESP_LOGE(TAG, "Scan was stopping when start complete.");
} else if (this->scanner_state_ == ScannerState::FAILED) {
ESP_LOGE(TAG, "Scan was in failed state when start complete.");
} else if (this->scanner_state_ == ScannerState::IDLE) {
ESP_LOGE(TAG, "Scan was idle when start complete.");
} else if (this->scanner_state_ == ScannerState::STOPPED) {
ESP_LOGE(TAG, "Scan was stopped when start complete.");
}
}
if (param.status == ESP_BT_STATUS_SUCCESS) {
this->scan_start_fail_count_ = 0;
@@ -322,15 +490,21 @@ void ESP32BLETracker::gap_scan_start_complete_(const esp_ble_gap_cb_param_t::ble
}
void ESP32BLETracker::gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param &param) {
// Called from main loop context via gap_event_handler after being queued from BT task
// This allows us to safely transition to IDLE state and perform cleanup without race conditions
ESP_LOGV(TAG, "gap_scan_stop_complete - status %d", param.status);
if (this->scanner_state_ != ScannerState::STOPPING) {
this->log_unexpected_state_("stop complete", ScannerState::STOPPING);
if (this->scanner_state_ == ScannerState::RUNNING) {
ESP_LOGE(TAG, "Scan was not running when stop complete.");
} else if (this->scanner_state_ == ScannerState::STARTING) {
ESP_LOGE(TAG, "Scan was not started when stop complete.");
} else if (this->scanner_state_ == ScannerState::FAILED) {
ESP_LOGE(TAG, "Scan was in failed state when stop complete.");
} else if (this->scanner_state_ == ScannerState::IDLE) {
ESP_LOGE(TAG, "Scan was idle when stop complete.");
} else if (this->scanner_state_ == ScannerState::STOPPED) {
ESP_LOGE(TAG, "Scan was stopped when stop complete.");
}
}
// Perform cleanup and transition to IDLE
this->cleanup_scan_state_(true);
this->set_scanner_state_(ScannerState::STOPPED);
}
void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
@@ -607,10 +781,28 @@ void ESP32BLETracker::dump_config() {
" Continuous Scanning: %s",
this->scan_duration_, this->scan_interval_ * 0.625f, this->scan_window_ * 0.625f,
this->scan_active_ ? "ACTIVE" : "PASSIVE", YESNO(this->scan_continuous_));
ESP_LOGCONFIG(TAG, " Scanner State: %s", this->scanner_state_to_string_(this->scanner_state_));
ESP_LOGCONFIG(TAG, " Connecting: %d, discovered: %d, searching: %d, disconnecting: %d",
this->client_state_counts_.connecting, this->client_state_counts_.discovered,
this->client_state_counts_.searching, this->client_state_counts_.disconnecting);
switch (this->scanner_state_) {
case ScannerState::IDLE:
ESP_LOGCONFIG(TAG, " Scanner State: IDLE");
break;
case ScannerState::STARTING:
ESP_LOGCONFIG(TAG, " Scanner State: STARTING");
break;
case ScannerState::RUNNING:
ESP_LOGCONFIG(TAG, " Scanner State: RUNNING");
break;
case ScannerState::STOPPING:
ESP_LOGCONFIG(TAG, " Scanner State: STOPPING");
break;
case ScannerState::STOPPED:
ESP_LOGCONFIG(TAG, " Scanner State: STOPPED");
break;
case ScannerState::FAILED:
ESP_LOGCONFIG(TAG, " Scanner State: FAILED");
break;
}
ESP_LOGCONFIG(TAG, " Connecting: %d, discovered: %d, searching: %d, disconnecting: %d", connecting_, discovered_,
searching_, disconnecting_);
if (this->scan_start_fail_count_) {
ESP_LOGCONFIG(TAG, " Scan Start Fail Count: %d", this->scan_start_fail_count_);
}
@@ -687,157 +879,8 @@ bool ESPBTDevice::resolve_irk(const uint8_t *irk) const {
return ecb_ciphertext[15] == (addr64 & 0xff) && ecb_ciphertext[14] == ((addr64 >> 8) & 0xff) &&
ecb_ciphertext[13] == ((addr64 >> 16) & 0xff);
}
bool ESP32BLETracker::has_connecting_clients_() const {
for (auto *client : this->clients_) {
auto state = client->state();
if (state == ClientState::CONNECTING || state == ClientState::READY_TO_CONNECT) {
return true;
}
}
return false;
}
#endif // USE_ESP32_BLE_DEVICE
bool ESP32BLETracker::process_scan_result_(const BLEScanResult &scan_result) {
bool found_discovered_client = false;
// Process raw advertisements
if (this->raw_advertisements_) {
for (auto *listener : this->listeners_) {
listener->parse_devices(&scan_result, 1);
}
for (auto *client : this->clients_) {
client->parse_devices(&scan_result, 1);
}
}
// Process parsed advertisements
if (this->parse_advertisements_) {
#ifdef USE_ESP32_BLE_DEVICE
ESPBTDevice device;
device.parse_scan_rst(scan_result);
bool found = false;
for (auto *listener : this->listeners_) {
if (listener->parse_device(device))
found = true;
}
for (auto *client : this->clients_) {
if (client->parse_device(device)) {
found = true;
// Check if this client is discovered and needs promotion
if (client->state() == ClientState::DISCOVERED) {
// Only check for connecting clients if we found a discovered client
// This matches the original logic: !connecting && client->state() == DISCOVERED
if (!this->has_connecting_clients_()) {
found_discovered_client = true;
}
}
}
}
if (!found && !this->scan_continuous_) {
this->print_bt_device_info(device);
}
#endif // USE_ESP32_BLE_DEVICE
}
return found_discovered_client;
}
void ESP32BLETracker::cleanup_scan_state_(bool is_stop_complete) {
ESP_LOGD(TAG, "Scan %scomplete, set scanner state to IDLE.", is_stop_complete ? "stop " : "");
#ifdef USE_ESP32_BLE_DEVICE
this->already_discovered_.clear();
#endif
this->cancel_timeout("scan");
for (auto *listener : this->listeners_)
listener->on_scan_end();
this->set_scanner_state_(ScannerState::IDLE);
}
void ESP32BLETracker::handle_scanner_failure_() {
this->stop_scan_();
if (this->scan_start_fail_count_ == std::numeric_limits<uint8_t>::max()) {
ESP_LOGE(TAG, "Scan could not restart after %d attempts, rebooting to restore stack (IDF)",
std::numeric_limits<uint8_t>::max());
App.reboot();
}
if (this->scan_start_failed_) {
ESP_LOGE(TAG, "Scan start failed: %d", this->scan_start_failed_);
this->scan_start_failed_ = ESP_BT_STATUS_SUCCESS;
}
if (this->scan_set_param_failed_) {
ESP_LOGE(TAG, "Scan set param failed: %d", this->scan_set_param_failed_);
this->scan_set_param_failed_ = ESP_BT_STATUS_SUCCESS;
}
}
void ESP32BLETracker::try_promote_discovered_clients_() {
// Only promote the first discovered client to avoid multiple simultaneous connections
for (auto *client : this->clients_) {
if (client->state() != ClientState::DISCOVERED) {
continue;
}
if (this->scanner_state_ == ScannerState::RUNNING) {
ESP_LOGD(TAG, "Stopping scan to make connection");
this->stop_scan_();
// Don't wait for scan stop complete - promote immediately.
// This is safe because ESP-IDF processes BLE commands sequentially through its internal mailbox queue.
// This guarantees that the stop scan command will be fully processed before any subsequent connect command,
// preventing race conditions or overlapping operations.
}
ESP_LOGD(TAG, "Promoting client to connect");
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
this->update_coex_preference_(true);
#endif
client->set_state(ClientState::READY_TO_CONNECT);
break;
}
}
const char *ESP32BLETracker::scanner_state_to_string_(ScannerState state) const {
switch (state) {
case ScannerState::IDLE:
return "IDLE";
case ScannerState::STARTING:
return "STARTING";
case ScannerState::RUNNING:
return "RUNNING";
case ScannerState::STOPPING:
return "STOPPING";
case ScannerState::FAILED:
return "FAILED";
default:
return "UNKNOWN";
}
}
void ESP32BLETracker::log_unexpected_state_(const char *operation, ScannerState expected_state) const {
ESP_LOGE(TAG, "Unexpected state: %s on %s, expected: %s", this->scanner_state_to_string_(this->scanner_state_),
operation, this->scanner_state_to_string_(expected_state));
}
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
void ESP32BLETracker::update_coex_preference_(bool force_ble) {
if (force_ble && !this->coex_prefer_ble_) {
ESP_LOGD(TAG, "Setting coexistence to Bluetooth to make connection.");
this->coex_prefer_ble_ = true;
esp_coex_preference_set(ESP_COEX_PREFER_BT); // Prioritize Bluetooth
} else if (!force_ble && this->coex_prefer_ble_) {
ESP_LOGD(TAG, "Setting coexistence preference to balanced.");
this->coex_prefer_ble_ = false;
esp_coex_preference_set(ESP_COEX_PREFER_BALANCE); // Reset to default
}
}
#endif
} // namespace esphome::esp32_ble_tracker
#endif // USE_ESP32

View File

@@ -6,6 +6,7 @@
#include "esphome/core/helpers.h"
#include <array>
#include <atomic>
#include <string>
#include <vector>
@@ -20,7 +21,6 @@
#include "esphome/components/esp32_ble/ble.h"
#include "esphome/components/esp32_ble/ble_uuid.h"
#include "esphome/components/esp32_ble/ble_scan_result.h"
namespace esphome::esp32_ble_tracker {
@@ -136,20 +136,6 @@ class ESPBTDeviceListener {
ESP32BLETracker *parent_{nullptr};
};
struct ClientStateCounts {
uint8_t connecting = 0;
uint8_t discovered = 0;
uint8_t searching = 0;
uint8_t disconnecting = 0;
bool operator==(const ClientStateCounts &other) const {
return connecting == other.connecting && discovered == other.discovered && searching == other.searching &&
disconnecting == other.disconnecting;
}
bool operator!=(const ClientStateCounts &other) const { return !(*this == other); }
};
enum class ClientState : uint8_t {
// Connection is allocated
INIT,
@@ -172,21 +158,20 @@ enum class ClientState : uint8_t {
};
enum class ScannerState {
// Scanner is idle, init state
// Scanner is idle, init state, set from the main loop when processing STOPPED
IDLE,
// Scanner is starting
// Scanner is starting, set from the main loop only
STARTING,
// Scanner is running
// Scanner is running, set from the ESP callback only
RUNNING,
// Scanner failed to start
// Scanner failed to start, set from the ESP callback only
FAILED,
// Scanner is stopping
// Scanner is stopping, set from the main loop only
STOPPING,
// Scanner is stopped, set from the ESP callback only
STOPPED,
};
// Helper function to convert ClientState to string
const char *client_state_to_string(ClientState state);
enum class ConnectionType : uint8_t {
// The default connection type, we hold all the services in ram
// for the duration of the connection.
@@ -277,6 +262,8 @@ class ESP32BLETracker : public Component,
void stop_scan_();
/// Start a single scan by setting up the parameters and doing some esp-idf calls.
void start_scan_(bool first);
/// Called when a scan ends
void end_of_scan_();
/// Called when a `ESP_GAP_BLE_SCAN_RESULT_EVT` event is received.
void gap_scan_result_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param &param);
/// Called when a `ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT` event is received.
@@ -287,58 +274,11 @@ class ESP32BLETracker : public Component,
void gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param &param);
/// Called to set the scanner state. Will also call callbacks to let listeners know when state is changed.
void set_scanner_state_(ScannerState state);
/// Common cleanup logic when transitioning scanner to IDLE state
void cleanup_scan_state_(bool is_stop_complete);
/// Process a single scan result immediately
/// Returns true if a discovered client needs promotion to READY_TO_CONNECT
bool process_scan_result_(const BLEScanResult &scan_result);
#ifdef USE_ESP32_BLE_DEVICE
/// Check if any clients are in connecting or ready to connect state
bool has_connecting_clients_() const;
#endif
/// Handle scanner failure states
void handle_scanner_failure_();
/// Try to promote discovered clients to ready to connect
void try_promote_discovered_clients_();
/// Convert scanner state enum to string for logging
const char *scanner_state_to_string_(ScannerState state) const;
/// Log an unexpected scanner state
void log_unexpected_state_(const char *operation, ScannerState expected_state) const;
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
/// Update BLE coexistence preference
void update_coex_preference_(bool force_ble);
#endif
/// Count clients in each state
ClientStateCounts count_client_states_() const {
ClientStateCounts counts;
for (auto *client : this->clients_) {
switch (client->state()) {
case ClientState::DISCONNECTING:
counts.disconnecting++;
break;
case ClientState::DISCOVERED:
counts.discovered++;
break;
case ClientState::SEARCHING:
counts.searching++;
break;
case ClientState::CONNECTING:
case ClientState::READY_TO_CONNECT:
counts.connecting++;
break;
default:
break;
}
}
return counts;
}
uint8_t app_id_{0};
#ifdef USE_ESP32_BLE_DEVICE
/// Vector of addresses that have already been printed in print_bt_device_info
std::vector<uint64_t> already_discovered_;
#endif
std::vector<ESPBTDeviceListener *> listeners_;
/// Client parameters.
std::vector<ESPBTClient *> clients_;
@@ -357,9 +297,21 @@ class ESP32BLETracker : public Component,
bool raw_advertisements_{false};
bool parse_advertisements_{false};
// Lock-free Single-Producer Single-Consumer (SPSC) ring buffer for scan results
// Producer: ESP-IDF Bluetooth stack callback (gap_scan_event_handler)
// Consumer: ESPHome main loop (loop() method)
// This design ensures zero blocking in the BT callback and prevents scan result loss
BLEScanResult *scan_ring_buffer_;
std::atomic<uint8_t> ring_write_index_{0}; // Written only by BT callback (producer)
std::atomic<uint8_t> ring_read_index_{0}; // Written only by main loop (consumer)
std::atomic<uint16_t> scan_results_dropped_{0}; // Tracks buffer overflow events
esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS};
esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS};
ClientStateCounts client_state_counts_;
int connecting_{0};
int discovered_{0};
int searching_{0};
int disconnecting_{0};
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
bool coex_prefer_ble_{false};
#endif

View File

@@ -345,7 +345,7 @@ async def to_code(config):
cg.add_define("USE_CAMERA")
if CORE.using_esp_idf:
add_idf_component(name="espressif/esp32-camera", ref="2.1.1")
add_idf_component(name="espressif/esp32-camera", ref="2.0.15")
for conf in config.get(CONF_ON_STREAM_START, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)

View File

@@ -2,7 +2,11 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#if defined(USE_ESP32_VARIANT_ESP32) || defined(USE_ESP32_VARIANT_ESP32S2)
#ifdef USE_ESP32
#ifdef USE_ARDUINO
#include <esp32-hal-dac.h>
#endif
namespace esphome {
namespace esp32_dac {
@@ -19,12 +23,18 @@ void ESP32DAC::setup() {
this->pin_->setup();
this->turn_off();
#ifdef USE_ESP_IDF
const dac_channel_t channel = this->pin_->get_pin() == DAC0_PIN ? DAC_CHAN_0 : DAC_CHAN_1;
const dac_oneshot_config_t oneshot_cfg{channel};
dac_oneshot_new_channel(&oneshot_cfg, &this->dac_handle_);
#endif
}
void ESP32DAC::on_safe_shutdown() { dac_oneshot_del_channel(this->dac_handle_); }
void ESP32DAC::on_safe_shutdown() {
#ifdef USE_ESP_IDF
dac_oneshot_del_channel(this->dac_handle_);
#endif
}
void ESP32DAC::dump_config() {
ESP_LOGCONFIG(TAG, "ESP32 DAC:");
@@ -38,10 +48,15 @@ void ESP32DAC::write_state(float state) {
state = state * 255;
#ifdef USE_ESP_IDF
dac_oneshot_output_voltage(this->dac_handle_, state);
#endif
#ifdef USE_ARDUINO
dacWrite(this->pin_->get_pin(), state);
#endif
}
} // namespace esp32_dac
} // namespace esphome
#endif // USE_ESP32_VARIANT_ESP32 || USE_ESP32_VARIANT_ESP32S2
#endif

View File

@@ -1,13 +1,15 @@
#pragma once
#include "esphome/components/output/float_output.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/automation.h"
#include "esphome/components/output/float_output.h"
#if defined(USE_ESP32_VARIANT_ESP32) || defined(USE_ESP32_VARIANT_ESP32S2)
#ifdef USE_ESP32
#ifdef USE_ESP_IDF
#include <driver/dac_oneshot.h>
#endif
namespace esphome {
namespace esp32_dac {
@@ -27,10 +29,12 @@ class ESP32DAC : public output::FloatOutput, public Component {
void write_state(float state) override;
InternalGPIOPin *pin_;
#ifdef USE_ESP_IDF
dac_oneshot_handle_t dac_handle_;
#endif
};
} // namespace esp32_dac
} // namespace esphome
#endif // USE_ESP32_VARIANT_ESP32 || USE_ESP32_VARIANT_ESP32S2
#endif

View File

@@ -42,6 +42,9 @@ static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size
symbols[i] = params->bit0;
}
}
if ((index + 1) >= size && params->reset.duration0 == 0 && params->reset.duration1 == 0) {
*done = true;
}
return RMT_SYMBOLS_PER_BYTE;
}
@@ -107,7 +110,7 @@ void ESP32RMTLEDStripLightOutput::setup() {
memset(&encoder, 0, sizeof(encoder));
encoder.callback = encoder_callback;
encoder.arg = &this->params_;
encoder.min_chunk_size = RMT_SYMBOLS_PER_BYTE;
encoder.min_chunk_size = 8;
if (rmt_new_simple_encoder(&encoder, &this->encoder_) != ESP_OK) {
ESP_LOGE(TAG, "Encoder creation failed");
this->mark_failed();

View File

@@ -65,6 +65,15 @@ CONF_WAIT_FOR_SENT = "wait_for_sent"
MAX_ESPNOW_PACKET_SIZE = 250 # Maximum size of the payload in bytes
def _validate_unknown_peer(config):
if config[CONF_AUTO_ADD_PEER] and config.get(CONF_ON_UNKNOWN_PEER):
raise cv.Invalid(
f"'{CONF_ON_UNKNOWN_PEER}' cannot be used when '{CONF_AUTO_ADD_PEER}' is enabled.",
path=[CONF_ON_UNKNOWN_PEER],
)
return config
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
@@ -94,6 +103,7 @@ CONFIG_SCHEMA = cv.All(
},
).extend(cv.COMPONENT_SCHEMA),
cv.only_on_esp32,
_validate_unknown_peer,
)
@@ -114,6 +124,7 @@ async def _trigger_to_code(config):
async def to_code(config):
print(config)
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)

View File

@@ -40,20 +40,20 @@ template<typename... Ts> class SendAction : public Action<Ts...>, public Parente
this->num_running_++;
send_callback_t send_callback = [this, x...](esp_err_t status) {
if (status == ESP_OK) {
if (!this->sent_.empty()) {
this->sent_.play(x...);
} else if (this->flags_.wait_for_sent) {
if (this->sent_.empty() && this->flags_.wait_for_sent) {
this->play_next_(x...);
} else if (!this->sent_.empty()) {
this->sent_.play(x...);
}
} else {
if (!this->error_.empty()) {
this->error_.play(x...);
} else if (this->flags_.wait_for_sent) {
if (this->error_.empty() && this->flags_.wait_for_sent) {
if (this->flags_.continue_on_error) {
this->play_next_(x...);
} else {
this->stop_complex();
}
} else if (!this->error_.empty()) {
this->error_.play(x...);
}
}
};

View File

@@ -154,7 +154,7 @@ void ESPNowComponent::setup() {
}
void ESPNowComponent::enable() {
if (this->state_ == ESPNOW_STATE_ENABLED)
if (this->state_ != ESPNOW_STATE_ENABLED)
return;
ESP_LOGD(TAG, "Enabling");
@@ -178,7 +178,11 @@ void ESPNowComponent::enable_() {
this->apply_wifi_channel();
}
this->get_wifi_channel();
#ifdef USE_WIFI
else {
this->wifi_channel_ = wifi::global_wifi_component->get_wifi_channel();
}
#endif
esp_err_t err = esp_now_init();
if (err != ESP_OK) {
@@ -211,7 +215,6 @@ void ESPNowComponent::enable_() {
for (auto peer : this->peers_) {
this->add_peer(peer.address);
}
this->state_ = ESPNOW_STATE_ENABLED;
}
@@ -225,6 +228,10 @@ void ESPNowComponent::disable() {
esp_now_unregister_recv_cb();
esp_now_unregister_send_cb();
for (auto peer : this->peers_) {
this->del_peer(peer.address);
}
esp_err_t err = esp_now_deinit();
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_now_deinit failed! 0x%x", err);
@@ -260,6 +267,7 @@ void ESPNowComponent::loop() {
}
}
#endif
// Process received packets
ESPNowPacket *packet = this->receive_packet_queue_.pop();
while (packet != nullptr) {
@@ -267,15 +275,13 @@ void ESPNowComponent::loop() {
case ESPNowPacket::RECEIVED: {
const ESPNowRecvInfo info = packet->get_receive_info();
if (!esp_now_is_peer_exist(info.src_addr)) {
bool handled = false;
for (auto *handler : this->unknown_peer_handlers_) {
if (handler->on_unknown_peer(info, packet->packet_.receive.data, packet->packet_.receive.size)) {
handled = true;
break; // If a handler returns true, stop processing further handlers
}
}
if (!handled && this->auto_add_peer_) {
if (this->auto_add_peer_) {
this->add_peer(info.src_addr);
} else {
for (auto *handler : this->unknown_peer_handlers_) {
if (handler->on_unknown_peer(info, packet->packet_.receive.data, packet->packet_.receive.size))
break; // If a handler returns true, stop processing further handlers
}
}
}
// Intentionally left as if instead of else in case the peer is added above
@@ -337,12 +343,6 @@ void ESPNowComponent::loop() {
}
}
uint8_t ESPNowComponent::get_wifi_channel() {
wifi_second_chan_t dummy;
esp_wifi_get_channel(&this->wifi_channel_, &dummy);
return this->wifi_channel_;
}
esp_err_t ESPNowComponent::send(const uint8_t *peer_address, const uint8_t *payload, size_t size,
const send_callback_t &callback) {
if (this->state_ != ESPNOW_STATE_ENABLED) {

View File

@@ -110,7 +110,6 @@ class ESPNowComponent : public Component {
void set_wifi_channel(uint8_t channel) { this->wifi_channel_ = channel; }
void apply_wifi_channel();
uint8_t get_wifi_channel();
void set_auto_add_peer(bool value) { this->auto_add_peer_ = value; }

View File

@@ -49,7 +49,7 @@ class ESPNowPacket {
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
// Constructor for sent data
ESPNowPacket(const esp_now_send_info_t *info, esp_now_send_status_t status) {
this->init_sent_data_(info->src_addr, status);
this->init_sent_data(info->src_addr, status);
}
#else
// Constructor for sent data

View File

@@ -15,7 +15,6 @@ from freetype import (
FT_LOAD_RENDER,
FT_LOAD_TARGET_MONO,
Face,
FT_Exception,
ft_pixel_mode_mono,
)
import requests
@@ -95,14 +94,7 @@ class FontCache(MutableMapping):
return self.store[self._keytransform(item)]
def __setitem__(self, key, value):
transformed = self._keytransform(key)
try:
self.store[transformed] = Face(str(value))
except FT_Exception as exc:
file = transformed.split(":", 1)
raise cv.Invalid(
f"{file[0].capitalize()} {file[1]} is not a valid font file"
) from exc
self.store[self._keytransform(key)] = Face(str(value))
FONT_CACHE = FontCache()

View File

@@ -2,11 +2,10 @@
#include <array>
#include <cstdint>
#include <cstring>
#include <limits>
#include "esphome/core/hal.h"
namespace esphome::gpio_expander {
namespace esphome {
namespace gpio_expander {
/// @brief A class to cache the read state of a GPIO expander.
/// This class caches reads between GPIO Pins which are on the same bank.
@@ -18,22 +17,12 @@ namespace esphome::gpio_expander {
/// N - Number of pins
template<typename T, T N> class CachedGpioExpander {
public:
/// @brief Read the state of the given pin. This will invalidate the cache for the given pin number.
/// @param pin Pin number to read
/// @return Pin state
bool digital_read(T pin) {
const uint8_t bank = pin / BANK_SIZE;
const T pin_mask = (1 << (pin % BANK_SIZE));
// Check if specific pin cache is valid
if (this->read_cache_valid_[bank] & pin_mask) {
// Invalidate pin
this->read_cache_valid_[bank] &= ~pin_mask;
} else {
// Read whole bank from hardware
uint8_t bank = pin / (sizeof(T) * BITS_PER_BYTE);
if (this->read_cache_invalidated_[bank]) {
this->read_cache_invalidated_[bank] = false;
if (!this->digital_read_hw(pin))
return false;
// Mark bank cache as valid except the pin that is being returned now
this->read_cache_valid_[bank] = std::numeric_limits<T>::max() & ~pin_mask;
}
return this->digital_read_cache(pin);
}
@@ -47,16 +36,18 @@ template<typename T, T N> class CachedGpioExpander {
virtual bool digital_read_cache(T pin) = 0;
/// @brief Call component low level function to write GPIO state to device
virtual void digital_write_hw(T pin, bool value) = 0;
const uint8_t cache_byte_size_ = N / (sizeof(T) * BITS_PER_BYTE);
/// @brief Invalidate cache. This function should be called in component loop().
void reset_pin_cache_() { memset(this->read_cache_valid_, 0x00, CACHE_SIZE_BYTES); }
void reset_pin_cache_() {
for (T i = 0; i < this->cache_byte_size_; i++) {
this->read_cache_invalidated_[i] = true;
}
}
static constexpr uint8_t BITS_PER_BYTE = 8;
static constexpr uint8_t BANK_SIZE = sizeof(T) * BITS_PER_BYTE;
static constexpr size_t BANKS = N / BANK_SIZE;
static constexpr size_t CACHE_SIZE_BYTES = BANKS * sizeof(T);
T read_cache_valid_[BANKS]{0};
static const uint8_t BITS_PER_BYTE = 8;
std::array<bool, N / (sizeof(T) * BITS_PER_BYTE)> read_cache_invalidated_{};
};
} // namespace esphome::gpio_expander
} // namespace gpio_expander
} // namespace esphome

View File

@@ -20,11 +20,12 @@ static const size_t MAX_BUTTONS = 4; // max number of buttons scanned
#define ERROR_CHECK(err) \
if ((err) != i2c::ERROR_OK) { \
this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); \
this->status_set_warning("Communication failure"); \
return; \
}
void GT911Touchscreen::setup() {
i2c::ErrorCode err;
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup();
this->reset_pin_->digital_write(false);
@@ -34,14 +35,9 @@ void GT911Touchscreen::setup() {
this->interrupt_pin_->digital_write(false);
}
delay(2);
this->reset_pin_->digital_write(true); // wait 50ms after reset
this->set_timeout(50, [this] { this->setup_internal_(); });
return;
this->reset_pin_->digital_write(true);
delay(50); // NOLINT
}
this->setup_internal_();
}
void GT911Touchscreen::setup_internal_() {
if (this->interrupt_pin_ != nullptr) {
// set pre-configured input mode
this->interrupt_pin_->setup();
@@ -49,7 +45,7 @@ void GT911Touchscreen::setup_internal_() {
// check the configuration of the int line.
uint8_t data[4];
i2c::ErrorCode err = this->write(GET_SWITCHES, sizeof(GET_SWITCHES));
err = this->write(GET_SWITCHES, sizeof(GET_SWITCHES));
if (err != i2c::ERROR_OK && this->address_ == PRIMARY_ADDRESS) {
this->address_ = SECONDARY_ADDRESS;
err = this->write(GET_SWITCHES, sizeof(GET_SWITCHES));
@@ -57,7 +53,7 @@ void GT911Touchscreen::setup_internal_() {
if (err == i2c::ERROR_OK) {
err = this->read(data, 1);
if (err == i2c::ERROR_OK) {
ESP_LOGD(TAG, "Switches ADDR: 0x%02X DATA: 0x%02X", this->address_, data[0]);
ESP_LOGD(TAG, "Read from switches at address 0x%02X: 0x%02X", this->address_, data[0]);
if (this->interrupt_pin_ != nullptr) {
this->attach_interrupt_(this->interrupt_pin_,
(data[0] & 1) ? gpio::INTERRUPT_FALLING_EDGE : gpio::INTERRUPT_RISING_EDGE);
@@ -79,24 +75,16 @@ void GT911Touchscreen::setup_internal_() {
}
}
if (err != i2c::ERROR_OK) {
this->mark_failed("Calibration error");
this->mark_failed("Failed to read calibration");
return;
}
}
if (err != i2c::ERROR_OK) {
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
return;
this->mark_failed("Failed to communicate");
}
this->setup_done_ = true;
}
void GT911Touchscreen::update_touches() {
this->skip_update_ = true; // skip send touch events by default, set to false after successful error checks
if (!this->setup_done_) {
return;
}
i2c::ErrorCode err;
uint8_t touch_state = 0;
uint8_t data[MAX_TOUCHES + 1][8]; // 8 bytes each for each point, plus extra space for the key byte
@@ -109,6 +97,7 @@ void GT911Touchscreen::update_touches() {
uint8_t num_of_touches = touch_state & 0x07;
if ((touch_state & 0x80) == 0 || num_of_touches > MAX_TOUCHES) {
this->skip_update_ = true; // skip send touch events, touchscreen is not ready yet.
return;
}
@@ -118,7 +107,6 @@ void GT911Touchscreen::update_touches() {
err = this->read(data[0], sizeof(data[0]) * num_of_touches + 1);
ERROR_CHECK(err);
this->skip_update_ = false; // All error checks passed, send touch events
for (uint8_t i = 0; i != num_of_touches; i++) {
uint16_t id = data[i][0];
uint16_t x = encode_uint16(data[i][2], data[i][1]);

View File

@@ -15,20 +15,8 @@ class GT911ButtonListener {
class GT911Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice {
public:
/// @brief Initialize the GT911 touchscreen.
///
/// If @ref reset_pin_ is set, the touchscreen will be hardware reset,
/// and the rest of the setup will be scheduled to run 50ms later using @ref set_timeout()
/// to allow the device to stabilize after reset.
///
/// If @ref interrupt_pin_ is set, it will be temporarily configured during reset
/// to control I2C address selection.
///
/// After the timeout, or immediately if no reset is performed, @ref setup_internal_()
/// is called to complete the initialization.
void setup() override;
void dump_config() override;
bool can_proceed() override { return this->setup_done_; }
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; }
@@ -37,20 +25,8 @@ class GT911Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice
protected:
void update_touches() override;
/// @brief Perform the internal setup routine for the GT911 touchscreen.
///
/// This function checks the I2C address, configures the interrupt pin (if available),
/// reads the touchscreen mode from the controller, and attempts to read calibration
/// data (maximum X and Y values) if not already set.
///
/// On success, sets @ref setup_done_ to true.
/// On failure, calls @ref mark_failed() with an appropriate error message.
void setup_internal_();
/// @brief True if the touchscreen setup has completed successfully.
bool setup_done_{false};
InternalGPIOPin *interrupt_pin_{nullptr};
GPIOPin *reset_pin_{nullptr};
InternalGPIOPin *interrupt_pin_{};
GPIOPin *reset_pin_{};
std::vector<GT911ButtonListener *> button_listeners_;
uint8_t button_state_{0xFF}; // last button state. Initial FF guarantees first update.
};

View File

@@ -159,7 +159,7 @@ void IDFI2CBus::dump_config() {
if (s.second) {
ESP_LOGCONFIG(TAG, "Found device at address 0x%02X", s.first);
} else {
ESP_LOGCONFIG(TAG, "Unknown error at address 0x%02X", s.first);
ESP_LOGE(TAG, "Unknown error at address 0x%02X", s.first);
}
}
}

View File

@@ -24,6 +24,9 @@ static const uint32_t READ_DURATION_MS = 16;
static const size_t TASK_STACK_SIZE = 4096;
static const ssize_t TASK_PRIORITY = 23;
// Use an exponential moving average to correct a DC offset with weight factor 1/1000
static const int32_t DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR = 1000;
static const char *const TAG = "i2s_audio.microphone";
enum MicrophoneEventGroupBits : uint32_t {
@@ -378,57 +381,26 @@ void I2SAudioMicrophone::mic_task(void *params) {
}
void I2SAudioMicrophone::fix_dc_offset_(std::vector<uint8_t> &data) {
/**
* From https://www.musicdsp.org/en/latest/Filters/135-dc-filter.html:
*
* y(n) = x(n) - x(n-1) + R * y(n-1)
* R = 1 - (pi * 2 * frequency / samplerate)
*
* From https://en.wikipedia.org/wiki/Hearing_range:
* The human range is commonly given as 20Hz up.
*
* From https://en.wikipedia.org/wiki/High-resolution_audio:
* A reasonable upper bound for sample rate seems to be 96kHz.
*
* Calculate R value for 20Hz on a 96kHz sample rate:
* R = 1 - (pi * 2 * 20 / 96000)
* R = 0.9986910031
*
* Transform floating point to bit-shifting approximation:
* output = input - prev_input + R * prev_output
* output = input - prev_input + (prev_output - (prev_output >> S))
*
* Approximate bit-shift value S from R:
* R = 1 - (1 >> S)
* R = 1 - (1 / 2^S)
* R = 1 - 2^-S
* 0.9986910031 = 1 - 2^-S
* S = 9.57732 ~= 10
*
* Actual R from S:
* R = 1 - 2^-10 = 0.9990234375
*
* Confirm this has effect outside human hearing on 96000kHz sample:
* 0.9990234375 = 1 - (pi * 2 * f / 96000)
* f = 14.9208Hz
*
* Confirm this has effect outside human hearing on PDM 16kHz sample:
* 0.9990234375 = 1 - (pi * 2 * f / 16000)
* f = 2.4868Hz
*
*/
const uint8_t dc_filter_shift = 10;
const size_t bytes_per_sample = this->audio_stream_info_.samples_to_bytes(1);
const uint32_t total_samples = this->audio_stream_info_.bytes_to_samples(data.size());
if (total_samples == 0) {
return;
}
int64_t offset_accumulator = 0;
for (uint32_t sample_index = 0; sample_index < total_samples; ++sample_index) {
const uint32_t byte_index = sample_index * bytes_per_sample;
int32_t input = audio::unpack_audio_sample_to_q31(&data[byte_index], bytes_per_sample);
int32_t output = input - this->dc_offset_prev_input_ +
(this->dc_offset_prev_output_ - (this->dc_offset_prev_output_ >> dc_filter_shift));
this->dc_offset_prev_input_ = input;
this->dc_offset_prev_output_ = output;
audio::pack_q31_as_audio_sample(output, &data[byte_index], bytes_per_sample);
int32_t sample = audio::unpack_audio_sample_to_q31(&data[byte_index], bytes_per_sample);
offset_accumulator += sample;
sample -= this->dc_offset_;
audio::pack_q31_as_audio_sample(sample, &data[byte_index], bytes_per_sample);
}
const int32_t new_offset = offset_accumulator / total_samples;
this->dc_offset_ = new_offset / DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR +
(DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR - 1) * this->dc_offset_ /
DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR;
}
size_t I2SAudioMicrophone::read_(uint8_t *buf, size_t len, TickType_t ticks_to_wait) {

View File

@@ -82,8 +82,7 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub
bool correct_dc_offset_;
bool locked_driver_{false};
int32_t dc_offset_prev_input_{0};
int32_t dc_offset_prev_output_{0};
int32_t dc_offset_{0};
};
} // namespace i2s_audio

View File

@@ -2,7 +2,7 @@ import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.core import coroutine_with_priority
CODEOWNERS = ["@esphome/core"]
CODEOWNERS = ["@OttoWinter"]
json_ns = cg.esphome_ns.namespace("json")
CONFIG_SCHEMA = cv.All(

View File

@@ -29,7 +29,6 @@ from esphome.const import (
CONF_WHITE,
CONF_WIDTH,
)
from esphome.cpp_generator import MockObjClass
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from esphome.util import Registry
@@ -89,15 +88,8 @@ ADDRESSABLE_EFFECTS = []
EFFECTS_REGISTRY = Registry()
def register_effect(
name: str,
effect_type: MockObjClass,
default_name: str,
schema: cv.Schema | dict,
*extra_validators,
):
schema = schema if isinstance(schema, cv.Schema) else cv.Schema(schema)
schema = schema.extend(
def register_effect(name, effect_type, default_name, schema, *extra_validators):
schema = cv.Schema(schema).extend(
{
cv.Optional(CONF_NAME, default=default_name): cv.string_strict,
}
@@ -106,13 +98,7 @@ def register_effect(
return EFFECTS_REGISTRY.register(name, effect_type, validator)
def register_binary_effect(
name: str,
effect_type: MockObjClass,
default_name: str,
schema: cv.Schema | dict,
*extra_validators,
):
def register_binary_effect(name, effect_type, default_name, schema, *extra_validators):
# binary effect can be used for all lights
BINARY_EFFECTS.append(name)
MONOCHROMATIC_EFFECTS.append(name)
@@ -123,11 +109,7 @@ def register_binary_effect(
def register_monochromatic_effect(
name: str,
effect_type: MockObjClass,
default_name: str,
schema: cv.Schema | dict,
*extra_validators,
name, effect_type, default_name, schema, *extra_validators
):
# monochromatic effect can be used for all lights expect binary
MONOCHROMATIC_EFFECTS.append(name)
@@ -137,13 +119,7 @@ def register_monochromatic_effect(
return register_effect(name, effect_type, default_name, schema, *extra_validators)
def register_rgb_effect(
name: str,
effect_type: MockObjClass,
default_name: str,
schema: cv.Schema | dict,
*extra_validators,
):
def register_rgb_effect(name, effect_type, default_name, schema, *extra_validators):
# RGB effect can be used for RGB and addressable lights
RGB_EFFECTS.append(name)
ADDRESSABLE_EFFECTS.append(name)
@@ -152,11 +128,7 @@ def register_rgb_effect(
def register_addressable_effect(
name: str,
effect_type: MockObjClass,
default_name: str,
schema: cv.Schema | dict,
*extra_validators,
name, effect_type, default_name, schema, *extra_validators
):
# addressable effect can be used only in addressable
ADDRESSABLE_EFFECTS.append(name)

View File

@@ -24,5 +24,5 @@ CONFIG_SCHEMA = (
async def to_code(config):
var = await mcp23xxx_base.register_mcp23xxx(config, mcp23x08_base.NUM_PINS)
var = await mcp23xxx_base.register_mcp23xxx(config)
await i2c.register_i2c_device(var, config)

View File

@@ -24,5 +24,5 @@ CONFIG_SCHEMA = (
async def to_code(config):
var = await mcp23xxx_base.register_mcp23xxx(config, mcp23x17_base.NUM_PINS)
var = await mcp23xxx_base.register_mcp23xxx(config)
await i2c.register_i2c_device(var, config)

View File

@@ -27,6 +27,6 @@ CONFIG_SCHEMA = (
async def to_code(config):
var = await mcp23xxx_base.register_mcp23xxx(config, mcp23x08_base.NUM_PINS)
var = await mcp23xxx_base.register_mcp23xxx(config)
cg.add(var.set_device_address(config[CONF_DEVICEADDRESS]))
await spi.register_spi_device(var, config)

View File

@@ -27,6 +27,6 @@ CONFIG_SCHEMA = (
async def to_code(config):
var = await mcp23xxx_base.register_mcp23xxx(config, mcp23x17_base.NUM_PINS)
var = await mcp23xxx_base.register_mcp23xxx(config)
cg.add(var.set_device_address(config[CONF_DEVICEADDRESS]))
await spi.register_spi_device(var, config)

View File

@@ -4,7 +4,5 @@ from esphome.components import mcp23xxx_base
AUTO_LOAD = ["mcp23xxx_base"]
CODEOWNERS = ["@jesserockz"]
NUM_PINS = 8
mcp23x08_base_ns = cg.esphome_ns.namespace("mcp23x08_base")
MCP23X08Base = mcp23x08_base_ns.class_("MCP23X08Base", mcp23xxx_base.MCP23XXXBase)

View File

@@ -6,21 +6,19 @@ namespace mcp23x08_base {
static const char *const TAG = "mcp23x08_base";
bool MCP23X08Base::digital_read_hw(uint8_t pin) {
if (!this->read_reg(mcp23x08_base::MCP23X08_GPIO, &this->input_mask_)) {
this->status_set_warning(ESP_LOG_MSG_COMM_FAIL);
return false;
}
return true;
bool MCP23X08Base::digital_read(uint8_t pin) {
uint8_t bit = pin % 8;
uint8_t reg_addr = mcp23x08_base::MCP23X08_GPIO;
uint8_t value = 0;
this->read_reg(reg_addr, &value);
return value & (1 << bit);
}
void MCP23X08Base::digital_write_hw(uint8_t pin, bool value) {
void MCP23X08Base::digital_write(uint8_t pin, bool value) {
uint8_t reg_addr = mcp23x08_base::MCP23X08_OLAT;
this->update_reg(pin, value, reg_addr);
}
bool MCP23X08Base::digital_read_cache(uint8_t pin) { return this->input_mask_ & (1 << pin); }
void MCP23X08Base::pin_mode(uint8_t pin, gpio::Flags flags) {
uint8_t iodir = mcp23x08_base::MCP23X08_IODIR;
uint8_t gppu = mcp23x08_base::MCP23X08_GPPU;

View File

@@ -1,7 +1,7 @@
#pragma once
#include "esphome/components/mcp23xxx_base/mcp23xxx_base.h"
#include "esphome/core/component.h"
#include "esphome/components/mcp23xxx_base/mcp23xxx_base.h"
#include "esphome/core/hal.h"
namespace esphome {
@@ -22,12 +22,10 @@ enum MCP23S08GPIORegisters {
MCP23X08_OLAT = 0x0A,
};
class MCP23X08Base : public mcp23xxx_base::MCP23XXXBase<8> {
class MCP23X08Base : public mcp23xxx_base::MCP23XXXBase {
public:
bool digital_read_hw(uint8_t pin) override;
void digital_write_hw(uint8_t pin, bool value) override;
bool digital_read_cache(uint8_t pin) override;
bool digital_read(uint8_t pin) override;
void digital_write(uint8_t pin, bool value) override;
void pin_mode(uint8_t pin, gpio::Flags flags) override;
void pin_interrupt_mode(uint8_t pin, mcp23xxx_base::MCP23XXXInterruptMode interrupt_mode) override;
@@ -35,9 +33,6 @@ class MCP23X08Base : public mcp23xxx_base::MCP23XXXBase<8> {
void update_reg(uint8_t pin, bool pin_value, uint8_t reg_a) override;
uint8_t olat_{0x00};
/// State read in digital_read_hw
uint8_t input_mask_{0x00};
};
} // namespace mcp23x08_base

View File

@@ -4,7 +4,5 @@ from esphome.components import mcp23xxx_base
AUTO_LOAD = ["mcp23xxx_base"]
CODEOWNERS = ["@jesserockz"]
NUM_PINS = 16
mcp23x17_base_ns = cg.esphome_ns.namespace("mcp23x17_base")
MCP23X17Base = mcp23x17_base_ns.class_("MCP23X17Base", mcp23xxx_base.MCP23XXXBase)

View File

@@ -1,5 +1,4 @@
#include "mcp23x17_base.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
@@ -7,31 +6,19 @@ namespace mcp23x17_base {
static const char *const TAG = "mcp23x17_base";
bool MCP23X17Base::digital_read_hw(uint8_t pin) {
uint8_t data;
if (pin < 8) {
if (!this->read_reg(mcp23x17_base::MCP23X17_GPIOA, &data)) {
this->status_set_warning(ESP_LOG_MSG_COMM_FAIL);
return false;
}
this->input_mask_ = encode_uint16(this->input_mask_ >> 8, data);
} else {
if (!this->read_reg(mcp23x17_base::MCP23X17_GPIOB, &data)) {
this->status_set_warning(ESP_LOG_MSG_COMM_FAIL);
return false;
}
this->input_mask_ = encode_uint16(data, this->input_mask_ & 0xFF);
}
return true;
bool MCP23X17Base::digital_read(uint8_t pin) {
uint8_t bit = pin % 8;
uint8_t reg_addr = pin < 8 ? mcp23x17_base::MCP23X17_GPIOA : mcp23x17_base::MCP23X17_GPIOB;
uint8_t value = 0;
this->read_reg(reg_addr, &value);
return value & (1 << bit);
}
void MCP23X17Base::digital_write_hw(uint8_t pin, bool value) {
void MCP23X17Base::digital_write(uint8_t pin, bool value) {
uint8_t reg_addr = pin < 8 ? mcp23x17_base::MCP23X17_OLATA : mcp23x17_base::MCP23X17_OLATB;
this->update_reg(pin, value, reg_addr);
}
bool MCP23X17Base::digital_read_cache(uint8_t pin) { return this->input_mask_ & (1 << pin); }
void MCP23X17Base::pin_mode(uint8_t pin, gpio::Flags flags) {
uint8_t iodir = pin < 8 ? mcp23x17_base::MCP23X17_IODIRA : mcp23x17_base::MCP23X17_IODIRB;
uint8_t gppu = pin < 8 ? mcp23x17_base::MCP23X17_GPPUA : mcp23x17_base::MCP23X17_GPPUB;

View File

@@ -1,7 +1,7 @@
#pragma once
#include "esphome/components/mcp23xxx_base/mcp23xxx_base.h"
#include "esphome/core/component.h"
#include "esphome/components/mcp23xxx_base/mcp23xxx_base.h"
#include "esphome/core/hal.h"
namespace esphome {
@@ -34,12 +34,10 @@ enum MCP23X17GPIORegisters {
MCP23X17_OLATB = 0x15,
};
class MCP23X17Base : public mcp23xxx_base::MCP23XXXBase<16> {
class MCP23X17Base : public mcp23xxx_base::MCP23XXXBase {
public:
bool digital_read_hw(uint8_t pin) override;
void digital_write_hw(uint8_t pin, bool value) override;
bool digital_read_cache(uint8_t pin) override;
bool digital_read(uint8_t pin) override;
void digital_write(uint8_t pin, bool value) override;
void pin_mode(uint8_t pin, gpio::Flags flags) override;
void pin_interrupt_mode(uint8_t pin, mcp23xxx_base::MCP23XXXInterruptMode interrupt_mode) override;
@@ -48,9 +46,6 @@ class MCP23X17Base : public mcp23xxx_base::MCP23XXXBase<16> {
uint8_t olat_a_{0x00};
uint8_t olat_b_{0x00};
/// State read in digital_read_hw
uint16_t input_mask_{0x00};
};
} // namespace mcp23x17_base

View File

@@ -12,9 +12,8 @@ from esphome.const import (
CONF_OUTPUT,
CONF_PULLUP,
)
from esphome.core import CORE, ID, coroutine
from esphome.core import coroutine
AUTO_LOAD = ["gpio_expander"]
CODEOWNERS = ["@jesserockz"]
mcp23xxx_base_ns = cg.esphome_ns.namespace("mcp23xxx_base")
@@ -37,11 +36,9 @@ MCP23XXX_CONFIG_SCHEMA = cv.Schema(
@coroutine
async def register_mcp23xxx(config, num_pins):
id: ID = config[CONF_ID]
var = cg.new_Pvariable(id)
async def register_mcp23xxx(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
CORE.data.setdefault(CONF_MCP23XXX, {})[id.id] = num_pins
cg.add(var.set_open_drain_ints(config[CONF_OPEN_DRAIN_INTERRUPT]))
return var
@@ -76,12 +73,9 @@ MCP23XXX_PIN_SCHEMA = pins.gpio_base_schema(
@pins.PIN_SCHEMA_REGISTRY.register(CONF_MCP23XXX, MCP23XXX_PIN_SCHEMA)
async def mcp23xxx_pin_to_code(config):
parent_id: ID = config[CONF_MCP23XXX]
parent = await cg.get_variable(parent_id)
var = cg.new_Pvariable(config[CONF_ID])
parent = await cg.get_variable(config[CONF_MCP23XXX])
num_pins = cg.TemplateArguments(CORE.data[CONF_MCP23XXX][parent_id.id])
var = cg.new_Pvariable(config[CONF_ID], num_pins)
cg.add(var.set_parent(parent))
num = config[CONF_NUMBER]

View File

@@ -1,27 +1,24 @@
#include "mcp23xxx_base.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
namespace mcp23xxx_base {
template<uint8_t N> void MCP23XXXGPIOPin<N>::setup() {
this->pin_mode(flags_);
float MCP23XXXBase::get_setup_priority() const { return setup_priority::IO; }
void MCP23XXXGPIOPin::setup() {
pin_mode(flags_);
this->parent_->pin_interrupt_mode(this->pin_, this->interrupt_mode_);
}
template<uint8_t N> void MCP23XXXGPIOPin<N>::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); }
template<uint8_t N> bool MCP23XXXGPIOPin<N>::digital_read() {
return this->parent_->digital_read(this->pin_) != this->inverted_;
}
template<uint8_t N> void MCP23XXXGPIOPin<N>::digital_write(bool value) {
this->parent_->digital_write(this->pin_, value != this->inverted_);
}
template<uint8_t N> std::string MCP23XXXGPIOPin<N>::dump_summary() const {
return str_snprintf("%u via MCP23XXX", 15, pin_);
}
template class MCP23XXXGPIOPin<8>;
template class MCP23XXXGPIOPin<16>;
void MCP23XXXGPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); }
bool MCP23XXXGPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; }
void MCP23XXXGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); }
std::string MCP23XXXGPIOPin::dump_summary() const {
char buffer[32];
snprintf(buffer, sizeof(buffer), "%u via MCP23XXX", pin_);
return buffer;
}
} // namespace mcp23xxx_base
} // namespace esphome

View File

@@ -1,6 +1,5 @@
#pragma once
#include "esphome/components/gpio_expander/cached_gpio.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
@@ -9,15 +8,15 @@ namespace mcp23xxx_base {
enum MCP23XXXInterruptMode : uint8_t { MCP23XXX_NO_INTERRUPT = 0, MCP23XXX_CHANGE, MCP23XXX_RISING, MCP23XXX_FALLING };
template<uint8_t N> class MCP23XXXBase : public Component, public gpio_expander::CachedGpioExpander<uint8_t, N> {
class MCP23XXXBase : public Component {
public:
virtual bool digital_read(uint8_t pin);
virtual void digital_write(uint8_t pin, bool value);
virtual void pin_mode(uint8_t pin, gpio::Flags flags);
virtual void pin_interrupt_mode(uint8_t pin, MCP23XXXInterruptMode interrupt_mode);
void set_open_drain_ints(const bool value) { this->open_drain_ints_ = value; }
float get_setup_priority() const override { return setup_priority::IO; }
void loop() override { this->reset_pin_cache_(); }
float get_setup_priority() const override;
protected:
// read a given register
@@ -30,7 +29,7 @@ template<uint8_t N> class MCP23XXXBase : public Component, public gpio_expander:
bool open_drain_ints_;
};
template<uint8_t N> class MCP23XXXGPIOPin : public GPIOPin {
class MCP23XXXGPIOPin : public GPIOPin {
public:
void setup() override;
void pin_mode(gpio::Flags flags) override;
@@ -38,7 +37,7 @@ template<uint8_t N> class MCP23XXXGPIOPin : public GPIOPin {
void digital_write(bool value) override;
std::string dump_summary() const override;
void set_parent(MCP23XXXBase<N> *parent) { parent_ = parent; }
void set_parent(MCP23XXXBase *parent) { parent_ = parent; }
void set_pin(uint8_t pin) { pin_ = pin; }
void set_inverted(bool inverted) { inverted_ = inverted; }
void set_flags(gpio::Flags flags) { flags_ = flags; }
@@ -47,7 +46,7 @@ template<uint8_t N> class MCP23XXXGPIOPin : public GPIOPin {
gpio::Flags get_flags() const override { return this->flags_; }
protected:
MCP23XXXBase<N> *parent_;
MCP23XXXBase *parent_;
uint8_t pin_;
bool inverted_;
gpio::Flags flags_;

View File

@@ -77,7 +77,6 @@ BRIGHTNESS = 0x51
WRDISBV = 0x51
RDDISBV = 0x52
WRCTRLD = 0x53
WCE = 0x58
SWIRE1 = 0x5A
SWIRE2 = 0x5B
IFMODE = 0xB0
@@ -92,7 +91,6 @@ PWCTR2 = 0xC1
PWCTR3 = 0xC2
PWCTR4 = 0xC3
PWCTR5 = 0xC4
SPIMODESEL = 0xC4
VMCTR1 = 0xC5
IFCTR = 0xC6
VMCTR2 = 0xC7

View File

@@ -5,13 +5,10 @@ from esphome.components.mipi import (
PAGESEL,
PIXFMT,
SLPOUT,
SPIMODESEL,
SWIRE1,
SWIRE2,
TEON,
WCE,
WRAM,
WRCTRLD,
DriverChip,
delay,
)
@@ -90,19 +87,4 @@ T4_S3_AMOLED = RM690B0.extend(
bus_mode=TYPE_QUAD,
)
CO5300 = DriverChip(
"CO5300",
brightness=0xD0,
color_order=MODE_RGB,
bus_mode=TYPE_QUAD,
initsequence=(
(SLPOUT,), # Requires early SLPOUT
(PAGESEL, 0x00),
(SPIMODESEL, 0x80),
(WRCTRLD, 0x20),
(WCE, 0x00),
),
)
models = {}

View File

@@ -1,7 +1,6 @@
from esphome.components.mipi import DriverChip
import esphome.config_validation as cv
from .amoled import CO5300
from .ili import ILI9488_A
DriverChip(
@@ -141,14 +140,3 @@ ILI9488_A.extend(
data_rate="20MHz",
invert_colors=True,
)
CO5300.extend(
"WAVESHARE-ESP32-S3-TOUCH-AMOLED-1.75",
width=466,
height=466,
pixel_mode="16bit",
offset_height=0,
offset_width=6,
cs_pin=12,
reset_pin=39,
)

View File

@@ -1,4 +1,4 @@
#include "nfc_binary_sensor.h"
#include "binary_sensor.h"
#include "../nfc_helpers.h"
#include "esphome/core/log.h"

View File

@@ -13,7 +13,6 @@ from esphome.const import (
CONF_PIN,
CONF_RMT_SYMBOLS,
CONF_USE_DMA,
CONF_VALUE,
PlatformFramework,
)
from esphome.core import CORE
@@ -23,17 +22,11 @@ AUTO_LOAD = ["remote_base"]
CONF_EOT_LEVEL = "eot_level"
CONF_ON_TRANSMIT = "on_transmit"
CONF_ON_COMPLETE = "on_complete"
CONF_TRANSMITTER_ID = remote_base.CONF_TRANSMITTER_ID
remote_transmitter_ns = cg.esphome_ns.namespace("remote_transmitter")
RemoteTransmitterComponent = remote_transmitter_ns.class_(
"RemoteTransmitterComponent", remote_base.RemoteTransmitterBase, cg.Component
)
DigitalWriteAction = remote_transmitter_ns.class_(
"DigitalWriteAction",
automation.Action,
cg.Parented.template(RemoteTransmitterComponent),
)
MULTI_CONF = True
CONFIG_SCHEMA = cv.Schema(
@@ -70,25 +63,6 @@ CONFIG_SCHEMA = cv.Schema(
}
).extend(cv.COMPONENT_SCHEMA)
DIGITAL_WRITE_ACTION_SCHEMA = cv.maybe_simple_value(
{
cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(RemoteTransmitterComponent),
cv.Required(CONF_VALUE): cv.templatable(cv.boolean),
},
key=CONF_VALUE,
)
@automation.register_action(
"remote_transmitter.digital_write", DigitalWriteAction, DIGITAL_WRITE_ACTION_SCHEMA
)
async def digital_write_action_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_TRANSMITTER_ID])
template_ = await cg.templatable(config[CONF_VALUE], args, bool)
cg.add(var.set_value(template_))
return var
async def to_code(config):
pin = await cg.gpio_pin_expression(config[CONF_PIN])

View File

@@ -1,18 +0,0 @@
#pragma once
#include "esphome/components/remote_transmitter/remote_transmitter.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace remote_transmitter {
template<typename... Ts> class DigitalWriteAction : public Action<Ts...>, public Parented<RemoteTransmitterComponent> {
public:
TEMPLATABLE_VALUE(bool, value)
void play(Ts... x) override { this->parent_->digital_write(this->value_.value(x...)); }
};
} // namespace remote_transmitter
} // namespace esphome

View File

@@ -30,11 +30,10 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase,
void set_carrier_duty_percent(uint8_t carrier_duty_percent) { this->carrier_duty_percent_ = carrier_duty_percent; }
void digital_write(bool value);
#if defined(USE_ESP32)
void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; }
void set_eot_level(bool eot_level) { this->eot_level_ = eot_level; }
void digital_write(bool value);
#endif
Trigger<> *get_transmit_trigger() const { return this->transmit_trigger_; };

View File

@@ -73,8 +73,6 @@ void RemoteTransmitterComponent::space_(uint32_t usec) {
this->target_time_ += usec;
}
void RemoteTransmitterComponent::digital_write(bool value) { this->pin_->digital_write(value); }
void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {
ESP_LOGD(TAG, "Sending remote code");
uint32_t on_time, off_time;

View File

@@ -75,8 +75,6 @@ void RemoteTransmitterComponent::space_(uint32_t usec) {
this->target_time_ += usec;
}
void RemoteTransmitterComponent::digital_write(bool value) { this->pin_->digital_write(value); }
void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {
ESP_LOGD(TAG, "Sending remote code");
uint32_t on_time, off_time;

View File

@@ -118,8 +118,8 @@ async def register_select(var, config, *, options: list[str]):
await setup_select_core_(var, config, options=options)
async def new_select(config, *args, options: list[str]):
var = cg.new_Pvariable(config[CONF_ID], *args)
async def new_select(config, *, options: list[str]):
var = cg.new_Pvariable(config[CONF_ID])
await register_select(var, config, options=options)
return var

View File

@@ -315,19 +315,31 @@ async def to_code(config):
cg.add_define("USE_AUDIO_FLAC_SUPPORT", True)
cg.add_define("USE_AUDIO_MP3_SUPPORT", True)
# Based on https://github.com/espressif/esp-idf/blob/release/v5.4/examples/wifi/iperf/sdkconfig.defaults.esp32
esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM", 16)
esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM", 64)
esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM", 64)
esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_TX_ENABLED", True)
esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_TX_BA_WIN", 32)
esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_RX_ENABLED", True)
esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_RX_BA_WIN", 32)
esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT", 65534)
esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCP_WND_DEFAULT", 65534)
esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCP_RECVMBOX_SIZE", 64)
esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_RECVMBOX_SIZE", 64)
# Wifi settings based on https://github.com/espressif/esp-adf/issues/297#issuecomment-783811702
esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_STATIC_RX_BUFFER_NUM", 16)
esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_DYNAMIC_RX_BUFFER_NUM", 512)
esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_STATIC_TX_BUFFER", True)
esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_TX_BUFFER_TYPE", 0)
esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_STATIC_TX_BUFFER_NUM", 8)
esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_CACHE_TX_BUFFER_NUM", 32)
esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_AMPDU_TX_ENABLED", True)
esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_TX_BA_WIN", 16)
esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_AMPDU_RX_ENABLED", True)
esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_RX_BA_WIN", 32)
esp32.add_idf_sdkconfig_option("CONFIG_LWIP_MAX_ACTIVE_TCP", 16)
esp32.add_idf_sdkconfig_option("CONFIG_LWIP_MAX_LISTENING_TCP", 16)
esp32.add_idf_sdkconfig_option("CONFIG_TCP_MAXRTX", 12)
esp32.add_idf_sdkconfig_option("CONFIG_TCP_SYNMAXRTX", 6)
esp32.add_idf_sdkconfig_option("CONFIG_TCP_MSS", 1436)
esp32.add_idf_sdkconfig_option("CONFIG_TCP_MSL", 60000)
esp32.add_idf_sdkconfig_option("CONFIG_TCP_SND_BUF_DEFAULT", 65535)
esp32.add_idf_sdkconfig_option("CONFIG_TCP_WND_DEFAULT", 512000)
esp32.add_idf_sdkconfig_option("CONFIG_TCP_RECVMBOX_SIZE", 512)
esp32.add_idf_sdkconfig_option("CONFIG_TCP_QUEUE_OOSEQ", True)
esp32.add_idf_sdkconfig_option("CONFIG_TCP_OVERSIZE_MSS", True)
esp32.add_idf_sdkconfig_option("CONFIG_LWIP_WND_SCALE", True)
esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCP_RCV_SCALE", 3)
esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_RECVMBOX_SIZE", 512)
# Allocate wifi buffers in PSRAM
esp32.add_idf_sdkconfig_option("CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP", True)

View File

@@ -259,10 +259,13 @@ esp_err_t AudioPipeline::allocate_communications_() {
esp_err_t AudioPipeline::start_tasks_() {
if (this->read_task_handle_ == nullptr) {
if (this->read_task_stack_buffer_ == nullptr) {
// Reader task uses the AudioReader class which uses esp_http_client. This crashes on IDF 5.4 if the task stack is
// in PSRAM. As a workaround, always allocate the read task in internal memory.
RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_INTERNAL);
this->read_task_stack_buffer_ = stack_allocator.allocate(READ_TASK_STACK_SIZE);
if (this->task_stack_in_psram_) {
RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_EXTERNAL);
this->read_task_stack_buffer_ = stack_allocator.allocate(READ_TASK_STACK_SIZE);
} else {
RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_INTERNAL);
this->read_task_stack_buffer_ = stack_allocator.allocate(READ_TASK_STACK_SIZE);
}
}
if (this->read_task_stack_buffer_ == nullptr) {

View File

@@ -30,7 +30,7 @@ from esphome.core import CORE, coroutine_with_priority
_LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@esphome/core"]
CODEOWNERS = ["@OttoWinter"]
IS_PLATFORM_COMPONENT = True
time_ns = cg.esphome_ns.namespace("time")

View File

@@ -298,7 +298,6 @@ async def to_code(config):
if config[CONF_ENABLE_PRIVATE_NETWORK_ACCESS]:
cg.add_define("USE_WEBSERVER_PRIVATE_NETWORK_ACCESS")
if CONF_AUTH in config:
cg.add_define("USE_WEBSERVER_AUTH")
cg.add(paren.set_auth_username(config[CONF_AUTH][CONF_USERNAME]))
cg.add(paren.set_auth_password(config[CONF_AUTH][CONF_PASSWORD]))
if CONF_CSS_INCLUDE in config:

View File

@@ -376,32 +376,23 @@ void WebServer::handle_js_request(AsyncWebServerRequest *request) {
}
#endif
// Helper functions to reduce code size by avoiding macro expansion
static void set_json_id(JsonObject &root, EntityBase *obj, const std::string &id, JsonDetail start_config) {
root["id"] = id;
if (start_config == DETAIL_ALL) {
root["name"] = obj->get_name();
root["icon"] = obj->get_icon();
root["entity_category"] = obj->get_entity_category();
bool is_disabled = obj->is_disabled_by_default();
if (is_disabled)
root["is_disabled_by_default"] = is_disabled;
#define set_json_id(root, obj, sensor, start_config) \
(root)["id"] = sensor; \
if (((start_config) == DETAIL_ALL)) { \
(root)["name"] = (obj)->get_name(); \
(root)["icon"] = (obj)->get_icon(); \
(root)["entity_category"] = (obj)->get_entity_category(); \
if ((obj)->is_disabled_by_default()) \
(root)["is_disabled_by_default"] = (obj)->is_disabled_by_default(); \
}
}
template<typename T>
static void set_json_value(JsonObject &root, EntityBase *obj, const std::string &id, const T &value,
JsonDetail start_config) {
set_json_id(root, obj, id, start_config);
root["value"] = value;
}
#define set_json_value(root, obj, sensor, value, start_config) \
set_json_id((root), (obj), sensor, start_config); \
(root)["value"] = value;
template<typename T>
static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const std::string &id,
const std::string &state, const T &value, JsonDetail start_config) {
set_json_value(root, obj, id, value, start_config);
root["state"] = state;
}
#define set_json_icon_state_value(root, obj, sensor, state, value, start_config) \
set_json_value(root, obj, sensor, value, start_config); \
(root)["state"] = state;
// Helper to get request detail parameter
static JsonDetail get_request_detail(AsyncWebServerRequest *request) {

View File

@@ -3,7 +3,7 @@ import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.core import CORE, coroutine_with_priority
CODEOWNERS = ["@esphome/core"]
CODEOWNERS = ["@OttoWinter"]
DEPENDENCIES = ["network"]

View File

@@ -14,11 +14,9 @@ WebServerBase *global_web_server_base = nullptr; // NOLINT(cppcoreguidelines-av
void WebServerBase::add_handler(AsyncWebHandler *handler) {
// remove all handlers
#ifdef USE_WEBSERVER_AUTH
if (!credentials_.username.empty()) {
handler = new internal::AuthMiddlewareHandler(handler, &credentials_);
}
#endif
this->handlers_.push_back(handler);
if (this->server_ != nullptr) {
this->server_->addHandler(handler);

View File

@@ -41,7 +41,6 @@ class MiddlewareHandler : public AsyncWebHandler {
AsyncWebHandler *next_;
};
#ifdef USE_WEBSERVER_AUTH
struct Credentials {
std::string username;
std::string password;
@@ -80,7 +79,6 @@ class AuthMiddlewareHandler : public MiddlewareHandler {
protected:
Credentials *credentials_;
};
#endif
} // namespace internal
@@ -110,10 +108,8 @@ class WebServerBase : public Component {
std::shared_ptr<AsyncWebServer> get_server() const { return server_; }
float get_setup_priority() const override;
#ifdef USE_WEBSERVER_AUTH
void set_auth_username(std::string auth_username) { credentials_.username = std::move(auth_username); }
void set_auth_password(std::string auth_password) { credentials_.password = std::move(auth_password); }
#endif
void add_handler(AsyncWebHandler *handler);
@@ -125,9 +121,7 @@ class WebServerBase : public Component {
uint16_t port_{80};
std::shared_ptr<AsyncWebServer> server_{nullptr};
std::vector<AsyncWebHandler *> handlers_;
#ifdef USE_WEBSERVER_AUTH
internal::Credentials credentials_;
#endif
};
} // namespace web_server_base

View File

@@ -116,7 +116,7 @@ esp_err_t AsyncWebServer::request_post_handler(httpd_req_t *r) {
}
// Handle regular form data
if (r->content_len > CONFIG_HTTPD_MAX_REQ_HDR_LEN) {
if (r->content_len > HTTPD_MAX_REQ_HDR_LEN) {
ESP_LOGW(TAG, "Request size is to big: %zu", r->content_len);
httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr);
return ESP_FAIL;
@@ -223,7 +223,6 @@ void AsyncWebServerRequest::init_response_(AsyncWebServerResponse *rsp, int code
this->rsp_ = rsp;
}
#ifdef USE_WEBSERVER_AUTH
bool AsyncWebServerRequest::authenticate(const char *username, const char *password) const {
if (username == nullptr || password == nullptr || *username == 0) {
return true;
@@ -262,7 +261,6 @@ void AsyncWebServerRequest::requestAuthentication(const char *realm) const {
httpd_resp_set_hdr(*this, "WWW-Authenticate", auth_val.c_str());
httpd_resp_send_err(*this, HTTPD_401_UNAUTHORIZED, nullptr);
}
#endif
AsyncWebParameter *AsyncWebServerRequest::getParam(const std::string &name) {
auto find = this->params_.find(name);
@@ -425,14 +423,14 @@ void AsyncEventSourceResponse::destroy(void *ptr) {
void AsyncEventSourceResponse::deq_push_back_with_dedup_(void *source, message_generator_t *message_generator) {
DeferredEvent item(source, message_generator);
// Use range-based for loop instead of std::find_if to reduce template instantiation overhead and binary size
for (auto &event : this->deferred_queue_) {
if (event == item) {
event = item;
return;
}
auto iter = std::find_if(this->deferred_queue_.begin(), this->deferred_queue_.end(),
[&item](const DeferredEvent &test) -> bool { return test == item; });
if (iter != this->deferred_queue_.end()) {
(*iter) = item;
} else {
this->deferred_queue_.push_back(item);
}
this->deferred_queue_.push_back(item);
}
void AsyncEventSourceResponse::process_deferred_queue_() {

View File

@@ -115,11 +115,9 @@ class AsyncWebServerRequest {
// NOLINTNEXTLINE(readability-identifier-naming)
size_t contentLength() const { return this->req_->content_len; }
#ifdef USE_WEBSERVER_AUTH
bool authenticate(const char *username, const char *password) const;
// NOLINTNEXTLINE(readability-identifier-naming)
void requestAuthentication(const char *realm = nullptr) const;
#endif
void redirect(const std::string &url);

View File

@@ -505,54 +505,6 @@ void WiFiComponent::start_scanning() {
this->state_ = WIFI_COMPONENT_STATE_STA_SCANNING;
}
// Helper function for WiFi scan result comparison
// Returns true if 'a' should be placed before 'b' in the sorted order
[[nodiscard]] inline static bool wifi_scan_result_is_better(const WiFiScanResult &a, const WiFiScanResult &b) {
// Matching networks always come before non-matching
if (a.get_matches() && !b.get_matches())
return true;
if (!a.get_matches() && b.get_matches())
return false;
if (a.get_matches() && b.get_matches()) {
// For APs with the same SSID, always prefer stronger signal
// This helps with mesh networks and multiple APs
if (a.get_ssid() == b.get_ssid()) {
return a.get_rssi() > b.get_rssi();
}
// For different SSIDs, check priority first
if (a.get_priority() != b.get_priority())
return a.get_priority() > b.get_priority();
// If priorities are equal, prefer stronger signal
return a.get_rssi() > b.get_rssi();
}
// Both don't match - sort by signal strength
return a.get_rssi() > b.get_rssi();
}
// Helper function for insertion sort of WiFi scan results
// Using insertion sort instead of std::stable_sort saves flash memory
// by avoiding template instantiations (std::rotate, std::stable_sort, lambdas)
// IMPORTANT: This sort is stable (preserves relative order of equal elements)
static void insertion_sort_scan_results(std::vector<WiFiScanResult> &results) {
const size_t size = results.size();
for (size_t i = 1; i < size; i++) {
// Make a copy to avoid issues with move semantics during comparison
WiFiScanResult key = results[i];
int32_t j = i - 1;
// Move elements that are worse than key to the right
// For stability, we only move if key is strictly better than results[j]
while (j >= 0 && wifi_scan_result_is_better(key, results[j])) {
results[j + 1] = results[j];
j--;
}
results[j + 1] = key;
}
}
void WiFiComponent::check_scanning_finished() {
if (!this->scan_done_) {
if (millis() - this->action_started_ > 30000) {
@@ -583,8 +535,30 @@ void WiFiComponent::check_scanning_finished() {
}
}
// Sort scan results using insertion sort for better memory efficiency
insertion_sort_scan_results(this->scan_result_);
std::stable_sort(this->scan_result_.begin(), this->scan_result_.end(),
[](const WiFiScanResult &a, const WiFiScanResult &b) {
// return true if a is better than b
if (a.get_matches() && !b.get_matches())
return true;
if (!a.get_matches() && b.get_matches())
return false;
if (a.get_matches() && b.get_matches()) {
// For APs with the same SSID, always prefer stronger signal
// This helps with mesh networks and multiple APs
if (a.get_ssid() == b.get_ssid()) {
return a.get_rssi() > b.get_rssi();
}
// For different SSIDs, check priority first
if (a.get_priority() != b.get_priority())
return a.get_priority() > b.get_priority();
// If priorities are equal, prefer stronger signal
return a.get_rssi() > b.get_rssi();
}
return a.get_rssi() > b.get_rssi();
});
for (auto &res : this->scan_result_) {
char bssid_s[18];

View File

@@ -87,7 +87,7 @@ from esphome.core import (
TimePeriodNanoseconds,
TimePeriodSeconds,
)
from esphome.helpers import add_class_to_obj, docs_url, list_starts_with
from esphome.helpers import add_class_to_obj, list_starts_with
from esphome.schema_extractors import (
SCHEMA_EXTRACT,
schema_extractor,
@@ -666,6 +666,14 @@ def only_with_framework(
if suggestions is None:
suggestions = {}
version = Version.parse(ESPHOME_VERSION)
if version.is_beta:
docs_format = "https://beta.esphome.io/components/{path}"
elif version.is_dev:
docs_format = "https://next.esphome.io/components/{path}"
else:
docs_format = "https://esphome.io/components/{path}"
def validator_(obj):
if CORE.target_framework not in frameworks:
err_str = f"This feature is only available with framework(s) {', '.join([framework.value for framework in frameworks])}"
@@ -673,7 +681,7 @@ def only_with_framework(
(component, docs_path) = suggestion
err_str += f"\nPlease use '{component}'"
if docs_path:
err_str += f": {docs_url(path=f'components/{docs_path}')}"
err_str += f": {docs_format.format(path=docs_path)}"
raise Invalid(err_str)
return obj

View File

@@ -34,44 +34,6 @@ namespace esphome {
static const char *const TAG = "app";
// Helper function for insertion sort of components by setup priority
// Using insertion sort instead of std::stable_sort saves ~1.3KB of flash
// by avoiding template instantiations (std::rotate, std::stable_sort, lambdas)
// IMPORTANT: This sort is stable (preserves relative order of equal elements),
// which is necessary to maintain user-defined component order for same priority
template<typename Iterator> static void insertion_sort_by_setup_priority(Iterator first, Iterator last) {
for (auto it = first + 1; it != last; ++it) {
auto key = *it;
float key_priority = key->get_actual_setup_priority();
auto j = it - 1;
// Using '<' (not '<=') ensures stability - equal priority components keep their order
while (j >= first && (*j)->get_actual_setup_priority() < key_priority) {
*(j + 1) = *j;
j--;
}
*(j + 1) = key;
}
}
// Helper function for insertion sort of components by loop priority
// IMPORTANT: This sort is stable (preserves relative order of equal elements),
// which is required when components are re-sorted during setup() if they block
template<typename Iterator> static void insertion_sort_by_loop_priority(Iterator first, Iterator last) {
for (auto it = first + 1; it != last; ++it) {
auto key = *it;
float key_priority = key->get_loop_priority();
auto j = it - 1;
// Using '<' (not '<=') ensures stability - equal priority components keep their order
while (j >= first && (*j)->get_loop_priority() < key_priority) {
*(j + 1) = *j;
j--;
}
*(j + 1) = key;
}
}
void Application::register_component_(Component *comp) {
if (comp == nullptr) {
ESP_LOGW(TAG, "Tried to register null component!");
@@ -89,9 +51,9 @@ void Application::register_component_(Component *comp) {
void Application::setup() {
ESP_LOGI(TAG, "Running through setup()");
ESP_LOGV(TAG, "Sorting components by setup priority");
// Sort by setup priority using our helper function
insertion_sort_by_setup_priority(this->components_.begin(), this->components_.end());
std::stable_sort(this->components_.begin(), this->components_.end(), [](const Component *a, const Component *b) {
return a->get_actual_setup_priority() > b->get_actual_setup_priority();
});
// Initialize looping_components_ early so enable_pending_loops_() works during setup
this->calculate_looping_components_();
@@ -107,8 +69,8 @@ void Application::setup() {
if (component->can_proceed())
continue;
// Sort components 0 through i by loop priority
insertion_sort_by_loop_priority(this->components_.begin(), this->components_.begin() + i + 1);
std::stable_sort(this->components_.begin(), this->components_.begin() + i + 1,
[](Component *a, Component *b) { return a->get_loop_priority() > b->get_loop_priority(); });
do {
uint8_t new_app_state = STATUS_LED_WARNING;
@@ -497,25 +459,24 @@ void Application::unregister_socket_fd(int fd) {
if (fd < 0)
return;
for (size_t i = 0; i < this->socket_fds_.size(); i++) {
if (this->socket_fds_[i] != fd)
continue;
auto it = std::find(this->socket_fds_.begin(), this->socket_fds_.end(), fd);
if (it != this->socket_fds_.end()) {
// Swap with last element and pop - O(1) removal since order doesn't matter
if (i < this->socket_fds_.size() - 1)
this->socket_fds_[i] = this->socket_fds_.back();
if (it != this->socket_fds_.end() - 1) {
std::swap(*it, this->socket_fds_.back());
}
this->socket_fds_.pop_back();
this->socket_fds_changed_ = true;
// Only recalculate max_fd if we removed the current max
if (fd == this->max_fd_) {
this->max_fd_ = -1;
for (int sock_fd : this->socket_fds_) {
if (sock_fd > this->max_fd_)
this->max_fd_ = sock_fd;
if (this->socket_fds_.empty()) {
this->max_fd_ = -1;
} else {
// Find new max using std::max_element
this->max_fd_ = *std::max_element(this->socket_fds_.begin(), this->socket_fds_.end());
}
}
return;
}
}

View File

@@ -101,9 +101,12 @@ class Application {
arch_init();
this->name_add_mac_suffix_ = name_add_mac_suffix;
if (name_add_mac_suffix) {
const std::string mac_suffix = get_mac_address().substr(6);
this->name_ = name + "-" + mac_suffix;
this->friendly_name_ = friendly_name.empty() ? "" : friendly_name + " " + mac_suffix;
this->name_ = name + "-" + get_mac_address().substr(6);
if (friendly_name.empty()) {
this->friendly_name_ = "";
} else {
this->friendly_name_ = friendly_name + " " + get_mac_address().substr(6);
}
} else {
this->name_ = name;
this->friendly_name_ = friendly_name;
@@ -211,6 +214,14 @@ class Application {
#endif
/// Reserve space for components to avoid memory fragmentation
void reserve_components(size_t count) { this->components_.reserve(count); }
#ifdef USE_AREAS
void reserve_area(size_t count) { this->areas_.reserve(count); }
#endif
#ifdef USE_DEVICES
void reserve_device(size_t count) { this->devices_.reserve(count); }
#endif
/// Register the component in this Application instance.
template<class C> C *register_component(C *c) {
@@ -305,7 +316,7 @@ class Application {
} \
return nullptr; \
}
const auto &get_devices() { return this->devices_; }
const std::vector<Device *> &get_devices() { return this->devices_; }
#else
#define GET_ENTITY_METHOD(entity_type, entity_name, entities_member) \
entity_type *get_##entity_name##_by_key(uint32_t key, bool include_internal = false) { \
@@ -317,7 +328,7 @@ class Application {
}
#endif // USE_DEVICES
#ifdef USE_AREAS
const auto &get_areas() { return this->areas_; }
const std::vector<Area *> &get_areas() { return this->areas_; }
#endif
#ifdef USE_BINARY_SENSOR
auto &get_binary_sensors() const { return this->binary_sensors_; }
@@ -451,7 +462,12 @@ class Application {
const char *comment_{nullptr};
const char *compilation_time_{nullptr};
// std::vector (3 pointers each: begin, end, capacity)
// size_t members
size_t dump_config_at_{SIZE_MAX};
// Vectors (largest members)
std::vector<Component *> components_{};
// Partitioned vector design for looping components
// =================================================
// Components are partitioned into [active | inactive] sections:
@@ -469,54 +485,12 @@ class Application {
// and active_end_ is incremented
// - This eliminates branch mispredictions from flag checking in the hot loop
std::vector<Component *> looping_components_{};
#ifdef USE_SOCKET_SELECT_SUPPORT
std::vector<int> socket_fds_; // Vector of all monitored socket file descriptors
#endif
// std::string members (typically 24-32 bytes each)
std::string name_;
std::string friendly_name_;
// size_t members
size_t dump_config_at_{SIZE_MAX};
// 4-byte members
uint32_t last_loop_{0};
uint32_t loop_component_start_time_{0};
#ifdef USE_SOCKET_SELECT_SUPPORT
int max_fd_{-1}; // Highest file descriptor number for select()
#endif
// 2-byte members (grouped together for alignment)
uint16_t loop_interval_{16}; // Loop interval in ms (max 65535ms = 65.5 seconds)
uint16_t looping_components_active_end_{0}; // Index marking end of active components in looping_components_
uint16_t current_loop_index_{0}; // For safe reentrant modifications during iteration
// 1-byte members (grouped together to minimize padding)
uint8_t app_state_{0};
bool name_add_mac_suffix_;
bool in_loop_{false};
volatile bool has_pending_enable_loop_requests_{false};
#ifdef USE_SOCKET_SELECT_SUPPORT
bool socket_fds_changed_{false}; // Flag to rebuild base_read_fds_ when socket_fds_ changes
#endif
#ifdef USE_SOCKET_SELECT_SUPPORT
// Variable-sized members
fd_set base_read_fds_{}; // Cached fd_set rebuilt only when socket_fds_ changes
fd_set read_fds_{}; // Working fd_set for select(), copied from base_read_fds_
#endif
// StaticVectors (largest members - contain actual array data inline)
StaticVector<Component *, ESPHOME_COMPONENT_COUNT> components_{};
#ifdef USE_DEVICES
StaticVector<Device *, ESPHOME_DEVICE_COUNT> devices_{};
std::vector<Device *> devices_{};
#endif
#ifdef USE_AREAS
StaticVector<Area *, ESPHOME_AREA_COUNT> areas_{};
std::vector<Area *> areas_{};
#endif
#ifdef USE_BINARY_SENSOR
StaticVector<binary_sensor::BinarySensor *, ESPHOME_ENTITY_BINARY_SENSOR_COUNT> binary_sensors_{};
@@ -582,6 +556,41 @@ class Application {
#ifdef USE_UPDATE
StaticVector<update::UpdateEntity *, ESPHOME_ENTITY_UPDATE_COUNT> updates_{};
#endif
#ifdef USE_SOCKET_SELECT_SUPPORT
std::vector<int> socket_fds_; // Vector of all monitored socket file descriptors
#endif
// String members
std::string name_;
std::string friendly_name_;
// 4-byte members
uint32_t last_loop_{0};
uint32_t loop_component_start_time_{0};
#ifdef USE_SOCKET_SELECT_SUPPORT
int max_fd_{-1}; // Highest file descriptor number for select()
#endif
// 2-byte members (grouped together for alignment)
uint16_t loop_interval_{16}; // Loop interval in ms (max 65535ms = 65.5 seconds)
uint16_t looping_components_active_end_{0};
uint16_t current_loop_index_{0}; // For safe reentrant modifications during iteration
// 1-byte members (grouped together to minimize padding)
uint8_t app_state_{0};
bool name_add_mac_suffix_;
bool in_loop_{false};
volatile bool has_pending_enable_loop_requests_{false};
#ifdef USE_SOCKET_SELECT_SUPPORT
bool socket_fds_changed_{false}; // Flag to rebuild base_read_fds_ when socket_fds_ changes
// Variable-sized members at end
fd_set base_read_fds_{}; // Cached fd_set rebuilt only when socket_fds_ changes
fd_set read_fds_{}; // Working fd_set for select(), copied from base_read_fds_
#endif
};
/// Global storage of Application pointer - only one Application can exist.

View File

@@ -1,13 +1,8 @@
#pragma once
#include "defines.h"
#include "component.h"
#include "helpers.h"
#ifdef USE_LVGL
#include "esphome/components/lvgl/lvgl_proxy.h"
#endif // USE_LVGL
namespace esphome {
inline static constexpr uint8_t esp_scale8(uint8_t i, uint8_t scale) {
@@ -38,11 +33,6 @@ struct Color {
uint32_t raw_32;
};
#ifdef USE_LVGL
// convenience function for Color to get a lv_color_t representation
operator lv_color_t() const { return lv_color_make(this->r, this->g, this->b); }
#endif
inline constexpr Color() ESPHOME_ALWAYS_INLINE : raw_32(0) {} // NOLINT
inline constexpr Color(uint8_t red, uint8_t green, uint8_t blue) ESPHOME_ALWAYS_INLINE : r(red),
g(green),

View File

@@ -459,8 +459,10 @@ async def to_code(config: ConfigType) -> None:
config[CONF_NAME_ADD_MAC_SUFFIX],
)
)
# Define component count for static allocation
cg.add_define("ESPHOME_COMPONENT_COUNT", len(CORE.component_ids))
# Reserve space for components to avoid reallocation during registration
cg.add(
cg.RawStatement(f"App.reserve_components({len(CORE.component_ids)});"),
)
CORE.add_job(_add_platform_defines)
@@ -529,8 +531,8 @@ async def to_code(config: ConfigType) -> None:
all_areas.extend(config[CONF_AREAS])
if all_areas:
cg.add(cg.RawStatement(f"App.reserve_area({len(all_areas)});"))
cg.add_define("USE_AREAS")
cg.add_define("ESPHOME_AREA_COUNT", len(all_areas))
for area_conf in all_areas:
area_id: core.ID = area_conf[CONF_ID]
@@ -547,9 +549,9 @@ async def to_code(config: ConfigType) -> None:
if not devices:
return
# Define device count for static allocation
# Reserve space for devices
cg.add(cg.RawStatement(f"App.reserve_device({len(devices)});"))
cg.add_define("USE_DEVICES")
cg.add_define("ESPHOME_DEVICE_COUNT", len(devices))
# Process each device
for dev_conf in devices:

View File

@@ -153,7 +153,6 @@
#define USE_ESP32_BLE_CLIENT
#define USE_ESP32_BLE_DEVICE
#define USE_ESP32_BLE_SERVER
#define USE_ESP32_BLE_ADVERTISING
#define USE_I2C
#define USE_IMPROV
#define USE_MICROPHONE
@@ -164,7 +163,6 @@
#define USE_SPI
#define USE_VOICE_ASSISTANT
#define USE_WEBSERVER
#define USE_WEBSERVER_AUTH
#define USE_WEBSERVER_OTA
#define USE_WEBSERVER_PORT 80 // NOLINT
#define USE_WEBSERVER_SORTING
@@ -212,7 +210,6 @@
{}
#define USE_WEBSERVER
#define USE_WEBSERVER_AUTH
#define USE_WEBSERVER_PORT 80 // NOLINT
#endif
@@ -229,7 +226,6 @@
#define USE_SOCKET_IMPL_LWIP_SOCKETS
#define USE_SOCKET_SELECT_SUPPORT
#define USE_WEBSERVER
#define USE_WEBSERVER_AUTH
#define USE_WEBSERVER_PORT 80 // NOLINT
#endif
@@ -244,10 +240,7 @@
#define USE_DASHBOARD_IMPORT
// Default counts for static analysis
#define ESPHOME_COMPONENT_COUNT 50
#define ESPHOME_DEVICE_COUNT 10
#define ESPHOME_AREA_COUNT 10
// Default entity counts for static analysis
#define ESPHOME_ENTITY_ALARM_CONTROL_PANEL_COUNT 1
#define ESPHOME_ENTITY_BINARY_SENSOR_COUNT 1
#define ESPHOME_ENTITY_BUTTON_COUNT 1

View File

@@ -5,7 +5,6 @@
#include <cstdint>
#include <cstring>
#include <functional>
#include <iterator>
#include <limits>
#include <memory>
#include <string>
@@ -101,8 +100,6 @@ template<typename T, size_t N> class StaticVector {
using value_type = T;
using iterator = typename std::array<T, N>::iterator;
using const_iterator = typename std::array<T, N>::const_iterator;
using reverse_iterator = std::reverse_iterator<iterator>;
using const_reverse_iterator = std::reverse_iterator<const_iterator>;
private:
std::array<T, N> data_{};
@@ -117,7 +114,6 @@ template<typename T, size_t N> class StaticVector {
}
size_t size() const { return count_; }
bool empty() const { return count_ == 0; }
T &operator[](size_t i) { return data_[i]; }
const T &operator[](size_t i) const { return data_[i]; }
@@ -127,12 +123,6 @@ template<typename T, size_t N> class StaticVector {
iterator end() { return data_.begin() + count_; }
const_iterator begin() const { return data_.begin(); }
const_iterator end() const { return data_.begin() + count_; }
// Reverse iterators
reverse_iterator rbegin() { return reverse_iterator(end()); }
reverse_iterator rend() { return reverse_iterator(begin()); }
const_reverse_iterator rbegin() const { return const_reverse_iterator(end()); }
const_reverse_iterator rend() const { return const_reverse_iterator(begin()); }
};
///@}

View File

@@ -324,47 +324,39 @@ class EsphomePortCommandWebSocket(EsphomeCommandWebSocket):
configuration = json_message["configuration"]
config_file = settings.rel_path(configuration)
port = json_message["port"]
addresses: list[str] = [port]
if (
port == "OTA" # pylint: disable=too-many-boolean-expressions
and (entry := entries.get(config_file))
and entry.loaded_integrations
and "api" in entry.loaded_integrations
):
addresses = []
# First priority: entry.address AKA use_address
if (
(use_address := entry.address)
and (
address_list := await dashboard.dns_cache.async_resolve(
use_address, time.monotonic()
)
)
and not isinstance(address_list, Exception)
):
addresses.extend(sort_ip_addresses(address_list))
# Second priority: mDNS
if (
(mdns := dashboard.mdns_status)
and (address_list := await mdns.async_resolve_host(entry.name))
and (
new_addresses := [
addr for addr in address_list if addr not in addresses
]
)
if (mdns := dashboard.mdns_status) and (
address_list := await mdns.async_resolve_host(entry.name)
):
# Use the IP address if available but only
# if the API is loaded and the device is online
# since MQTT logging will not work otherwise
addresses.extend(sort_ip_addresses(new_addresses))
port = sort_ip_addresses(address_list)[0]
elif (
entry.address
and (
address_list := await dashboard.dns_cache.async_resolve(
entry.address, time.monotonic()
)
)
and not isinstance(address_list, Exception)
):
# If mdns is not available, try to use the DNS cache
port = sort_ip_addresses(address_list)[0]
device_args: list[str] = [
arg for address in addresses for arg in ("--device", address)
return [
*DASHBOARD_COMMAND,
*args,
config_file,
"--device",
port,
]
return [*DASHBOARD_COMMAND, *args, config_file, *device_args]
class EsphomeLogsHandler(EsphomePortCommandWebSocket):
async def build_command(self, json_message: dict[str, Any]) -> list[str]:

View File

@@ -9,8 +9,6 @@ import re
import tempfile
from urllib.parse import urlparse
from esphome.const import __version__ as ESPHOME_VERSION
_LOGGER = logging.getLogger(__name__)
IS_MACOS = platform.system() == "Darwin"
@@ -505,20 +503,3 @@ _DISALLOWED_CHARS = re.compile(r"[^a-zA-Z0-9-_]")
def sanitize(value):
"""Same behaviour as `helpers.cpp` method `str_sanitize`."""
return _DISALLOWED_CHARS.sub("_", value)
def docs_url(path: str) -> str:
"""Return the URL to the documentation for a given path."""
# Local import to avoid circular import
from esphome.config_validation import Version
version = Version.parse(ESPHOME_VERSION)
if version.is_beta:
docs_format = "https://beta.esphome.io/{path}"
elif version.is_dev:
docs_format = "https://next.esphome.io/{path}"
else:
docs_format = "https://esphome.io/{path}"
path = path.removeprefix("/")
return docs_format.format(path=path)

Some files were not shown because too many files have changed in this diff Show More