Compare commits

..

4 Commits

Author SHA1 Message Date
J. Nick Koston
5e881738da [api] Add speed_optimized proto option for hot encode paths
Add a new (speed_optimized) message option that emits
__attribute__((optimize("O2"))) on the generated encode() and
calculate_size() methods. Under -Os, GCC does not inline the small
ProtoEncode helpers (write_raw_byte, encode_varint, etc.) into the
generated methods, causing significant overhead on hot paths.

Apply to SensorStateResponse and BluetoothLERawAdvertisementsResponse
which are the highest-frequency encode paths.
2026-04-12 19:12:31 -10:00
J. Nick Koston
5a250cc74f [api] Compile noise-c and libsodium with -O2 for speed
Crypto libraries are CPU-bound and benefit significantly from speed
optimization over the default -Os. Add a post: extra_script that
appends -O2 to noise-c and libsodium build flags when API noise
encryption is enabled. GCC uses the last -O flag, so this overrides
the global -Os for these libraries only.
2026-04-12 19:03:21 -10:00
J. Nick Koston
02f828fcbf [benchmark] Use -Os to match firmware optimization level
CodSpeed benchmarks were building with -O2, while all firmware
targets (ESP8266, ESP32, LibreTiny) use -Os. This mismatch means
the benchmarks cannot detect inlining regressions that affect real
devices — GCC under -O2 inlines functions that -Os outlines due to
its size-conscious cost model.

Switch to -Os with -ffunction-sections/-fdata-sections for proper
dead-code stripping (needed because -Os preserves references that
-O2 optimizes away at compile time).
2026-04-12 18:37:50 -10:00
J. Nick Koston
ab64916c37 [benchmark] Use -Os to match firmware optimization level
CodSpeed benchmarks were building with -O2, while all firmware
targets (ESP8266, ESP32, LibreTiny) use -Os. This mismatch means
the benchmarks cannot detect inlining regressions that affect real
devices — GCC under -O2 inlines functions that -Os outlines due to
its size-conscious cost model.

Remove the -Os unflag and -O2 override so benchmarks use the
platform default -Os, matching what actually runs on devices.
2026-04-12 18:32:03 -10:00
172 changed files with 1734 additions and 4543 deletions

View File

@@ -1 +1 @@
075ed2142432dc59883bb52db8ac11270f952851d6400deae080f5468c7cb592
d48687d988ae2a94a9973226df773478a7db1d52133545f07aa05e34fc678dcf

View File

@@ -12,7 +12,7 @@
"--privileged",
"-e",
"GIT_EDITOR=code --wait"
// uncomment and edit the path in order to pass through local USB serial to the container
// uncomment and edit the path in order to pass though local USB serial to the conatiner
// , "--device=/dev/ttyACM0"
],
"appPort": 6052,

View File

@@ -22,7 +22,7 @@ runs:
python-version: ${{ inputs.python-version }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
# yamllint disable-line rule:line-length

View File

@@ -27,7 +27,7 @@ jobs:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v2
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}

View File

@@ -47,7 +47,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
# yamllint disable-line rule:line-length
@@ -159,7 +159,7 @@ jobs:
token: ${{ secrets.CODECOV_TOKEN }}
- name: Save Python virtual environment cache
if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
@@ -198,7 +198,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Restore components graph cache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: .temp/components_graph.json
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
@@ -231,7 +231,7 @@ jobs:
echo "benchmarks=$(echo "$output" | jq -r '.benchmarks')" >> $GITHUB_OUTPUT
- name: Save components graph cache
if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: .temp/components_graph.json
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
@@ -253,7 +253,7 @@ jobs:
python-version: "3.13"
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
@@ -387,14 +387,14 @@ jobs:
- name: Cache platformio
if: github.ref == 'refs/heads/dev'
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
@@ -466,14 +466,14 @@ jobs:
- name: Cache platformio
if: github.ref == 'refs/heads/dev'
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
@@ -555,14 +555,14 @@ jobs:
- name: Cache platformio
if: github.ref == 'refs/heads/dev'
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
@@ -817,7 +817,7 @@ jobs:
- name: Restore cached memory analysis
id: cache-memory-analysis
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true'
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: memory-analysis-target.json
key: ${{ steps.cache-key.outputs.cache-key }}
@@ -841,7 +841,7 @@ jobs:
- name: Cache platformio
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.platformio
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
@@ -883,7 +883,7 @@ jobs:
- name: Save memory analysis to cache
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success'
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: memory-analysis-target.json
key: ${{ steps.cache-key.outputs.cache-key }}
@@ -930,7 +930,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache platformio
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.platformio
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}

View File

@@ -58,7 +58,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -86,6 +86,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
with:
category: "/language:${{matrix.language}}"

View File

@@ -8,4 +8,4 @@ on:
jobs:
lock:
uses: esphome/workflows/.github/workflows/lock.yml@3c4e8446aa1029f1c346a482034b3ee1489077ca # 2026.4.0
uses: esphome/workflows/.github/workflows/lock.yml@main

View File

@@ -221,7 +221,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
@@ -256,7 +256,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
@@ -287,7 +287,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}

View File

@@ -11,7 +11,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.15.11
rev: v0.15.10
hooks:
# Run the linter.
- id: ruff

View File

@@ -569,7 +569,6 @@ def wrap_to_code(name, comp):
@functools.wraps(comp.to_code)
async def wrapped(conf):
cg.add(cg.ComponentMarker(name))
cg.add(cg.LineComment(f"{name}:"))
if comp.config_schema is not None:
conf_str = yaml_util.dump(conf)
@@ -751,15 +750,8 @@ def upload_using_esptool(
platformio_api.FlashImage(
path=idedata.firmware_bin_path, offset=firmware_offset
),
*idedata.extra_flash_images,
]
for image in idedata.extra_flash_images:
if not image.path.is_file():
_LOGGER.warning(
"Skipping missing flash image declared by platform: %s",
image.path,
)
continue
flash_images.append(image)
mcu = "esp8266"
if CORE.is_esp32:

View File

@@ -199,10 +199,11 @@ def validate_automation(extra_schema=None, extra_validators=None, single=False):
return cv.Schema([schema])(value)
except cv.Invalid as err2:
if "extra keys not allowed" in str(err2) and len(err2.path) == 2:
raise err from None
# pylint: disable=raise-missing-from
raise err
if "Unable to find action" in str(err):
raise err2 from None
raise cv.MultipleInvalid([err, err2]) from None
raise err2
raise cv.MultipleInvalid([err, err2])
elif isinstance(value, dict):
if CONF_THEN in value:
return [schema(value)]

View File

@@ -151,8 +151,8 @@ class ConfigBundleCreator:
def __init__(self, config: dict[str, Any]) -> None:
self._config = config
self._config_dir = Path(CORE.config_dir).resolve()
self._config_path = Path(CORE.config_path).resolve()
self._config_dir = CORE.config_dir
self._config_path = CORE.config_path
self._files: list[BundleFile] = []
self._seen_paths: set[Path] = set()
self._secrets_paths: set[Path] = set()
@@ -258,36 +258,21 @@ class ConfigBundleCreator:
def _discover_yaml_includes(self) -> None:
"""Discover YAML files loaded during config parsing.
Deliberately uses a fresh re-parse and force-loads every deferred
``IncludeFile`` to include *all* potentially-reachable includes,
even branches not selected by the local substitutions. Bundles are
meant to be compiled on another system where command-line
substitution overrides may choose a different branch — e.g.
``!include network/${eth_model}/config.yaml`` must ship every
candidate so the remote build can pick any one.
Entries with unresolved substitution variables in the filename
path are skipped with a warning (they cannot be resolved without
the substitution pass).
We track files by wrapping _load_yaml_internal. The config has already
been loaded at this point (bundle is a POST_CONFIG_ACTION), so we
re-load just to discover the file list.
Secrets files are tracked separately so we can filter them to
only include the keys this config actually references.
"""
# Must be a fresh parse: IncludeFile.load() caches its result in
# _content, and we discover files by listening for loader calls. On
# an already-parsed tree the cache is populated, .load() returns
# without calling the loader, the listener never fires, and the
# referenced files would be silently dropped from the bundle.
with yaml_util.track_yaml_loads() as loaded_files:
try:
data = yaml_util.load_yaml(self._config_path)
yaml_util.load_yaml(self._config_path)
except EsphomeError:
_LOGGER.debug(
"Bundle: re-loading YAML for include discovery failed, "
"proceeding with partial file list"
)
else:
_force_load_include_files(data)
for fpath in loaded_files:
if fpath == self._config_path.resolve():
@@ -623,57 +608,6 @@ def _add_bytes_to_tar(tar: tarfile.TarFile, name: str, data: bytes) -> None:
tar.addfile(info, io.BytesIO(data))
def _force_load_include_files(obj: Any, _seen: set[int] | None = None) -> None:
"""Recursively resolve any ``IncludeFile`` instances in a YAML tree.
Nested ``!include`` returns a deferred ``IncludeFile`` that is only
resolved during the substitution pass. During bundle discovery we need
the referenced files to actually load so the ``track_yaml_loads``
listener fires for them.
``IncludeFile`` instances with unresolved substitution variables in the
filename cannot be loaded — we skip and warn about those.
"""
if _seen is None:
_seen = set()
if isinstance(obj, yaml_util.IncludeFile):
if id(obj) in _seen:
return
_seen.add(id(obj))
if obj.has_unresolved_expressions():
_LOGGER.warning(
"Bundle: cannot resolve !include %s (referenced from %s) "
"with substitutions in path",
obj.file,
obj.parent_file,
)
return
try:
loaded = obj.load()
except EsphomeError as err:
_LOGGER.warning(
"Bundle: failed to load !include %s (referenced from %s): %s",
obj.file,
obj.parent_file,
err,
)
return
_force_load_include_files(loaded, _seen)
elif isinstance(obj, dict):
if id(obj) in _seen:
return
_seen.add(id(obj))
for value in obj.values():
_force_load_include_files(value, _seen)
elif isinstance(obj, (list, tuple)):
if id(obj) in _seen:
return
_seen.add(id(obj))
for item in obj:
_force_load_include_files(item, _seen)
def _resolve_include_path(include_path: Any) -> Path | None:
"""Resolve an include path to absolute, skipping system includes."""
if isinstance(include_path, str) and include_path.startswith("<"):

View File

@@ -10,10 +10,8 @@
# pylint: disable=unused-import
from esphome.cpp_generator import ( # noqa: F401
ArrayInitializer,
ComponentMarker,
Expression,
FlashStringLiteral,
IIFEUnsafeStatement,
LineComment,
LogStringLiteral,
MockObj,

View File

@@ -2,11 +2,7 @@ import logging
import esphome.codegen as cg
from esphome.components import sensor, voltage_sampler
from esphome.components.esp32 import (
get_esp32_variant,
include_builtin_idf_component,
require_adc_oneshot_iram,
)
from esphome.components.esp32 import get_esp32_variant, include_builtin_idf_component
from esphome.components.nrf52.const import AIN_TO_GPIO, EXTRA_ADC
from esphome.components.zephyr import (
zephyr_add_overlay,
@@ -28,7 +24,6 @@ from esphome.const import (
PlatformFramework,
)
from esphome.core import CORE
from esphome.types import ConfigType
from . import (
ATTENUATION_MODES,
@@ -70,13 +65,6 @@ def validate_config(config):
return config
def _require_adc_iram(config: ConfigType) -> ConfigType:
"""Register ADC oneshot IRAM requirement during config validation."""
if CORE.is_esp32:
require_adc_oneshot_iram()
return config
ADCSensor = adc_ns.class_(
"ADCSensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler
)
@@ -107,7 +95,6 @@ CONFIG_SCHEMA = cv.All(
)
.extend(cv.polling_component_schema("60s")),
validate_config,
_require_adc_iram,
)
CONF_ADC_CHANNEL_ID = "adc_channel_id"

View File

@@ -1,5 +1,6 @@
import base64
import logging
import pathlib
from esphome import automation
from esphome.automation import Condition
@@ -458,6 +459,10 @@ async def to_code(config: ConfigType) -> None:
# Enable optimized memzero/memcmp in libsodium instead of volatile byte loops
cg.add_build_flag("-DHAVE_WEAK_SYMBOLS=1")
cg.add_build_flag("-DHAVE_INLINE_ASM=1")
# Compile crypto libraries with -O2 for speed instead of -Os.
# Crypto is CPU-bound and benefits significantly from speed optimization.
# GCC uses the last -O flag, so appending -O2 overrides the global -Os.
_write_crypto_optimize_script()
else:
cg.add_define("USE_API_PLAINTEXT")
@@ -465,6 +470,17 @@ async def to_code(config: ConfigType) -> None:
cg.add_global(api_ns.using)
_CRYPTO_OPTIMIZE_SCRIPT = "crypto_optimize.py"
def _write_crypto_optimize_script() -> None:
from esphome.helpers import copy_file_if_changed
script_src = pathlib.Path(__file__).parent / f"{_CRYPTO_OPTIMIZE_SCRIPT}.script"
copy_file_if_changed(script_src, CORE.relative_build_path(_CRYPTO_OPTIMIZE_SCRIPT))
cg.add_platformio_option("extra_scripts", [f"post:{_CRYPTO_OPTIMIZE_SCRIPT}"])
KEY_VALUE_SCHEMA = cv.Schema({cv.string: cv.templatable(cv.string_strict)})

View File

@@ -778,10 +778,9 @@ message SubscribeLogsResponse {
option (source) = SOURCE_SERVER;
option (log) = false;
option (no_delay) = false;
option (speed_optimized) = true;
LogLevel level = 1 [(force) = true];
bytes message = 3 [(force) = true];
LogLevel level = 1;
bytes message = 3;
}
// ==================== NOISE ENCRYPTION ====================

View File

@@ -745,9 +745,8 @@ uint32_t ListEntitiesSensorResponse::calculate_size() const {
#endif
return size;
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
uint8_t *
SensorStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
__attribute__((optimize("O2"))) uint8_t *SensorStateResponse::encode(
ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
uint8_t *__restrict__ pos = buffer.get_pos();
ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key);
ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 2, this->state);
@@ -757,9 +756,7 @@ SensorStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) c
#endif
return pos;
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
uint32_t
SensorStateResponse::calculate_size() const {
__attribute__((optimize("O2"))) uint32_t SensorStateResponse::calculate_size() const {
uint32_t size = 0;
size += 5;
size += ProtoSize::calc_float(1, this->state);
@@ -916,22 +913,16 @@ bool SubscribeLogsRequest::decode_varint(uint32_t field_id, proto_varint_value_t
}
return true;
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
uint8_t *
SubscribeLogsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
uint8_t *SubscribeLogsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
uint8_t *__restrict__ pos = buffer.get_pos();
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, static_cast<uint32_t>(this->level), true);
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 26);
ProtoEncode::encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, this->message_len_);
ProtoEncode::encode_raw(pos PROTO_ENCODE_DEBUG_ARG, this->message_ptr_, this->message_len_);
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, static_cast<uint32_t>(this->level));
ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 3, this->message_ptr_, this->message_len_);
return pos;
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
uint32_t
SubscribeLogsResponse::calculate_size() const {
uint32_t SubscribeLogsResponse::calculate_size() const {
uint32_t size = 0;
size += 2;
size += ProtoSize::calc_length_force(1, this->message_len_);
size += this->level ? 2 : 0;
size += ProtoSize::calc_length(1, this->message_len_);
return size;
}
#ifdef USE_API_NOISE
@@ -2338,9 +2329,8 @@ bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id,
}
return true;
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
uint8_t *
BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
__attribute__((optimize("O2"))) uint8_t *BluetoothLERawAdvertisementsResponse::encode(
ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
uint8_t *__restrict__ pos = buffer.get_pos();
for (uint16_t i = 0; i < this->advertisements_len; i++) {
auto &sub_msg = this->advertisements[i];
@@ -2362,9 +2352,7 @@ BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCO
}
return pos;
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
uint32_t
BluetoothLERawAdvertisementsResponse::calculate_size() const {
__attribute__((optimize("O2"))) uint32_t BluetoothLERawAdvertisementsResponse::calculate_size() const {
uint32_t size = 0;
for (uint16_t i = 0; i < this->advertisements_len; i++) {
auto &sub_msg = this->advertisements[i];

View File

@@ -0,0 +1,9 @@
# Compile crypto libraries with -O2 for speed instead of the default -Os.
# Crypto is CPU-bound and benefits significantly from speed optimization.
# GCC uses the last -O flag, so appending -O2 overrides the global -Os
# for these libraries only.
Import("env")
for lb in env.GetLibBuilders():
if lb.name in ("noise-c", "libsodium"):
lb.env.Append(CCFLAGS=["-O2"])

View File

@@ -83,7 +83,7 @@ def angle_to_position(value, min=-360, max=360):
value = angle(min=min, max=max)(value)
return (RESOLUTION + round(value * ANGLE_TO_POSITION)) % RESOLUTION
except cv.Invalid as e:
raise cv.Invalid(f"When using angle, {e.error_message}") from e
raise cv.Invalid(f"When using angle, {e.error_message}")
def percent_to_position(value):
@@ -164,7 +164,7 @@ def has_valid_range_config():
except cv.Invalid as e:
raise cv.Invalid(
f"The range between start and end position is invalid. It was was {range} but {e.error_message}"
) from e
)
return validator

View File

@@ -111,14 +111,14 @@ class ATM90E32Component : public PollingComponent,
#endif
float get_reference_voltage(uint8_t phase) {
#ifdef USE_NUMBER
return (phase < 3 && ref_voltages_[phase]) ? ref_voltages_[phase]->state : 120.0; // Default voltage
return (phase >= 0 && phase < 3 && ref_voltages_[phase]) ? ref_voltages_[phase]->state : 120.0; // Default voltage
#else
return 120.0; // Default voltage
#endif
}
float get_reference_current(uint8_t phase) {
#ifdef USE_NUMBER
return (phase < 3 && ref_currents_[phase]) ? ref_currents_[phase]->state : 5.0f; // Default current
return (phase >= 0 && phase < 3 && ref_currents_[phase]) ? ref_currents_[phase]->state : 5.0f; // Default current
#else
return 5.0f; // Default current
#endif

View File

@@ -1,11 +1,7 @@
from dataclasses import dataclass
import esphome.codegen as cg
from esphome.components.esp32 import (
add_idf_component,
add_idf_sdkconfig_option,
include_builtin_idf_component,
)
from esphome.components.esp32 import add_idf_component, include_builtin_idf_component
import esphome.config_validation as cv
from esphome.const import CONF_BITS_PER_SAMPLE, CONF_NUM_CHANNELS, CONF_SAMPLE_RATE
from esphome.core import CORE
@@ -31,7 +27,6 @@ class AudioData:
flac_support: bool = False
mp3_support: bool = False
opus_support: bool = False
micro_decoder_support: bool = False
def _get_data() -> AudioData:
@@ -55,11 +50,6 @@ def request_opus_support() -> None:
_get_data().opus_support = True
def request_micro_decoder_support() -> None:
"""Request micro-decoder library support for audio decoding."""
_get_data().micro_decoder_support = True
CONF_MIN_BITS_PER_SAMPLE = "min_bits_per_sample"
CONF_MAX_BITS_PER_SAMPLE = "max_bits_per_sample"
CONF_MIN_CHANNELS = "min_channels"
@@ -218,19 +208,6 @@ async def to_code(config):
)
data = _get_data()
if data.micro_decoder_support:
add_idf_component(name="esphome/micro-decoder", ref="0.1.1")
# All codecs are enabled by default in micro-decoder, so disable the ones that aren't requested to save flash
if not data.flac_support:
add_idf_sdkconfig_option("CONFIG_MICRO_DECODER_CODEC_FLAC", False)
if not data.mp3_support:
add_idf_sdkconfig_option("CONFIG_MICRO_DECODER_CODEC_MP3", False)
if not data.opus_support:
add_idf_sdkconfig_option("CONFIG_MICRO_DECODER_CODEC_OPUS", False)
# Legacy audio_decoder.cpp support defines and components
if data.flac_support:
cg.add_define("USE_AUDIO_FLAC_SUPPORT")
add_idf_component(name="esphome/micro-flac", ref="0.1.1")

View File

@@ -116,7 +116,7 @@ def read_audio_file_and_type(file_config: ConfigType) -> tuple[bytes, MockObj]:
raise cv.Invalid(
f"Unable to determine audio file type of '{path}'. "
f"Try re-encoding the file into a supported format. Details: {e}"
) from e
)
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["NONE"]
if file_type == "wav":

View File

@@ -332,9 +332,8 @@ def parse_multi_click_timing_str(value):
try:
state = cv.boolean(parts[0])
except cv.Invalid:
raise cv.Invalid(
f"First word must either be ON or OFF, not {parts[0]}"
) from None
# pylint: disable=raise-missing-from
raise cv.Invalid(f"First word must either be ON or OFF, not {parts[0]}")
if parts[1] != "for":
raise cv.Invalid(f"Second word must be 'for', got {parts[1]}")
@@ -351,9 +350,7 @@ def parse_multi_click_timing_str(value):
try:
length = cv.positive_time_period_milliseconds(parts[4])
except cv.Invalid as err:
raise cv.Invalid(
f"Multi Click Grammar Parsing length failed: {err}"
) from err
raise cv.Invalid(f"Multi Click Grammar Parsing length failed: {err}")
return {CONF_STATE: state, key: str(length)}
if parts[3] != "to":
@@ -362,16 +359,12 @@ def parse_multi_click_timing_str(value):
try:
min_length = cv.positive_time_period_milliseconds(parts[2])
except cv.Invalid as err:
raise cv.Invalid(
f"Multi Click Grammar Parsing minimum length failed: {err}"
) from err
raise cv.Invalid(f"Multi Click Grammar Parsing minimum length failed: {err}")
try:
max_length = cv.positive_time_period_milliseconds(parts[4])
except cv.Invalid as err:
raise cv.Invalid(
f"Multi Click Grammar Parsing maximum length failed: {err}"
) from err
raise cv.Invalid(f"Multi Click Grammar Parsing minimum length failed: {err}")
return {
CONF_STATE: state,

View File

@@ -65,8 +65,3 @@ async def to_code(config):
@pins.PIN_SCHEMA_REGISTRY.register("bk72xx", PIN_SCHEMA)
async def pin_to_code(config):
return await libretiny.gpio.component_pin_to_code(config)
# Called by writer.py; delegates to the shared libretiny implementation.
def copy_files() -> None:
libretiny.copy_files()

View File

@@ -63,7 +63,7 @@ void BM8563::read_time() {
rtc_time.day_of_week, rtc_time.hour, rtc_time.minute, rtc_time.second);
rtc_time.recalc_timestamp_utc(false);
if (!rtc_time.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) {
if (!rtc_time.is_valid()) {
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
return;
}

View File

@@ -17,7 +17,6 @@ CODEOWNERS = ["@neffs", "@kbx81"]
DOMAIN = "bme68x_bsec2"
BSEC2_LIBRARY_VERSION = "1.10.2610"
BME68x_LIBRARY_VERSION = "v1.3.40408"
CONF_ALGORITHM_OUTPUT = "algorithm_output"
CONF_BME68X_BSEC2_ID = "bme68x_bsec2_id"
@@ -171,9 +170,7 @@ async def to_code_base(config):
with open(path, encoding="utf-8") as f:
bsec2_iaq_config = f.read()
except Exception as e:
raise core.EsphomeError(
f"Could not open binary configuration file {path}: {e}"
) from e
raise core.EsphomeError(f"Could not open binary configuration file {path}: {e}")
# Convert retrieved BSEC2 config to an array of ints
rhs = [int(x) for x in bsec2_iaq_config.split(",")]
@@ -187,31 +184,16 @@ async def to_code_base(config):
if core.CORE.using_arduino:
cg.add_library("Wire", None)
cg.add_library("SPI", None)
if core.CORE.is_esp32:
from esphome.components.esp32 import add_idf_component
add_idf_component(
name="boschsensortec/Bosch-BME68x-Library",
repo="https://github.com/esphome-libs/Bosch-BME68x-Library",
ref=BME68x_LIBRARY_VERSION,
)
add_idf_component(
name="boschsensortec/Bosch-BSEC2-Library",
repo="https://github.com/esphome-libs/Bosch-BSEC2-Library",
ref=BSEC2_LIBRARY_VERSION,
)
else:
cg.add_library(
"BME68x Sensor library",
None,
f"https://github.com/boschsensortec/Bosch-BME68x-Library#{BME68x_LIBRARY_VERSION}",
)
cg.add_library(
"BSEC2 Software Library",
None,
f"https://github.com/boschsensortec/Bosch-BSEC2-Library.git#{BSEC2_LIBRARY_VERSION}",
)
cg.add_library(
"BME68x Sensor library",
None,
"https://github.com/boschsensortec/Bosch-BME68x-Library#v1.3.40408",
)
cg.add_library(
"BSEC2 Software Library",
None,
f"https://github.com/boschsensortec/Bosch-BSEC2-Library.git#{BSEC2_LIBRARY_VERSION}",
)
cg.add_define("USE_BSEC2")

View File

@@ -44,7 +44,7 @@ void DS1307Component::read_time() {
.year = uint16_t(ds1307_.reg.year + 10u * ds1307_.reg.year_10 + 2000),
};
rtc_time.recalc_timestamp_utc(false);
if (!rtc_time.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) {
if (!rtc_time.is_valid()) {
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
return;
}

View File

@@ -676,7 +676,7 @@ ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
"dev": cv.Version(3, 3, 8),
}
ARDUINO_PLATFORM_VERSION_LOOKUP = {
cv.Version(3, 3, 8): cv.Version(55, 3, 38, "1"),
cv.Version(3, 3, 8): cv.Version(55, 3, 38),
cv.Version(3, 3, 7): cv.Version(55, 3, 37),
cv.Version(3, 3, 6): cv.Version(55, 3, 36),
cv.Version(3, 3, 5): cv.Version(55, 3, 35),
@@ -724,7 +724,7 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = {
cv.Version(
6, 0, 0
): "https://github.com/pioarduino/platform-espressif32.git#prep_IDF6",
cv.Version(5, 5, 4): cv.Version(55, 3, 38, "1"),
cv.Version(5, 5, 4): cv.Version(55, 3, 38),
cv.Version(5, 5, 3, "1"): cv.Version(55, 3, 37),
cv.Version(5, 5, 3): cv.Version(55, 3, 37),
cv.Version(5, 5, 2): cv.Version(55, 3, 37),
@@ -744,8 +744,8 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = {
# The platform-espressif32 version
# - https://github.com/pioarduino/platform-espressif32/releases
PLATFORM_VERSION_LOOKUP = {
"recommended": cv.Version(55, 3, 38, "1"),
"latest": cv.Version(55, 3, 38, "1"),
"recommended": cv.Version(55, 3, 38),
"latest": cv.Version(55, 3, 38),
"dev": "https://github.com/pioarduino/platform-espressif32.git#develop",
}
@@ -1058,7 +1058,6 @@ CONF_DISABLE_MBEDTLS_PEER_CERT = "disable_mbedtls_peer_cert"
CONF_DISABLE_MBEDTLS_PKCS7 = "disable_mbedtls_pkcs7"
CONF_DISABLE_REGI2C_IN_IRAM = "disable_regi2c_in_iram"
CONF_DISABLE_FATFS = "disable_fatfs"
CONF_ADC_ONESHOT_IN_IRAM = "adc_oneshot_in_iram"
# VFS requirement tracking
# Components that need VFS features can call require_vfs_*() functions
@@ -1072,7 +1071,6 @@ KEY_MBEDTLS_PEER_CERT_REQUIRED = "mbedtls_peer_cert_required"
KEY_MBEDTLS_PKCS7_REQUIRED = "mbedtls_pkcs7_required"
KEY_FATFS_REQUIRED = "fatfs_required"
KEY_MBEDTLS_SHA512_REQUIRED = "mbedtls_sha512_required"
KEY_ADC_ONESHOT_IRAM_REQUIRED = "adc_oneshot_iram_required"
def require_vfs_select() -> None:
@@ -1170,17 +1168,6 @@ def require_fatfs() -> None:
CORE.data[KEY_ESP32][KEY_FATFS_REQUIRED] = True
def require_adc_oneshot_iram() -> None:
"""Mark that ADC oneshot IRAM safety is required by a component.
Call this from components that use the ADC oneshot driver. When flash cache is
disabled (e.g., during NVS writes by WiFi, BLE, Zigbee, or power management),
the ADC oneshot read function must be in IRAM to avoid crashes.
This sets CONFIG_ADC_ONESHOT_CTRL_FUNC_IN_IRAM.
"""
CORE.data[KEY_ESP32][KEY_ADC_ONESHOT_IRAM_REQUIRED] = True
def _parse_idf_component(value: str) -> ConfigType:
"""Parse IDF component shorthand syntax like 'owner/component^version'"""
# Match operator followed by version-like string (digit or *)
@@ -1222,7 +1209,7 @@ FRAMEWORK_SCHEMA = cv.Schema(
cv.Optional(CONF_IGNORE_EFUSE_CUSTOM_MAC, default=False): cv.boolean,
cv.Optional(CONF_IGNORE_EFUSE_MAC_CRC, default=False): cv.boolean,
cv.Optional(CONF_MINIMUM_CHIP_REVISION): cv.one_of(
*ESP32_CHIP_REVISIONS, string=True
*ESP32_CHIP_REVISIONS
),
cv.Optional(CONF_SRAM1_AS_IRAM, default=False): cv.boolean,
# DHCP server is needed for WiFi AP mode. When WiFi component is used,
@@ -1281,7 +1268,6 @@ FRAMEWORK_SCHEMA = cv.Schema(
cv.Optional(CONF_DISABLE_MBEDTLS_PEER_CERT, default=True): cv.boolean,
cv.Optional(CONF_DISABLE_MBEDTLS_PKCS7, default=True): cv.boolean,
cv.Optional(CONF_DISABLE_REGI2C_IN_IRAM, default=True): cv.boolean,
cv.Optional(CONF_ADC_ONESHOT_IN_IRAM, default=False): cv.boolean,
cv.Optional(CONF_DISABLE_FATFS, default=True): cv.boolean,
}
),
@@ -2082,16 +2068,6 @@ async def to_code(config):
if advanced[CONF_DISABLE_REGI2C_IN_IRAM]:
add_idf_sdkconfig_option("CONFIG_ESP_REGI2C_CTRL_FUNC_IN_IRAM", False)
# Place ADC oneshot control functions in IRAM for cache safety
# When flash cache is disabled (during NVS writes by WiFi, BLE, Zigbee, Thread,
# power management, etc.), ADC reads will crash if these functions are in flash.
# Components using ADC call require_adc_oneshot_iram() to force this.
if (
CORE.data[KEY_ESP32].get(KEY_ADC_ONESHOT_IRAM_REQUIRED, False)
or advanced[CONF_ADC_ONESHOT_IN_IRAM]
):
add_idf_sdkconfig_option("CONFIG_ADC_ONESHOT_CTRL_FUNC_IN_IRAM", True)
# Disable FATFS support
# Components that need FATFS (SD card, etc.) can call require_fatfs()
if CORE.data[KEY_ESP32].get(KEY_FATFS_REQUIRED, False):

View File

@@ -61,9 +61,6 @@ uint32_t arch_get_cpu_freq_hz() {
}
TaskHandle_t loop_task_handle = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static StaticTask_t loop_task_tcb; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static StackType_t
loop_task_stack[ESPHOME_LOOP_TASK_STACK_SIZE]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void loop_task(void *pv_params) {
setup();
@@ -76,11 +73,9 @@ extern "C" void app_main() {
initArduino();
esp32::setup_preferences();
#if CONFIG_FREERTOS_UNICORE
loop_task_handle = xTaskCreateStatic(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, loop_task_stack,
&loop_task_tcb);
xTaskCreate(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, &loop_task_handle);
#else
loop_task_handle = xTaskCreateStaticPinnedToCore(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1,
loop_task_stack, &loop_task_tcb, 1);
xTaskCreatePinnedToCore(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, &loop_task_handle, 1);
#endif
}

View File

@@ -4,6 +4,7 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <nvs_flash.h>
#include <cinttypes>
#include <cstring>
#include <vector>
@@ -11,6 +12,9 @@ namespace esphome::esp32 {
static const char *const TAG = "preferences";
// Buffer size for converting uint32_t to string: max "4294967295" (10 chars) + null terminator + 1 padding
static constexpr size_t KEY_BUFFER_SIZE = 12;
struct NVSData {
uint32_t key;
SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.)
@@ -47,8 +51,8 @@ bool ESP32PreferenceBackend::load(uint8_t *data, size_t len) {
}
}
char key_str[UINT32_MAX_STR_SIZE];
uint32_to_str(key_str, this->key);
char key_str[KEY_BUFFER_SIZE];
snprintf(key_str, sizeof(key_str), "%" PRIu32, this->key);
size_t actual_len;
esp_err_t err = nvs_get_blob(this->nvs_handle, key_str, nullptr, &actual_len);
if (err != 0) {
@@ -104,8 +108,8 @@ bool ESP32Preferences::sync() {
uint32_t last_key = 0;
for (const auto &save : s_pending_save) {
char key_str[UINT32_MAX_STR_SIZE];
uint32_to_str(key_str, save.key);
char key_str[KEY_BUFFER_SIZE];
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str);
if (this->is_changed_(this->nvs_handle, save, key_str)) {
esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.data(), save.data.size());

View File

@@ -78,14 +78,6 @@ def ota_esphome_final_validate(config):
else:
new_ota_conf.append(ota_conf)
if len(merged_ota_esphome_configs_by_port) > 1:
raise cv.Invalid(
f"Only a single port is supported for '{CONF_OTA}' "
f"'{CONF_PLATFORM}: {CONF_ESPHOME}'. Got ports "
f"{sorted(merged_ota_esphome_configs_by_port.keys())}. Consolidate "
f"onto a single port; configs sharing a port are merged automatically."
)
new_ota_conf.extend(merged_ota_esphome_configs_by_port.values())
full_conf[CONF_OTA] = new_ota_conf
@@ -155,8 +147,6 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_auth_password(config[CONF_PASSWORD]))
cg.add_define("USE_OTA_PASSWORD")
cg.add_define("USE_OTA_VERSION", config[CONF_VERSION])
# Build flag so lwip_fast_select.c (a .c file that can't include defines.h) sees it.
cg.add_build_flag("-DUSE_OTA_PLATFORM_ESPHOME")
await cg.register_component(var, config)
await ota_to_code(var, config)

View File

@@ -15,9 +15,6 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/util.h"
#ifdef USE_LWIP_FAST_SELECT
#include "esphome/core/lwip_fast_select.h"
#endif
#include <cerrno>
#include <cstdio>
@@ -31,17 +28,6 @@ static constexpr size_t OTA_BUFFER_SIZE = 1024; // buffer size
static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 20000; // milliseconds for initial handshake
static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer
// Single-instance pointer — multi-port configs are rejected in final_validate.
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
static ESPHomeOTAComponent *global_esphome_ota_component = nullptr;
// Called from any context (LwIP TCP/IP task, RP2040 user-IRQ).
extern "C" void esphome_wake_ota_component_any_context() {
if (global_esphome_ota_component != nullptr) {
global_esphome_ota_component->enable_loop_soon_any_context();
}
}
void ESPHomeOTAComponent::setup() {
this->server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0).release(); // monitored for incoming connections
if (this->server_ == nullptr) {
@@ -79,14 +65,6 @@ void ESPHomeOTAComponent::setup() {
this->server_failed_(LOG_STR("listen"));
return;
}
// loop() self-disables on its first idle tick; no explicit disable_loop() needed here.
global_esphome_ota_component = this;
#ifdef USE_LWIP_FAST_SELECT
// Filter fast-select wakes to this listener only. If the sock lookup returns nullptr,
// no wakes fire and loop() falls back to the self-disable safety net.
esphome_fast_select_set_ota_listener_sock(esphome_lwip_get_sock(this->server_->get_fd()));
#endif
}
void ESPHomeOTAComponent::dump_config() {
@@ -103,15 +81,13 @@ void ESPHomeOTAComponent::dump_config() {
}
void ESPHomeOTAComponent::loop() {
// Self-disabling idle loop. Runs when a wake path marks us pending-enable (fast-select
// listener filter, raw-TCP accept_fn_, or host select), finds no work, and goes back
// to sleep. cleanup_connection_() deliberately leaves the loop enabled for one more
// iteration so a connection queued mid-session is still caught here.
if (this->client_ == nullptr && !this->server_->ready()) {
this->disable_loop();
return;
// Skip handle_handshake_() call if no client connected and no incoming connections
// This optimization reduces idle loop overhead when OTA is not active
// Note: No need to check server_ for null as the component is marked failed in setup()
// if server_ creation fails
if (this->client_ != nullptr || this->server_->ready()) {
this->handle_handshake_();
}
this->handle_handshake_();
}
static const uint8_t FEATURE_SUPPORTS_COMPRESSION = 0x01;
@@ -590,9 +566,6 @@ void ESPHomeOTAComponent::cleanup_connection_() {
#ifdef USE_OTA_PASSWORD
this->cleanup_auth_();
#endif
// Intentionally no disable_loop() — letting loop() run one more iteration catches
// any connection that queued on the listener mid-session (otherwise the wake flag,
// set while we were in LOOP state, would be lost to enable_pending_loops_()).
}
void ESPHomeOTAComponent::yield_and_feed_watchdog_() {

View File

@@ -221,7 +221,7 @@ class EthernetComponent final : public Component {
int reset_pin_{-1};
int phy_addr_spi_{-1};
int clock_speed_;
spi_host_device_t interface_{SPI2_HOST};
spi_host_device_t interface_{SPI3_HOST};
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
uint32_t polling_interval_{0};
#endif

View File

@@ -325,7 +325,7 @@ def download_gfont(value):
raise cv.Invalid(
f"Could not download font at {url}, please check the fonts exists "
f"at google fonts ({e})"
) from e
)
match = re.search(r"src:\s+url\((.+)\)\s+format\('truetype'\);", req.text)
if match is None:
raise cv.Invalid(

View File

@@ -108,13 +108,8 @@ async def globals_set_to_code(config, action_id, template_arg, args):
full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID])
template_arg = cg.TemplateArguments(full_id.type, *template_arg)
var = cg.new_Pvariable(action_id, template_arg, paren)
# Use the global's value_type alias as the lambda return type so
# TemplatableFn stores a direct function pointer instead of going through
# the deprecated converting trampoline when the value expression deduces
# to a different type (e.g. int literal assigned to a float global).
value_type = cg.RawExpression(f"{full_id.type}::value_type")
templ = await cg.templatable(
config[CONF_VALUE], args, value_type, to_exp=cg.RawExpression
config[CONF_VALUE], args, None, to_exp=cg.RawExpression, wrap_constant=True
)
cg.add(var.set_value(templ))
return var

View File

@@ -60,73 +60,6 @@ CONFIG_SCHEMA = (
)
def _pin_shared_only_with_deep_sleep(pin_num: int) -> bool:
"""Check if pin is shared exclusively with deep_sleep (wakeup pin)."""
pin_key = (CORE.target_platform, CORE.target_platform, pin_num)
pin_users = pins.PIN_SCHEMA_REGISTRY.pins_used.get(pin_key, [])
if len(pin_users) != 2:
return False
return any(path and path[0] == "deep_sleep" for path, _, _ in pin_users)
def _final_validate(config):
use_interrupt = config[CONF_USE_INTERRUPT]
if not use_interrupt:
return config
pin_num = config[CONF_PIN][CONF_NUMBER]
# Expander pins (e.g. PCF8574, MCP23017) don't support direct interrupt
# attachment — only internal/native GPIO pins do.
if pins.PIN_SCHEMA_REGISTRY.get_key(config[CONF_PIN]) != CORE.target_platform:
_LOGGER.info(
"GPIO binary_sensor '%s': Pin is not an internal GPIO, "
"falling back to polling mode.",
config.get(CONF_NAME, config[CONF_ID]),
)
config[CONF_USE_INTERRUPT] = False
return config
# GPIO16 on ESP8266 doesn't support interrupts through attachInterrupt().
if CORE.is_esp8266 and pin_num == 16:
_LOGGER.warning(
"GPIO binary_sensor '%s': GPIO16 on ESP8266 doesn't support interrupts. "
"Falling back to polling mode (same as in ESPHome <2025.7). "
"The sensor will work exactly as before, but other pins have better "
"performance with interrupts.",
config.get(CONF_NAME, config[CONF_ID]),
)
config[CONF_USE_INTERRUPT] = False
return config
# When a pin is shared, interrupts can interfere with other components
# (e.g., duty_cycle sensor) that need to monitor the pin's state changes.
# Exception: deep_sleep wakeup pins are compatible with interrupts when
# the pin is only shared between this sensor and deep_sleep (count == 2).
if config[CONF_PIN].get(CONF_ALLOW_OTHER_USES, False):
if not _pin_shared_only_with_deep_sleep(pin_num):
_LOGGER.info(
"GPIO binary_sensor '%s': Disabling interrupts because pin %s is shared "
"with other components. The sensor will use polling mode for "
"compatibility with other pin uses.",
config.get(CONF_NAME, config[CONF_ID]),
pin_num,
)
config[CONF_USE_INTERRUPT] = False
else:
_LOGGER.debug(
"GPIO binary_sensor '%s': Pin %s is shared with deep_sleep, "
"keeping interrupts enabled.",
config.get(CONF_NAME, config[CONF_ID]),
pin_num,
)
return config
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config):
var = await binary_sensor.new_binary_sensor(config)
await cg.register_component(var, config)
@@ -134,7 +67,36 @@ async def to_code(config):
pin = await cg.gpio_pin_expression(config[CONF_PIN])
cg.add(var.set_pin(pin))
if config[CONF_USE_INTERRUPT]:
# Check for ESP8266 GPIO16 interrupt limitation
# GPIO16 on ESP8266 is a special pin that doesn't support interrupts through
# the Arduino attachInterrupt() function. This is the only known GPIO pin
# across all supported platforms that has this limitation, so we handle it
# here instead of in the platform-specific code.
use_interrupt = config[CONF_USE_INTERRUPT]
if use_interrupt and CORE.is_esp8266 and config[CONF_PIN][CONF_NUMBER] == 16:
_LOGGER.warning(
"GPIO binary_sensor '%s': GPIO16 on ESP8266 doesn't support interrupts. "
"Falling back to polling mode (same as in ESPHome <2025.7). "
"The sensor will work exactly as before, but other pins have better "
"performance with interrupts.",
config.get(CONF_NAME, config[CONF_ID]),
)
use_interrupt = False
# Check if pin is shared with other components (allow_other_uses)
# When a pin is shared, interrupts can interfere with other components
# (e.g., duty_cycle sensor) that need to monitor the pin's state changes
if use_interrupt and config[CONF_PIN].get(CONF_ALLOW_OTHER_USES, False):
_LOGGER.info(
"GPIO binary_sensor '%s': Disabling interrupts because pin %s is shared with other components. "
"The sensor will use polling mode for compatibility with other pin uses.",
config.get(CONF_NAME, config[CONF_ID]),
config[CONF_PIN][CONF_NUMBER],
)
use_interrupt = False
if use_interrupt:
cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE]))
else:
cg.add(var.set_use_interrupt(False))
# Only generate call when disabling interrupts (default is true)
cg.add(var.set_use_interrupt(use_interrupt))

View File

@@ -46,6 +46,11 @@ void GPIOBinarySensorStore::setup(InternalGPIOPin *pin, Component *component) {
}
void GPIOBinarySensor::setup() {
if (this->store_.use_interrupt_ && !this->pin_->is_internal()) {
ESP_LOGD(TAG, "GPIO is not internal, falling back to polling mode");
this->store_.use_interrupt_ = false;
}
if (this->store_.use_interrupt_) {
auto *internal_pin = static_cast<InternalGPIOPin *>(this->pin_);
this->store_.setup(internal_pin, this);

View File

@@ -127,6 +127,6 @@ async def to_code(config):
cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE]))
cg.add_build_flag("-Wno-error=overloaded-virtual")
cg.add_library("tonia/HeatpumpIR", "1.0.41")
cg.add_library("tonia/HeatpumpIR", "1.0.40")
if CORE.is_libretiny or CORE.is_esp32:
CORE.add_platformio_option("lib_ignore", ["IRremoteESP8266"])

View File

@@ -36,7 +36,7 @@ I2SAudioMicrophone = i2s_audio_ns.class_(
)
INTERNAL_ADC_VARIANTS = [esp32.VARIANT_ESP32]
PDM_VARIANTS = [esp32.VARIANT_ESP32, esp32.VARIANT_ESP32S3, esp32.VARIANT_ESP32P4]
PDM_VARIANTS = [esp32.VARIANT_ESP32, esp32.VARIANT_ESP32S3]
def _validate_esp32_variant(config):

View File

@@ -283,7 +283,7 @@ async def to_code(config):
try:
return Image.open(path)
except Exception as e:
raise core.EsphomeError(f"Could not load image file {path}: {e}") from e
raise core.EsphomeError(f"Could not load image file {path}: {e}")
# make a wide horizontal combined image.
images = [load_image(x) for x in config[CONF_COLOR_PALETTE_IMAGES]]

View File

@@ -1,7 +1,5 @@
#pragma once
#include <cstdint>
namespace esphome {
namespace ili9xxx {

View File

@@ -229,10 +229,6 @@ void ILI9XXXDisplay::update() {
}
void ILI9XXXDisplay::display_() {
// buffer may be null if allocation failed
if (this->buffer_ == nullptr) {
return;
}
// check if something was displayed
if ((this->x_high_ < this->x_low_) || (this->y_high_ < this->y_low_)) {
return;

View File

@@ -28,6 +28,7 @@ from esphome.const import (
CONF_URL,
)
from esphome.core import CORE, HexInt
from esphome.final_validate import full_config
_LOGGER = logging.getLogger(__name__)
@@ -675,16 +676,12 @@ def _final_validate(config):
:param config:
:return:
"""
config = config.copy()
for c in config:
if byte_order := c.get(CONF_BYTE_ORDER):
if byte_order == "BIG_ENDIAN":
_LOGGER.warning(
"The image '%s' is configured with big-endian byte order, little-endian is expected",
c.get(CONF_FILE),
)
else:
c[CONF_BYTE_ORDER] = "LITTLE_ENDIAN"
fv = full_config.get()
if "lvgl" in fv and not all(CONF_BYTE_ORDER in x for x in config):
config = config.copy()
for c in config:
if not c.get(CONF_BYTE_ORDER):
c[CONF_BYTE_ORDER] = "LITTLE_ENDIAN"
return config

View File

@@ -189,7 +189,7 @@ Color Image::get_rgb_pixel_(int x, int y) const {
}
Color Image::get_rgb565_pixel_(int x, int y) const {
const uint8_t *pos = this->data_start_ + (x + y * this->width_) * this->bpp_ / 8;
uint16_t rgb565 = encode_uint16(progmem_read_byte(pos + 1), progmem_read_byte(pos));
uint16_t rgb565 = encode_uint16(progmem_read_byte(pos), progmem_read_byte(pos + 1));
auto r = (rgb565 & 0xF800) >> 11;
auto g = (rgb565 & 0x07E0) >> 5;
auto b = rgb565 & 0x001F;

View File

@@ -360,8 +360,8 @@ void LD2410Component::handle_periodic_data_() {
*/
#ifdef USE_SENSOR
SAFE_PUBLISH_SENSOR(this->moving_target_distance_sensor_,
encode_uint16(this->buffer_data_[MOVING_TARGET_HIGH], this->buffer_data_[MOVING_TARGET_LOW]));
SAFE_PUBLISH_SENSOR(this->moving_target_energy_sensor_, this->buffer_data_[MOVING_ENERGY]);
encode_uint16(this->buffer_data_[MOVING_TARGET_HIGH], this->buffer_data_[MOVING_TARGET_LOW]))
SAFE_PUBLISH_SENSOR(this->moving_target_energy_sensor_, this->buffer_data_[MOVING_ENERGY])
SAFE_PUBLISH_SENSOR(this->still_target_distance_sensor_,
encode_uint16(this->buffer_data_[STILL_TARGET_HIGH], this->buffer_data_[STILL_TARGET_LOW]));
SAFE_PUBLISH_SENSOR(this->still_target_energy_sensor_, this->buffer_data_[STILL_ENERGY]);
@@ -375,26 +375,26 @@ void LD2410Component::handle_periodic_data_() {
Moving energy: 20~28th bytes
*/
for (uint8_t i = 0; i < TOTAL_GATES; i++) {
SAFE_PUBLISH_SENSOR(this->gate_move_sensors_[i], this->buffer_data_[MOVING_SENSOR_START + i]);
SAFE_PUBLISH_SENSOR(this->gate_move_sensors_[i], this->buffer_data_[MOVING_SENSOR_START + i])
}
/*
Still energy: 29~37th bytes
*/
for (uint8_t i = 0; i < TOTAL_GATES; i++) {
SAFE_PUBLISH_SENSOR(this->gate_still_sensors_[i], this->buffer_data_[STILL_SENSOR_START + i]);
SAFE_PUBLISH_SENSOR(this->gate_still_sensors_[i], this->buffer_data_[STILL_SENSOR_START + i])
}
/*
Light sensor: 38th bytes
*/
SAFE_PUBLISH_SENSOR(this->light_sensor_, this->buffer_data_[LIGHT_SENSOR]);
SAFE_PUBLISH_SENSOR(this->light_sensor_, this->buffer_data_[LIGHT_SENSOR])
} else {
for (auto &gate_move_sensor : this->gate_move_sensors_) {
SAFE_PUBLISH_SENSOR_UNKNOWN(gate_move_sensor);
SAFE_PUBLISH_SENSOR_UNKNOWN(gate_move_sensor)
}
for (auto &gate_still_sensor : this->gate_still_sensors_) {
SAFE_PUBLISH_SENSOR_UNKNOWN(gate_still_sensor);
SAFE_PUBLISH_SENSOR_UNKNOWN(gate_still_sensor)
}
SAFE_PUBLISH_SENSOR_UNKNOWN(this->light_sensor_);
SAFE_PUBLISH_SENSOR_UNKNOWN(this->light_sensor_)
}
#endif
#ifdef USE_BINARY_SENSOR
@@ -786,12 +786,13 @@ void LD2410Component::set_light_out_control() {
}
#ifdef USE_SENSOR
// These could leak memory, but they are only set once prior to 'setup()' and should never be used again.
void LD2410Component::set_gate_move_sensor(uint8_t gate, sensor::Sensor *s) {
this->gate_move_sensors_[gate].set_sensor(s);
this->gate_move_sensors_[gate] = new SensorWithDedup<uint8_t>(s);
}
void LD2410Component::set_gate_still_sensor(uint8_t gate, sensor::Sensor *s) {
this->gate_still_sensors_[gate].set_sensor(s);
this->gate_still_sensors_[gate] = new SensorWithDedup<uint8_t>(s);
}
#endif

View File

@@ -129,8 +129,8 @@ class LD2410Component : public Component, public uart::UARTDevice {
std::array<number::Number *, TOTAL_GATES> gate_still_threshold_numbers_{};
#endif
#ifdef USE_SENSOR
std::array<SensorWithDedup<uint8_t>, TOTAL_GATES> gate_move_sensors_{};
std::array<SensorWithDedup<uint8_t>, TOTAL_GATES> gate_still_sensors_{};
std::array<SensorWithDedup<uint8_t> *, TOTAL_GATES> gate_move_sensors_{};
std::array<SensorWithDedup<uint8_t> *, TOTAL_GATES> gate_still_sensors_{};
#endif
};

View File

@@ -397,12 +397,12 @@ void LD2412Component::handle_periodic_data_() {
*/
#ifdef USE_SENSOR
SAFE_PUBLISH_SENSOR(this->moving_target_distance_sensor_,
encode_uint16(this->buffer_data_[MOVING_TARGET_HIGH], this->buffer_data_[MOVING_TARGET_LOW]));
SAFE_PUBLISH_SENSOR(this->moving_target_energy_sensor_, this->buffer_data_[MOVING_ENERGY]);
encode_uint16(this->buffer_data_[MOVING_TARGET_HIGH], this->buffer_data_[MOVING_TARGET_LOW]))
SAFE_PUBLISH_SENSOR(this->moving_target_energy_sensor_, this->buffer_data_[MOVING_ENERGY])
SAFE_PUBLISH_SENSOR(this->still_target_distance_sensor_,
encode_uint16(this->buffer_data_[STILL_TARGET_HIGH], this->buffer_data_[STILL_TARGET_LOW]));
SAFE_PUBLISH_SENSOR(this->still_target_energy_sensor_, this->buffer_data_[STILL_ENERGY]);
if (this->detection_distance_sensor_.has_sensor()) {
encode_uint16(this->buffer_data_[STILL_TARGET_HIGH], this->buffer_data_[STILL_TARGET_LOW]))
SAFE_PUBLISH_SENSOR(this->still_target_energy_sensor_, this->buffer_data_[STILL_ENERGY])
if (this->detection_distance_sensor_ != nullptr) {
int new_detect_distance = 0;
if (target_state != 0x00 && (target_state & MOVE_BITMASK)) {
new_detect_distance =
@@ -410,7 +410,7 @@ void LD2412Component::handle_periodic_data_() {
} else if (target_state != 0x00) {
new_detect_distance = encode_uint16(this->buffer_data_[STILL_TARGET_HIGH], this->buffer_data_[STILL_TARGET_LOW]);
}
this->detection_distance_sensor_.publish_state_if_not_dup(new_detect_distance);
this->detection_distance_sensor_->publish_state_if_not_dup(new_detect_distance);
}
if (engineering_mode) {
// Engineering mode needs at least LIGHT_SENSOR + 1 bytes
@@ -423,27 +423,27 @@ void LD2412Component::handle_periodic_data_() {
Moving energy: 20~28th bytes
*/
for (uint8_t i = 0; i < TOTAL_GATES; i++) {
SAFE_PUBLISH_SENSOR(this->gate_move_sensors_[i], this->buffer_data_[MOVING_SENSOR_START + i]);
SAFE_PUBLISH_SENSOR(this->gate_move_sensors_[i], this->buffer_data_[MOVING_SENSOR_START + i])
}
/*
Still energy: 29~37th bytes
*/
for (uint8_t i = 0; i < TOTAL_GATES; i++) {
SAFE_PUBLISH_SENSOR(this->gate_still_sensors_[i], this->buffer_data_[STILL_SENSOR_START + i]);
SAFE_PUBLISH_SENSOR(this->gate_still_sensors_[i], this->buffer_data_[STILL_SENSOR_START + i])
}
/*
Light sensor value
*/
SAFE_PUBLISH_SENSOR(this->light_sensor_, this->buffer_data_[LIGHT_SENSOR]);
SAFE_PUBLISH_SENSOR(this->light_sensor_, this->buffer_data_[LIGHT_SENSOR])
}
} else {
for (auto &gate_move_sensor : this->gate_move_sensors_) {
SAFE_PUBLISH_SENSOR_UNKNOWN(gate_move_sensor);
SAFE_PUBLISH_SENSOR_UNKNOWN(gate_move_sensor)
}
for (auto &gate_still_sensor : this->gate_still_sensors_) {
SAFE_PUBLISH_SENSOR_UNKNOWN(gate_still_sensor);
SAFE_PUBLISH_SENSOR_UNKNOWN(gate_still_sensor)
}
SAFE_PUBLISH_SENSOR_UNKNOWN(this->light_sensor_);
SAFE_PUBLISH_SENSOR_UNKNOWN(this->light_sensor_)
}
#endif
// the radar module won't tell us when it's done, so we just have to keep polling...
@@ -846,11 +846,12 @@ void LD2412Component::set_light_out_control() {
}
#ifdef USE_SENSOR
// These could leak memory, but they are only set once prior to 'setup()' and should never be used again.
void LD2412Component::set_gate_move_sensor(uint8_t gate, sensor::Sensor *s) {
this->gate_move_sensors_[gate].set_sensor(s);
this->gate_move_sensors_[gate] = new SensorWithDedup<uint8_t>(s);
}
void LD2412Component::set_gate_still_sensor(uint8_t gate, sensor::Sensor *s) {
this->gate_still_sensors_[gate].set_sensor(s);
this->gate_still_sensors_[gate] = new SensorWithDedup<uint8_t>(s);
}
#endif

View File

@@ -133,8 +133,8 @@ class LD2412Component : public Component, public uart::UARTDevice {
std::array<number::Number *, TOTAL_GATES> gate_still_threshold_numbers_{};
#endif
#ifdef USE_SENSOR
std::array<SensorWithDedup<uint8_t>, TOTAL_GATES> gate_move_sensors_{};
std::array<SensorWithDedup<uint8_t>, TOTAL_GATES> gate_still_sensors_{};
std::array<SensorWithDedup<uint8_t> *, TOTAL_GATES> gate_move_sensors_{};
std::array<SensorWithDedup<uint8_t> *, TOTAL_GATES> gate_still_sensors_{};
#endif
};

View File

@@ -565,7 +565,6 @@ void LD2450Component::handle_periodic_data_() {
SAFE_PUBLISH_SENSOR(this->still_target_count_sensor_, still_target_count);
// Moving Target Count
SAFE_PUBLISH_SENSOR(this->moving_target_count_sensor_, moving_target_count);
#endif
#ifdef USE_BINARY_SENSOR
@@ -873,32 +872,33 @@ void LD2450Component::query_target_tracking_mode_() { this->send_command_(CMD_QU
void LD2450Component::query_zone_() { this->send_command_(CMD_QUERY_ZONE, nullptr, 0); }
#ifdef USE_SENSOR
// These could leak memory, but they are only set once prior to 'setup()' and should never be used again.
void LD2450Component::set_move_x_sensor(uint8_t target, sensor::Sensor *s) {
this->move_x_sensors_[target].set_sensor(s);
this->move_x_sensors_[target] = new SensorWithDedup<int16_t>(s);
}
void LD2450Component::set_move_y_sensor(uint8_t target, sensor::Sensor *s) {
this->move_y_sensors_[target].set_sensor(s);
this->move_y_sensors_[target] = new SensorWithDedup<int16_t>(s);
}
void LD2450Component::set_move_speed_sensor(uint8_t target, sensor::Sensor *s) {
this->move_speed_sensors_[target].set_sensor(s);
this->move_speed_sensors_[target] = new SensorWithDedup<int16_t>(s);
}
void LD2450Component::set_move_angle_sensor(uint8_t target, sensor::Sensor *s) {
this->move_angle_sensors_[target].set_sensor(s);
this->move_angle_sensors_[target] = new SensorWithDedup<float>(s);
}
void LD2450Component::set_move_distance_sensor(uint8_t target, sensor::Sensor *s) {
this->move_distance_sensors_[target].set_sensor(s);
this->move_distance_sensors_[target] = new SensorWithDedup<uint16_t>(s);
}
void LD2450Component::set_move_resolution_sensor(uint8_t target, sensor::Sensor *s) {
this->move_resolution_sensors_[target].set_sensor(s);
this->move_resolution_sensors_[target] = new SensorWithDedup<uint16_t>(s);
}
void LD2450Component::set_zone_target_count_sensor(uint8_t zone, sensor::Sensor *s) {
this->zone_target_count_sensors_[zone].set_sensor(s);
this->zone_target_count_sensors_[zone] = new SensorWithDedup<uint8_t>(s);
}
void LD2450Component::set_zone_still_target_count_sensor(uint8_t zone, sensor::Sensor *s) {
this->zone_still_target_count_sensors_[zone].set_sensor(s);
this->zone_still_target_count_sensors_[zone] = new SensorWithDedup<uint8_t>(s);
}
void LD2450Component::set_zone_moving_target_count_sensor(uint8_t zone, sensor::Sensor *s) {
this->zone_moving_target_count_sensors_[zone].set_sensor(s);
this->zone_moving_target_count_sensors_[zone] = new SensorWithDedup<uint8_t>(s);
}
#endif
#ifdef USE_TEXT_SENSOR

View File

@@ -182,15 +182,15 @@ class LD2450Component : public Component, public uart::UARTDevice {
ZoneOfNumbers zone_numbers_[MAX_ZONES];
#endif
#ifdef USE_SENSOR
std::array<SensorWithDedup<int16_t>, MAX_TARGETS> move_x_sensors_{};
std::array<SensorWithDedup<int16_t>, MAX_TARGETS> move_y_sensors_{};
std::array<SensorWithDedup<int16_t>, MAX_TARGETS> move_speed_sensors_{};
std::array<SensorWithDedup<float>, MAX_TARGETS> move_angle_sensors_{};
std::array<SensorWithDedup<uint16_t>, MAX_TARGETS> move_distance_sensors_{};
std::array<SensorWithDedup<uint16_t>, MAX_TARGETS> move_resolution_sensors_{};
std::array<SensorWithDedup<uint8_t>, MAX_ZONES> zone_target_count_sensors_{};
std::array<SensorWithDedup<uint8_t>, MAX_ZONES> zone_still_target_count_sensors_{};
std::array<SensorWithDedup<uint8_t>, MAX_ZONES> zone_moving_target_count_sensors_{};
std::array<SensorWithDedup<int16_t> *, MAX_TARGETS> move_x_sensors_{};
std::array<SensorWithDedup<int16_t> *, MAX_TARGETS> move_y_sensors_{};
std::array<SensorWithDedup<int16_t> *, MAX_TARGETS> move_speed_sensors_{};
std::array<SensorWithDedup<float> *, MAX_TARGETS> move_angle_sensors_{};
std::array<SensorWithDedup<uint16_t> *, MAX_TARGETS> move_distance_sensors_{};
std::array<SensorWithDedup<uint16_t> *, MAX_TARGETS> move_resolution_sensors_{};
std::array<SensorWithDedup<uint8_t> *, MAX_ZONES> zone_target_count_sensors_{};
std::array<SensorWithDedup<uint8_t> *, MAX_ZONES> zone_still_target_count_sensors_{};
std::array<SensorWithDedup<uint8_t> *, MAX_ZONES> zone_moving_target_count_sensors_{};
#endif
#ifdef USE_TEXT_SENSOR
std::array<text_sensor::TextSensor *, MAX_TARGETS> direction_text_sensors_{};

View File

@@ -11,20 +11,28 @@
#define SUB_SENSOR_WITH_DEDUP(name, dedup_type) \
protected: \
ld24xx::SensorWithDedup<dedup_type> name##_sensor_{}; \
ld24xx::SensorWithDedup<dedup_type> *name##_sensor_{nullptr}; \
\
public: \
void set_##name##_sensor(sensor::Sensor *sensor) { this->name##_sensor_.set_sensor(sensor); }
void set_##name##_sensor(sensor::Sensor *sensor) { \
this->name##_sensor_ = new ld24xx::SensorWithDedup<dedup_type>(sensor); \
}
#endif
#define LOG_SENSOR_WITH_DEDUP_SAFE(tag, name, sensor) \
if ((sensor).has_sensor()) { \
LOG_SENSOR(tag, name, (sensor).get_sensor()); \
if ((sensor) != nullptr) { \
LOG_SENSOR(tag, name, (sensor)->sens); \
}
#define SAFE_PUBLISH_SENSOR(sensor, value) (sensor).publish_state_if_not_dup(value)
#define SAFE_PUBLISH_SENSOR(sensor, value) \
if ((sensor) != nullptr) { \
(sensor)->publish_state_if_not_dup(value); \
}
#define SAFE_PUBLISH_SENSOR_UNKNOWN(sensor) (sensor).publish_state_unknown()
#define SAFE_PUBLISH_SENSOR_UNKNOWN(sensor) \
if ((sensor) != nullptr) { \
(sensor)->publish_state_unknown(); \
}
#define highbyte(val) (uint8_t)((val) >> 8)
#define lowbyte(val) (uint8_t)((val) &0xff)
@@ -62,33 +70,25 @@ inline void format_version_str(const uint8_t *version, std::span<char, 20> buffe
}
#ifdef USE_SENSOR
/// Sensor with deduplication — sensor may be null, null check is internal.
/// Stored inline, no heap allocation. Does nothing when no sensor is set.
// Helper class to store a sensor with a deduplicator & publish state only when the value changes
template<typename T> class SensorWithDedup {
public:
void set_sensor(sensor::Sensor *sens) {
this->sens_ = sens;
this->dedup_ = {};
}
SensorWithDedup(sensor::Sensor *sens) : sens(sens) {}
void publish_state_if_not_dup(T state) {
if (this->sens_ != nullptr && this->dedup_.next(state)) {
this->sens_->publish_state(static_cast<float>(state));
if (this->publish_dedup.next(state)) {
this->sens->publish_state(static_cast<float>(state));
}
}
void publish_state_unknown() {
if (this->sens_ != nullptr && this->dedup_.next_unknown()) {
this->sens_->publish_state(NAN);
if (this->publish_dedup.next_unknown()) {
this->sens->publish_state(NAN);
}
}
bool has_sensor() const { return this->sens_ != nullptr; }
sensor::Sensor *get_sensor() const { return this->sens_; }
protected:
sensor::Sensor *sens_{nullptr};
Deduplicator<T> dedup_;
sensor::Sensor *sens;
Deduplicator<T> publish_dedup;
};
#endif
} // namespace esphome::ld24xx

View File

@@ -1,6 +1,5 @@
import json
import logging
from pathlib import Path
import esphome.codegen as cg
import esphome.config_validation as cv
@@ -25,7 +24,6 @@ from esphome.const import (
)
from esphome.core import CORE
from esphome.core.config import BOARD_MAX_LENGTH
from esphome.helpers import copy_file_if_changed
from esphome.storage_json import StorageJSON
from . import gpio # noqa
@@ -467,11 +465,6 @@ async def component_to_code(config):
# it for project source files only. GCC uses the last -O flag.
build_src_flags += " -Os"
cg.add_platformio_option("build_src_flags", build_src_flags)
# IRAM_ATTR is a no-op on BK72xx (SDK masks FIQ+IRQ around flash ops).
# On other families, patch_linker.py routes .sram.text into the right
# RAM-executable output section and prints a post-link placement summary.
if FAMILY_COMPONENT[config[CONF_FAMILY]] != COMPONENT_BK72XX:
cg.add_platformio_option("extra_scripts", ["pre:patch_linker.py"])
# dummy version code
cg.add_define("USE_ARDUINO_VERSION_CODE", cg.RawExpression("VERSION_CODE(0, 0, 0)"))
# decrease web server stack size (16k words -> 4k words)
@@ -556,13 +549,3 @@ async def component_to_code(config):
_configure_lwip(config)
await cg.register_component(var, config)
# Called by writer.py
def copy_files() -> None:
script_dir = Path(__file__).parent
patch_linker_file = script_dir / "patch_linker.py.script"
copy_file_if_changed(
patch_linker_file,
CORE.relative_build_path("patch_linker.py"),
)

View File

@@ -79,11 +79,6 @@ async def to_code(config):
@pins.PIN_SCHEMA_REGISTRY.register("{COMPONENT_LOWER}", PIN_SCHEMA)
async def pin_to_code(config):
return await libretiny.gpio.component_pin_to_code(config)
# Called by writer.py; delegates to the shared libretiny implementation.
def copy_files() -> None:
libretiny.copy_files()
'''
BASE_CODE_BOARDS = '''

View File

@@ -1,171 +0,0 @@
# pylint: disable=E0602
Import("env") # noqa
import os
import re
import subprocess
# ESPHome marks ISR code IRAM_ATTR, which on LibreTiny maps to a per-family
# section routed into RAM-executable memory (see esphome/core/hal.h).
#
# This script is NOT loaded on BK72xx (IRAM_ATTR is a no-op there; the SDK
# masks FIQ+IRQ around flash writes). On the remaining families:
# - RTL8710B: hal.h uses section(".image2.ram.text"); stock linker consumes it.
# - RTL8720C: hal.h uses section(".sram.text"); stock linker consumes it.
# - LN882H: stock linker has no glob for ".sram.text", so we inject
# KEEP(*(.sram.text*)) into ".flash_copysection" (> RAM0 AT> FLASH).
#
# All families also get a post-link summary showing where IRAM_ATTR landed.
_MARKER = "/* esphome .sram.text */"
# Strong assignments (not PROVIDE) so the symbols are always emitted in the
# ELF; PROVIDE symbols with no references can be garbage-collected.
_KEEP_LINE = (
" __esphome_sram_text_start = .; "
"KEEP(*(.sram.text*)) "
"__esphome_sram_text_end = .; "
+ _MARKER + "\n"
)
_LN_COPY = re.compile(r"(\.flash_copysection\s*:\s*\{\s*\n)")
def _detect(env):
prefix = "USE_LIBRETINY_VARIANT_"
# CPPDEFINES may hold strings or (name, value) tuples; BUILD_FLAGS holds
# the raw "-DNAME" strings. PlatformIO populates both, but the exact order
# vs. extra_scripts varies, so check both to be robust.
for token in env.get("CPPDEFINES", []):
if isinstance(token, (list, tuple)):
token = token[0]
if isinstance(token, str) and token.startswith(prefix):
return token[len(prefix):]
for flag in env.get("BUILD_FLAGS", []):
if isinstance(flag, str) and "-D" + prefix in flag:
name = flag.split("-D", 1)[1].split("=", 1)[0].strip()
if name.startswith(prefix):
return name[len(prefix):]
return None
KNOWN_VARIANTS = frozenset({
"LN882H",
"RTL8710B",
"RTL8720C",
})
def _inject_keep(host_section):
"""Return a patcher that injects _KEEP_LINE at the top of `host_section`."""
def patch(content):
if _MARKER in content:
return content
return host_section.sub(r"\1" + _KEEP_LINE, content, count=1)
return patch
# Variants not listed here intentionally have no .ld patcher:
# - RTL8710B: hal.h uses section(".image2.ram.text") which the stock linker
# already routes into .ram_image2.text (> BD_RAM).
# - RTL8720C: stock linker already consumes *(.sram.text*).
# - BK72xx (all): SDK masks FIQ+IRQ around flash writes, IRAM_ATTR is no-op.
_PATCHERS_BY_VARIANT = {
"LN882H": (_inject_keep(_LN_COPY),),
}
def _patchers_for(variant):
return _PATCHERS_BY_VARIANT.get(variant, ())
def _pre_link(target, source, env):
build_dir = env.subst("$BUILD_DIR")
ld_files = [f for f in os.listdir(build_dir) if f.endswith(".ld")]
patched = 0
for name in ld_files:
path = os.path.join(build_dir, name)
with open(path, "r", encoding="utf-8") as fh:
original = fh.read()
if _MARKER in original:
patched += 1
continue
content = original
for fn in _patchers:
content = fn(content)
if content != original:
with open(path, "w", encoding="utf-8") as fh:
fh.write(content)
print("ESPHome: patched {} for IRAM_ATTR placement".format(name))
patched += 1
if not patched:
raise RuntimeError(
"ESPHome: no .ld in {} was patched for IRAM_ATTR. Update the "
"regex in patch_linker.py.script (_PATCHERS_BY_VARIANT).".format(
build_dir
)
)
# Substrings matched against demangled names as a fallback on RTL8720C,
# where we cannot inject __esphome_sram_text_start/end markers.
_FALLBACK_SUBSTRINGS = ("wake_loop_any_context", "wake_loop_isrsafe",
"enable_loop_soon_any_context")
def _post_link(target, source, env):
"""Print where IRAM_ATTR ended up so users can confirm at a glance."""
elf = env.subst("$BUILD_DIR/${PROGNAME}.elf")
if not os.path.isfile(elf):
return
nm = env.subst("$NM")
try:
out = subprocess.check_output(
[nm, "--defined-only", "--demangle", elf], text=True
)
except (OSError, subprocess.CalledProcessError) as exc:
print("ESPHome: IRAM_ATTR summary unavailable (nm failed: {})".format(exc))
return
start = end = None
fallback = []
for line in out.splitlines():
parts = line.split(maxsplit=2)
if len(parts) != 3:
continue
addr_str, _kind, name = parts
if name == "__esphome_sram_text_start":
start = int(addr_str, 16)
elif name == "__esphome_sram_text_end":
end = int(addr_str, 16)
elif "veneer" not in name and any(s in name for s in _FALLBACK_SUBSTRINGS):
fallback.append(int(addr_str, 16))
print("ESPHome: IRAM_ATTR placement summary ({}):".format(_variant))
if start is not None and end is not None:
print(" .sram.text: {} bytes at 0x{:08x} - 0x{:08x}".format(end - start, start, end))
elif fallback:
lo, hi = min(fallback), max(fallback)
print(" IRAM symbols at 0x{:08x} - 0x{:08x} (approx {} bytes)".format(lo, hi, hi - lo))
else:
print(" no IRAM_ATTR symbols found")
if (_variant := _detect(env)) is None:
raise RuntimeError(
"ESPHome: could not determine LibreTiny variant from build flags. "
"patch_linker.py needs USE_LIBRETINY_VARIANT_* to route IRAM_ATTR "
"into SRAM; without it, ISR handlers would silently end up in flash."
)
if _variant not in KNOWN_VARIANTS:
raise RuntimeError(
"ESPHome: unknown LibreTiny variant {!r}; patch_linker.py does not "
"know how to route IRAM_ATTR into SRAM for this family. Update "
"patch_linker.py.script before shipping firmware.".format(_variant)
)
if _patchers := _patchers_for(_variant):
# LibreTiny writes the processed .ld templates into $BUILD_DIR during its
# own builder setup, which may run after this script. Register the patch
# as a pre-link action so it executes once the linker scripts exist.
env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", _pre_link)
# Post-link summary for every family that reaches this script.
env.AddPostAction("$BUILD_DIR/${PROGNAME}.elf", _post_link)

View File

@@ -3,6 +3,7 @@
#include "preferences.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <cinttypes>
#include <cstring>
#include <vector>
@@ -10,6 +11,9 @@ namespace esphome::libretiny {
static const char *const TAG = "preferences";
// Buffer size for converting uint32_t to string: max "4294967295" (10 chars) + null terminator + 1 padding
static constexpr size_t KEY_BUFFER_SIZE = 12;
struct NVSData {
uint32_t key;
SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.)
@@ -46,8 +50,8 @@ bool LibreTinyPreferenceBackend::load(uint8_t *data, size_t len) {
}
}
char key_str[UINT32_MAX_STR_SIZE];
uint32_to_str(key_str, this->key);
char key_str[KEY_BUFFER_SIZE];
snprintf(key_str, sizeof(key_str), "%" PRIu32, this->key);
fdb_blob_make(this->blob, data, len);
size_t actual_len = fdb_kv_get_blob(this->db, key_str, this->blob);
if (actual_len != len) {
@@ -88,8 +92,8 @@ bool LibreTinyPreferences::sync() {
uint32_t last_key = 0;
for (const auto &save : s_pending_save) {
char key_str[UINT32_MAX_STR_SIZE];
uint32_to_str(key_str, save.key);
char key_str[KEY_BUFFER_SIZE];
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
ESP_LOGVV(TAG, "Checking if FDB data %s has changed", key_str);
if (this->is_changed_(&this->db, save, key_str)) {
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size());

View File

@@ -58,12 +58,6 @@ void AddressableLightTransformer::start() {
// our transition will handle brightness, disable brightness in correction.
this->light_.correction_.set_local_brightness(255);
this->target_color_ *= to_uint8_scale(end_values.get_brightness() * end_values.get_state());
// Uniformity scan is deferred to the first apply() call. start() can run before the underlying
// LED output's setup() has allocated its frame buffer (e.g. on_boot at priority > HARDWARE
// triggering a transition), and reading through ESPColorView would deref a null buffer.
this->uniform_start_scanned_ = false;
this->uniform_start_is_uniform_ = false;
}
inline constexpr uint8_t subtract_scaled_difference(uint8_t a, uint8_t b, int32_t scale) {
@@ -103,57 +97,12 @@ optional<LightColorValues> AddressableLightTransformer::apply() {
// non-linear when applying small deltas.
if (smoothed_progress > this->last_transition_progress_ && this->last_transition_progress_ < 1.f) {
// Lazy uniformity scan: deferred from start() so the LED output's setup() has run and the
// frame buffer is valid. When every LED already has the same color (the common case: plain
// turn_on/turn_off on a uniform strip), interpolate math-only against a single start color.
// Avoiding the per-step read-back through the 8-bit stored byte prevents gamma round-trip
// quantization from stalling the fade at low values (e.g. gamma 2.8 pre-gamma values <27
// round to stored 0, freezing progress).
if (!this->uniform_start_scanned_) {
this->uniform_start_scanned_ = true;
if (this->light_.size() > 0) {
Color first = this->light_[0].get();
bool uniform = true;
for (int32_t i = 1; i < this->light_.size(); i++) {
if (this->light_[i].get() != first) {
uniform = false;
break;
}
}
if (uniform) {
this->uniform_start_color_ = first;
this->uniform_start_is_uniform_ = true;
}
}
}
if (this->uniform_start_is_uniform_) {
// All LEDs started at the same color: compute the interpolated value once and write it to
// every LED. No read-back, so each LED's stored byte advances through every gamma threshold
// as smoothed_progress crosses it, instead of stalling at 0 for low pre-gamma values.
//
// Trade-off: any mid-transition writes to individual LEDs (e.g. from a user lambda) will be
// overwritten on the next apply() here. The fallback path below would have respected them
// via its read-back. Concurrent per-LED mutation during a transition isn't a pattern we
// support, so this is acceptable.
// lerp(start, target, progress) via existing helper: target - (target-start)*(1-progress).
const Color &start = this->uniform_start_color_;
int32_t remaining = int32_t(256.f * (1.f - smoothed_progress));
uint8_t r = subtract_scaled_difference(this->target_color_.red, start.red, remaining);
uint8_t g = subtract_scaled_difference(this->target_color_.green, start.green, remaining);
uint8_t b = subtract_scaled_difference(this->target_color_.blue, start.blue, remaining);
uint8_t w = subtract_scaled_difference(this->target_color_.white, start.white, remaining);
for (auto led : this->light_) {
led.set_rgbw(r, g, b, w);
}
} else {
int32_t scale =
int32_t(256.f * std::max((1.f - smoothed_progress) / (1.f - this->last_transition_progress_), 0.f));
for (auto led : this->light_) {
led.set_rgbw(subtract_scaled_difference(this->target_color_.red, led.get_red(), scale),
subtract_scaled_difference(this->target_color_.green, led.get_green(), scale),
subtract_scaled_difference(this->target_color_.blue, led.get_blue(), scale),
subtract_scaled_difference(this->target_color_.white, led.get_white(), scale));
}
int32_t scale = int32_t(256.f * std::max((1.f - smoothed_progress) / (1.f - this->last_transition_progress_), 0.f));
for (auto led : this->light_) {
led.set_rgbw(subtract_scaled_difference(this->target_color_.red, led.get_red(), scale),
subtract_scaled_difference(this->target_color_.green, led.get_green(), scale),
subtract_scaled_difference(this->target_color_.blue, led.get_blue(), scale),
subtract_scaled_difference(this->target_color_.white, led.get_white(), scale));
}
this->last_transition_progress_ = smoothed_progress;
this->light_.schedule_show();

View File

@@ -115,9 +115,6 @@ class AddressableLightTransformer : public LightTransformer {
AddressableLight &light_;
float last_transition_progress_{0.0f};
Color target_color_{};
Color uniform_start_color_{};
bool uniform_start_scanned_{false};
bool uniform_start_is_uniform_{false};
};
} // namespace esphome::light

View File

@@ -22,20 +22,4 @@ uint8_t ESPColorCorrection::gamma_uncorrect_(uint8_t value) const {
return (target - a <= b - target) ? lo : lo + 1;
}
Color ESPColorCorrection::color_uncorrect(Color color) const {
// uncorrected = corrected^(1/gamma) / (max_brightness * local_brightness)
return Color(this->color_uncorrect_red(color.red), this->color_uncorrect_green(color.green),
this->color_uncorrect_blue(color.blue), this->color_uncorrect_white(color.white));
}
uint8_t ESPColorCorrection::color_uncorrect_channel_(uint8_t value, uint8_t max_brightness) const {
if (max_brightness == 0 || this->local_brightness_ == 0)
return 0;
// Use 32-bit intermediates: when max_brightness and local_brightness_ are small but non-zero,
// (uncorrected / max_brightness) * 255 can exceed 65535 before the std::min(255) clamp runs.
uint32_t uncorrected = this->gamma_uncorrect_(value) * 255UL;
uint32_t res = ((uncorrected / max_brightness) * 255UL) / this->local_brightness_;
return static_cast<uint8_t>(std::min(res, uint32_t(255)));
}
} // namespace esphome::light

View File

@@ -46,18 +46,38 @@ class ESPColorCorrection {
uint8_t res = esp_scale8_twice(white, this->max_brightness_.white, this->local_brightness_);
return this->gamma_correct_(res);
}
Color color_uncorrect(Color color) const;
inline Color color_uncorrect(Color color) const ESPHOME_ALWAYS_INLINE {
// uncorrected = corrected^(1/gamma) / (max_brightness * local_brightness)
return Color(this->color_uncorrect_red(color.red), this->color_uncorrect_green(color.green),
this->color_uncorrect_blue(color.blue), this->color_uncorrect_white(color.white));
}
inline uint8_t color_uncorrect_red(uint8_t red) const ESPHOME_ALWAYS_INLINE {
return this->color_uncorrect_channel_(red, this->max_brightness_.red);
if (this->max_brightness_.red == 0 || this->local_brightness_ == 0)
return 0;
uint16_t uncorrected = this->gamma_uncorrect_(red) * 255UL;
uint16_t res = ((uncorrected / this->max_brightness_.red) * 255UL) / this->local_brightness_;
return (uint8_t) std::min(res, uint16_t(255));
}
inline uint8_t color_uncorrect_green(uint8_t green) const ESPHOME_ALWAYS_INLINE {
return this->color_uncorrect_channel_(green, this->max_brightness_.green);
if (this->max_brightness_.green == 0 || this->local_brightness_ == 0)
return 0;
uint16_t uncorrected = this->gamma_uncorrect_(green) * 255UL;
uint16_t res = ((uncorrected / this->max_brightness_.green) * 255UL) / this->local_brightness_;
return (uint8_t) std::min(res, uint16_t(255));
}
inline uint8_t color_uncorrect_blue(uint8_t blue) const ESPHOME_ALWAYS_INLINE {
return this->color_uncorrect_channel_(blue, this->max_brightness_.blue);
if (this->max_brightness_.blue == 0 || this->local_brightness_ == 0)
return 0;
uint16_t uncorrected = this->gamma_uncorrect_(blue) * 255UL;
uint16_t res = ((uncorrected / this->max_brightness_.blue) * 255UL) / this->local_brightness_;
return (uint8_t) std::min(res, uint16_t(255));
}
inline uint8_t color_uncorrect_white(uint8_t white) const ESPHOME_ALWAYS_INLINE {
return this->color_uncorrect_channel_(white, this->max_brightness_.white);
if (this->max_brightness_.white == 0 || this->local_brightness_ == 0)
return 0;
uint16_t uncorrected = this->gamma_uncorrect_(white) * 255UL;
uint16_t res = ((uncorrected / this->max_brightness_.white) * 255UL) / this->local_brightness_;
return (uint8_t) std::min(res, uint16_t(255));
}
protected:
@@ -65,9 +85,6 @@ class ESPColorCorrection {
uint8_t gamma_correct_(uint8_t value) const;
/// Reverse gamma: binary search the forward PROGMEM table
uint8_t gamma_uncorrect_(uint8_t value) const;
/// Shared body of color_uncorrect_{red,green,blue,white}. Kept out-of-line
/// to avoid duplicating two 16-bit divides at every call site.
uint8_t color_uncorrect_channel_(uint8_t value, uint8_t max_brightness) const;
const uint16_t *gamma_table_{nullptr};
Color max_brightness_{255, 255, 255, 255};

View File

@@ -65,8 +65,3 @@ async def to_code(config):
@pins.PIN_SCHEMA_REGISTRY.register("ln882x", PIN_SCHEMA)
async def pin_to_code(config):
return await libretiny.gpio.component_pin_to_code(config)
# Called by writer.py; delegates to the shared libretiny implementation.
def copy_files() -> None:
libretiny.copy_files()

View File

@@ -44,7 +44,6 @@ from esphome.core import CORE, ID, Lambda
from esphome.cpp_generator import MockObj
from esphome.final_validate import full_config
from esphome.helpers import write_file_if_changed
from esphome.writer import clean_build
from esphome.yaml_util import load_yaml
from . import defines as df, helpers, lv_validation as lvalid, widgets
@@ -452,8 +451,7 @@ async def to_code(configs):
df.add_define(f"LV_DRAW_SW_SUPPORT_{fmt}", "1")
lv_conf_h_file = CORE.relative_src_path(LV_CONF_FILENAME)
if write_file_if_changed(lv_conf_h_file, generate_lv_conf_h()):
clean_build(clear_pio_cache=False)
write_file_if_changed(lv_conf_h_file, generate_lv_conf_h())
cg.add_build_flag("-DLV_CONF_H=1")
# handle windows paths in a way that doesn't break the generated C++
lv_conf_h_path = Path(lv_conf_h_file).as_posix()

View File

@@ -76,17 +76,16 @@ inline void lv_style_set_text_font(lv_style_t *style, const font::Font *font) {
}
#endif
#if defined(USE_LVGL_IMAGE) && defined(USE_IMAGE)
#if LV_USE_IMAGE
// Shortcut / overload, so that the source of an image widget can easily be updated from within a lambda.
inline void lv_image_set_src(lv_obj_t *obj, image::Image *image) { ::lv_image_set_src(obj, image->get_lv_image_dsc()); }
#endif // LV_USE_IMAGE
// Shortcut / overload, so that the source of an image can easily be updated
// from within a lambda.
inline void lv_image_set_src(lv_obj_t *obj, image::Image *image) { lv_image_set_src(obj, image->get_lv_image_dsc()); }
inline void lv_obj_set_style_bitmap_mask_src(lv_obj_t *obj, image::Image *image, lv_style_selector_t selector) {
::lv_obj_set_style_bitmap_mask_src(obj, image->get_lv_image_dsc(), selector);
lv_obj_set_style_bitmap_mask_src(obj, image->get_lv_image_dsc(), selector);
}
inline void lv_obj_set_style_bg_image_src(lv_obj_t *obj, image::Image *image, lv_style_selector_t selector) {
::lv_obj_set_style_bg_image_src(obj, image->get_lv_image_dsc(), selector);
lv_obj_set_style_bg_image_src(obj, image->get_lv_image_dsc(), selector);
}
#endif // USE_LVGL_IMAGE
#ifdef USE_LVGL_ANIMIMG

View File

@@ -452,7 +452,7 @@ async def to_code(config):
esp32.add_idf_component(name="espressif/esp-tflite-micro", ref="1.3.3~1")
# Pin esp-nn for stable future builds (esp-tflite-micro depends on esp-nn)
esp32.add_idf_component(name="espressif/esp-nn", ref="1.1.2")
esp32.add_idf_component(name="espressif/esp-nn", ref="1.2.1")
cg.add_build_flag("-DTF_LITE_STATIC_MEMORY")
cg.add_build_flag("-DTF_LITE_DISABLE_X86_NEON")

View File

@@ -28,8 +28,7 @@ void AirConditioner::on_status_change() {
if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK &&
this->base_.getCapabilities().supportFrostProtectionPreset() && !this->frost_protection_set_) {
// Read existing presets (set by codegen), append frost protection, write back
auto traits = this->get_traits();
const auto &existing = traits.get_supported_custom_presets();
const auto &existing = this->get_traits().get_supported_custom_presets();
bool found = false;
for (const char *p : existing) {
if (strcmp(p, Constants::FREEZE_PROTECTION) == 0) {

View File

@@ -195,7 +195,7 @@ def model_schema(config):
"big_endian", "little_endian", lower=True
),
model.option(CONF_COLOR_DEPTH, 16): cv.one_of(*color_depth, lower=True),
model.option(CONF_DRAW_ROUNDING, 1): power_of_two,
model.option(CONF_DRAW_ROUNDING, 2): power_of_two,
model.option(CONF_PIXEL_MODE, DISPLAY_16BIT): cv.one_of(
*pixel_modes, lower=True
),
@@ -297,9 +297,9 @@ def _final_validate(config):
buffer_size = color_depth // 8 * width * height // frac
# Target a buffer size of 20kB, except for large displays, which shouldn't end up here
fraction = min(20000.0, buffer_size // 4) / buffer_size
fraction = min(20000.0, buffer_size // 16) / buffer_size
config[CONF_BUFFER_SIZE] = 1.0 / next(
(x for x in range(2, 8) if fraction >= 1 / x), 8
x for x in range(2, 17) if fraction >= 1 / x
)

View File

@@ -234,9 +234,9 @@ class MipiSpi : public display::Display,
}
void dump_config() override {
internal_dump_config(this->model_, this->get_width(), this->get_height(), OFFSET_WIDTH, OFFSET_HEIGHT,
(uint8_t) MADCTL, this->invert_colors_, DISPLAYPIXEL * 8, IS_BIG_ENDIAN, this->brightness_,
this->cs_, this->reset_pin_, this->dc_pin_, this->mode_, this->data_rate_, BUS_TYPE,
internal_dump_config(this->model_, this->get_width(), this->get_height(), OFFSET_WIDTH, OFFSET_HEIGHT, MADCTL,
this->invert_colors_, DISPLAYPIXEL * 8, IS_BIG_ENDIAN, this->brightness_, this->cs_,
this->reset_pin_, this->dc_pin_, this->mode_, this->data_rate_, BUS_TYPE,
HAS_HARDWARE_ROTATION);
}
@@ -305,7 +305,7 @@ class MipiSpi : public display::Display,
this->write_command_(BRIGHTNESS, this->brightness_.value());
// calculate new madctl value from base value adjusted for rotation
uint8_t madctl = (uint8_t) MADCTL; // lower 8 bits only
uint8_t madctl = MADCTL; // lower 8 bits only
constexpr bool use_flips = (MADCTL & MADCTL_FLIP_FLAG) != 0;
constexpr uint8_t x_mask = use_flips ? MADCTL_XFLIP : MADCTL_MX;
constexpr uint8_t y_mask = use_flips ? MADCTL_YFLIP : MADCTL_MY;
@@ -546,12 +546,13 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
}
// for updates with a small buffer, we repeatedly call the writer_ function, clipping the height to a fraction of
// the display height,
auto increment = (this->get_height_internal() / FRACTION / ROUNDING) * ROUNDING;
for (this->start_line_ = 0; this->start_line_ < this->get_height_internal(); this->start_line_ = this->end_line_) {
for (this->start_line_ = 0; this->start_line_ < this->get_height_internal();
this->start_line_ += this->get_height_internal() / FRACTION) {
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
auto lap = millis();
#endif
this->end_line_ = clamp_at_most(this->start_line_ + increment, this->get_height_internal());
this->end_line_ =
clamp_at_most(this->start_line_ + this->get_height_internal() / FRACTION, this->get_height_internal());
if (this->auto_clear_enabled_) {
this->clear();
}
@@ -573,13 +574,12 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
// Some chips require that the drawing window be aligned on certain boundaries
this->x_low_ = this->x_low_ / ROUNDING * ROUNDING;
this->y_low_ = this->y_low_ / ROUNDING * ROUNDING;
this->x_high_ = round_buffer(this->x_high_ + 1) - 1;
this->y_high_ = clamp_at_most(round_buffer(this->y_high_ + 1) - 1, this->end_line_ - 1);
this->x_high_ = (this->x_high_ + ROUNDING) / ROUNDING * ROUNDING - 1;
this->y_high_ = (this->y_high_ + ROUNDING) / ROUNDING * ROUNDING - 1;
int w = this->x_high_ - this->x_low_ + 1;
int h = this->y_high_ - this->y_low_ + 1;
this->write_to_display_(this->x_low_, this->y_low_, w, h, this->buffer_, this->x_low_,
this->y_low_ - this->start_line_,
round_buffer(this->get_width_internal()) - w - this->x_low_);
this->y_low_ - this->start_line_, round_buffer(this->get_width_internal()) - w);
// invalidate watermarks
this->x_low_ = this->get_width_internal();
this->y_low_ = this->get_height_internal();

View File

@@ -15,7 +15,7 @@ from esphome.components.mipi import (
import esphome.config_validation as cv
from .amoled import CO5300
from .ili import ILI9488_A, ST7789V
from .ili import ILI9488_A
from .jc import AXS15231
DriverChip(
@@ -243,15 +243,3 @@ ST7789P.extend(
),
),
)
ST7789V.extend(
"WAVESHARE-ESP32-C6-LCD-1.47",
width=172,
height=320,
offset_width=34,
invert_colors=True,
data_rate="40MHz",
reset_pin=21,
cs_pin=14,
dc_pin={"number": 15, "ignore_strapping_warning": True},
)

View File

@@ -7,7 +7,7 @@ namespace esphome::mitsubishi_cn105 {
static const char *const TAG = "mitsubishi_cn105.climate";
static constexpr std::array MODE_MAP{
std::pair{MitsubishiCN105::Mode::AUTO, climate::CLIMATE_MODE_HEAT_COOL},
std::pair{MitsubishiCN105::Mode::AUTO, climate::CLIMATE_MODE_AUTO},
std::pair{MitsubishiCN105::Mode::HEAT, climate::CLIMATE_MODE_HEAT},
std::pair{MitsubishiCN105::Mode::DRY, climate::CLIMATE_MODE_DRY},
std::pair{MitsubishiCN105::Mode::COOL, climate::CLIMATE_MODE_COOL},
@@ -76,13 +76,23 @@ void MitsubishiCN105Climate::loop() {
climate::ClimateTraits MitsubishiCN105Climate::traits() {
climate::ClimateTraits traits;
for (const auto &p : MODE_MAP) {
traits.add_supported_mode(p.second);
}
traits.set_supported_modes({
climate::CLIMATE_MODE_OFF,
climate::CLIMATE_MODE_COOL,
climate::CLIMATE_MODE_HEAT,
climate::CLIMATE_MODE_DRY,
climate::CLIMATE_MODE_FAN_ONLY,
climate::CLIMATE_MODE_AUTO,
});
for (const auto &p : FAN_MODE_MAP) {
traits.add_supported_fan_mode(p.second);
}
traits.set_supported_fan_modes({
climate::CLIMATE_FAN_AUTO,
climate::CLIMATE_FAN_QUIET,
climate::CLIMATE_FAN_LOW,
climate::CLIMATE_FAN_MEDIUM,
climate::CLIMATE_FAN_MIDDLE,
climate::CLIMATE_FAN_HIGH,
});
traits.set_visual_min_temperature(16.0f);
traits.set_visual_max_temperature(31.0f);

View File

@@ -36,9 +36,8 @@ bool Nextion::send_command_(const std::string &command) {
}
#ifdef USE_NEXTION_COMMAND_SPACING
const uint32_t now = App.get_loop_component_start_time();
if (!this->connection_state_.ignore_is_setup_ && !this->command_pacer_.can_send(now)) {
ESP_LOGN(TAG, "Command spacing: delaying '%s'", command.c_str());
if (!this->connection_state_.ignore_is_setup_ && !this->command_pacer_.can_send()) {
ESP_LOGN(TAG, "Command spacing: delaying command '%s'", command.c_str());
return false;
}
#endif // USE_NEXTION_COMMAND_SPACING
@@ -49,16 +48,6 @@ bool Nextion::send_command_(const std::string &command) {
const uint8_t to_send[3] = {0xFF, 0xFF, 0xFF};
this->write_array(to_send, sizeof(to_send));
#ifdef USE_NEXTION_COMMAND_SPACING
// Mark sent immediately after writing to UART. The pacer enforces inter-command
// spacing from the transmit side. Marking on ACK (0x01) would leave last_command_time_
// at zero indefinitely, making can_send() always return true and spacing a no-op.
// ignore_is_setup_ commands (setup/init sequence) bypass spacing intentionally.
if (!this->connection_state_.ignore_is_setup_) {
this->command_pacer_.mark_sent(now);
}
#endif // USE_NEXTION_COMMAND_SPACING
return true;
}
@@ -264,8 +253,11 @@ bool Nextion::send_command(const char *command) {
if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || this->is_sleeping())
return false;
this->add_no_result_to_queue_with_command_("command", command);
return true;
if (this->send_command_(command)) {
this->add_no_result_to_queue_("command");
return true;
}
return false;
}
bool Nextion::send_command_printf(const char *format, ...) {
@@ -282,8 +274,11 @@ bool Nextion::send_command_printf(const char *format, ...) {
return false;
}
this->add_no_result_to_queue_with_command_("command_printf", buffer);
return true;
if (this->send_command_(buffer)) {
this->add_no_result_to_queue_("command_printf");
return true;
}
return false;
}
#ifdef NEXTION_PROTOCOL_LOG
@@ -354,43 +349,25 @@ void Nextion::loop() {
}
#ifdef USE_NEXTION_COMMAND_SPACING
// Try to send any pending commands if spacing allows
this->process_pending_in_queue_();
#ifdef USE_NEXTION_WAVEFORM
if (!this->waveform_queue_.empty()) {
this->check_pending_waveform_();
}
#endif // USE_NEXTION_WAVEFORM
#endif // USE_NEXTION_COMMAND_SPACING
}
#ifdef USE_NEXTION_COMMAND_SPACING
void Nextion::process_pending_in_queue_() {
#ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP
size_t commands_sent = 0;
#endif // USE_NEXTION_MAX_COMMANDS_PER_LOOP
if (this->nextion_queue_.empty() || !this->command_pacer_.can_send()) {
return;
}
for (auto *item : this->nextion_queue_) {
if (item == nullptr || item->pending_command.empty()) {
continue; // Already sent, waiting for ACK — skip, don't stop
// Check if first item in queue has a pending command
auto *front_item = this->nextion_queue_.front();
if (front_item && !front_item->pending_command.empty()) {
if (this->send_command_(front_item->pending_command)) {
// Command sent successfully, clear the pending command
front_item->pending_command.clear();
ESP_LOGVV(TAG, "Pending command sent: %s", front_item->component->get_variable_name().c_str());
}
#ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP
if (++commands_sent > this->max_commands_per_loop_) {
ESP_LOGV(TAG, "Pending cmds: loop limit reached, deferring");
break;
}
#endif // USE_NEXTION_MAX_COMMANDS_PER_LOOP
const uint32_t now = App.get_loop_component_start_time();
if (!this->command_pacer_.can_send(now)) {
break; // Spacing not elapsed, stop for this loop iteration
}
if (!this->send_command_(item->pending_command)) {
break; // Unexpected send failure, stop
}
item->pending_command.clear();
ESP_LOGVV(TAG, "Pending cmd sent: %s", item->component->get_variable_name().c_str());
}
}
#endif // USE_NEXTION_COMMAND_SPACING
@@ -493,6 +470,10 @@ void Nextion::process_nextion_commands_() {
this->setup_callback_.call();
}
}
#ifdef USE_NEXTION_COMMAND_SPACING
this->command_pacer_.mark_sent(); // Here is where we should mark the command as sent
ESP_LOGN(TAG, "Command spacing: marked command sent");
#endif
break;
case 0x02: // invalid Component ID or name was used
ESP_LOGW(TAG, "Invalid component ID/name");
@@ -1098,18 +1079,10 @@ void Nextion::add_no_result_to_queue_(const std::string &variable_name) {
}
/**
* @brief Send a command and enqueue it for response tracking.
* @brief
*
* Callers are responsible for checking is_sleeping() before calling this
* method. The sleep guard is deliberately absent here because some callers
* (e.g. add_no_result_to_queue_with_ignore_sleep_printf_()) are explicitly
* sleep-safe and must bypass it.
*
* If USE_NEXTION_COMMAND_SPACING is enabled and the pacer is not ready,
* the command is saved in the queue entry for retry rather than dropped.
*
* @param variable_name Name of the variable or component associated with the command.
* @param command The raw command string to send.
* @param variable_name Variable name for the queue
* @param command
*/
void Nextion::add_no_result_to_queue_with_command_(const std::string &variable_name, const std::string &command) {
if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || command.empty())
@@ -1290,22 +1263,9 @@ void Nextion::add_to_get_queue(NextionComponentBase *component) {
std::string command = "get " + component->get_variable_name_to_send();
#ifdef USE_NEXTION_COMMAND_SPACING
// Always enqueue first so the response handler is present when the command
// is eventually sent. Store the command for retry if spacing blocked it;
// process_pending_in_queue_() will transmit it when the pacer allows.
nextion_queue->pending_command = command;
this->nextion_queue_.push_back(nextion_queue);
if (this->send_command_(command)) {
nextion_queue->pending_command.clear();
}
#else // USE_NEXTION_COMMAND_SPACING
if (this->send_command_(command)) {
this->nextion_queue_.push_back(nextion_queue);
} else {
delete nextion_queue; // NOLINT(cppcoreguidelines-owning-memory)
}
#endif // USE_NEXTION_COMMAND_SPACING
}
#ifdef USE_NEXTION_WAVEFORM
@@ -1349,10 +1309,10 @@ void Nextion::check_pending_waveform_() {
char command[24]; // "addt " + uint8 + "," + uint8 + "," + uint8 + null = max 17 chars
buf_append_printf(command, sizeof(command), 0, "addt %u,%u,%zu", component->get_component_id(),
component->get_wave_channel_id(), buffer_to_send);
// If spacing or setup state blocks the send, leave the entry at the front
// of waveform_queue_ for retry on the next loop iteration via
// check_pending_waveform_(). Only pop on a successful send.
this->send_command_(command);
if (!this->send_command_(command)) {
delete nb; // NOLINT(cppcoreguidelines-owning-memory)
this->waveform_queue_.pop();
}
}
#endif // USE_NEXTION_WAVEFORM

View File

@@ -55,20 +55,15 @@ class NextionCommandPacer {
uint8_t get_spacing() const { return spacing_ms_; }
/**
* @brief Check if enough time has passed to send the next command.
* @param now Current timestamp in milliseconds (use App.get_loop_component_start_time()
* for consistency with the rest of the queue timing).
* @return true if the spacing interval has elapsed since the last command was sent.
* @brief Check if enough time has passed to send next command
* @return true if enough time has passed since last command
*/
bool can_send(uint32_t now) const { return (now - last_command_time_) >= spacing_ms_; }
bool can_send() const { return (millis() - last_command_time_) >= spacing_ms_; }
/**
* @brief Record the transmit timestamp for the most recently sent command.
* @param now Current timestamp in milliseconds, as returned by
* App.get_loop_component_start_time(). Must use the same clock
* source as can_send() to avoid unsigned underflow.
* @brief Mark a command as sent, updating the timing
*/
void mark_sent(uint32_t now) { last_command_time_ = now; }
void mark_sent() { last_command_time_ = millis(); }
private:
uint8_t spacing_ms_;

View File

@@ -44,7 +44,7 @@ void PCF85063Component::read_time() {
.year = uint16_t(pcf85063_.reg.year + 10u * pcf85063_.reg.year_10 + 2000),
};
rtc_time.recalc_timestamp_utc(false);
if (!rtc_time.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) {
if (!rtc_time.is_valid()) {
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
return;
}

View File

@@ -44,7 +44,7 @@ void PCF8563Component::read_time() {
.year = uint16_t(pcf8563_.reg.year + 10u * pcf8563_.reg.year_10 + 2000),
};
rtc_time.recalc_timestamp_utc(false);
if (!rtc_time.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) {
if (!rtc_time.is_valid()) {
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
return;
}

View File

@@ -100,7 +100,7 @@ void QMC5883LComponent::update() {
// ROL_PNT in setup and reading 7 bytes starting at the status register.
// If status and all three axes are desired, using ROL_PNT saves you 3 bytes.
// But simply not reading status saves you 4 bytes always and is much simpler.
if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) {
if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG) {
err = this->read_register(QMC5883L_REGISTER_STATUS, &status, 1);
if (err != i2c::ERROR_OK) {
char buf[32];
@@ -165,7 +165,7 @@ void QMC5883LComponent::update() {
temp = int16_t(raw_temp) * 0.01f;
}
ESP_LOGV(TAG, "Got x=%0.02fµT y=%0.02fµT z=%0.02fµT heading=%0.01f° temperature=%0.01f°C status=%u", x, y, z, heading,
ESP_LOGD(TAG, "Got x=%0.02fµT y=%0.02fµT z=%0.02fµT heading=%0.01f° temperature=%0.01f°C status=%u", x, y, z, heading,
temp, status);
if (this->x_sensor_ != nullptr)

View File

@@ -65,8 +65,3 @@ async def to_code(config):
@pins.PIN_SCHEMA_REGISTRY.register("rtl87xx", PIN_SCHEMA)
async def pin_to_code(config):
return await libretiny.gpio.component_pin_to_code(config)
# Called by writer.py; delegates to the shared libretiny implementation.
def copy_files() -> None:
libretiny.copy_files()

View File

@@ -32,101 +32,40 @@ void RuntimeStatsCollector::log_stats_() {
" Period stats (last %" PRIu32 "ms): %zu active components",
this->log_interval_, count);
// Sum component time so we can derive main-loop overhead
// (active loop time minus time attributable to component loop()s).
// Period sum iterates the active-in-period subset; total sum must iterate
// all components since total_active_time_us_ includes iterations where
// currently-idle components previously ran.
uint64_t period_component_sum_us = 0;
if (count == 0) {
return;
}
// Sort by period runtime (descending)
std::sort(sorted, sorted + count, compare_period_time);
// Log top components by period runtime
for (size_t i = 0; i < count; i++) {
period_component_sum_us += sorted[i]->runtime_stats_.period_time_us;
}
uint64_t total_component_sum_us = 0;
for (auto *component : components) {
total_component_sum_us += component->runtime_stats_.total_time_us;
}
if (count > 0) {
// Sort by period runtime (descending)
std::sort(sorted, sorted + count, compare_period_time);
// Log top components by period runtime
for (size_t i = 0; i < count; i++) {
const auto &stats = sorted[i]->runtime_stats_;
ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms",
LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.period_count,
stats.period_count > 0 ? stats.period_time_us / (float) stats.period_count / 1000.0f : 0.0f,
stats.period_max_time_us / 1000.0f, stats.period_time_us / 1000.0f);
}
}
// Main-loop overhead for the period: active wall time minus component time.
// active = sum of per-iteration loop time excluding yield/sleep.
if (this->period_active_count_ > 0) {
uint64_t active = this->period_active_time_us_;
uint64_t overhead = active > period_component_sum_us ? active - period_component_sum_us : 0;
// Use double for µs→ms conversion so multi-day uptimes (where total
// microsecond counters exceed float's ~7-digit mantissa) keep resolution.
ESP_LOGI(TAG,
" main_loop: iters=%" PRIu64 ", active_avg=%.3fms, active_max=%.2fms, active_total=%.1fms, "
"overhead_total=%.1fms",
this->period_active_count_,
static_cast<double>(active) / static_cast<double>(this->period_active_count_) / 1000.0,
static_cast<double>(this->period_active_max_us_) / 1000.0, static_cast<double>(active) / 1000.0,
static_cast<double>(overhead) / 1000.0);
uint64_t before = this->period_before_time_us_;
uint64_t tail = this->period_tail_time_us_;
uint64_t accounted = before + tail;
uint64_t inter = overhead > accounted ? overhead - accounted : 0;
ESP_LOGI(TAG, " main_loop_overhead_section: before=%.1fms, tail=%.1fms, inter_component=%.1fms",
static_cast<double>(before) / 1000.0, static_cast<double>(tail) / 1000.0,
static_cast<double>(inter) / 1000.0);
const auto &stats = sorted[i]->runtime_stats_;
ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms",
LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.period_count,
stats.period_count > 0 ? stats.period_time_us / (float) stats.period_count / 1000.0f : 0.0f,
stats.period_max_time_us / 1000.0f, stats.period_time_us / 1000.0f);
}
// Log total stats since boot (only for active components - idle ones haven't changed)
ESP_LOGI(TAG, " Total stats (since boot): %zu active components", count);
if (count > 0) {
// Re-sort by total runtime for all-time stats
std::sort(sorted, sorted + count, compare_total_time);
// Re-sort by total runtime for all-time stats
std::sort(sorted, sorted + count, compare_total_time);
for (size_t i = 0; i < count; i++) {
const auto &stats = sorted[i]->runtime_stats_;
ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms",
LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.total_count,
stats.total_count > 0 ? stats.total_time_us / (float) stats.total_count / 1000.0f : 0.0f,
stats.total_max_time_us / 1000.0f, stats.total_time_us / 1000.0);
}
}
if (this->total_active_count_ > 0) {
uint64_t active = this->total_active_time_us_;
uint64_t overhead = active > total_component_sum_us ? active - total_component_sum_us : 0;
ESP_LOGI(TAG,
" main_loop: iters=%" PRIu64 ", active_avg=%.3fms, active_max=%.2fms, active_total=%.1fms, "
"overhead_total=%.1fms",
this->total_active_count_,
static_cast<double>(active) / static_cast<double>(this->total_active_count_) / 1000.0,
static_cast<double>(this->total_active_max_us_) / 1000.0, static_cast<double>(active) / 1000.0,
static_cast<double>(overhead) / 1000.0);
uint64_t before = this->total_before_time_us_;
uint64_t tail = this->total_tail_time_us_;
uint64_t accounted = before + tail;
uint64_t inter = overhead > accounted ? overhead - accounted : 0;
ESP_LOGI(TAG, " main_loop_overhead_section: before=%.1fms, tail=%.1fms, inter_component=%.1fms",
static_cast<double>(before) / 1000.0, static_cast<double>(tail) / 1000.0,
static_cast<double>(inter) / 1000.0);
for (size_t i = 0; i < count; i++) {
const auto &stats = sorted[i]->runtime_stats_;
ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms",
LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.total_count,
stats.total_count > 0 ? stats.total_time_us / (float) stats.total_count / 1000.0f : 0.0f,
stats.total_max_time_us / 1000.0f, stats.total_time_us / 1000.0);
}
// Reset period stats
for (auto *component : components) {
component->runtime_stats_.reset_period();
}
this->period_active_count_ = 0;
this->period_active_time_us_ = 0;
this->period_active_max_us_ = 0;
this->period_before_time_us_ = 0;
this->period_tail_time_us_ = 0;
}
bool RuntimeStatsCollector::compare_period_time(Component *a, Component *b) {

View File

@@ -29,31 +29,6 @@ class RuntimeStatsCollector {
// Process any pending stats printing (should be called after component loop)
void process_pending_stats(uint32_t current_time);
// Record the wall time of one main loop iteration excluding the yield/sleep.
// Called once per loop from Application::loop().
// active_us = total time between loop start and just before yield.
// before_us = time spent in before_loop_tasks_ (scheduler + ISR enable_loop).
// tail_us = time spent in after_loop_tasks_ + the trailing record/stats prefix.
// Residual overhead at log time = active Σ(component) before tail,
// which captures per-iteration inter-component bookkeeping (set_current_component,
// WarnIfComponentBlockingGuard construction/destruction, feed_wdt_with_time calls,
// the for-loop itself).
void record_loop_active(uint32_t active_us, uint32_t before_us, uint32_t tail_us) {
this->period_active_count_++;
this->period_active_time_us_ += active_us;
if (active_us > this->period_active_max_us_)
this->period_active_max_us_ = active_us;
this->total_active_count_++;
this->total_active_time_us_ += active_us;
if (active_us > this->total_active_max_us_)
this->total_active_max_us_ = active_us;
this->period_before_time_us_ += before_us;
this->total_before_time_us_ += before_us;
this->period_tail_time_us_ += tail_us;
this->total_tail_time_us_ += tail_us;
}
protected:
void log_stats_();
// Static comparators — member functions have friend access, lambdas do not
@@ -62,22 +37,6 @@ class RuntimeStatsCollector {
uint32_t log_interval_;
uint32_t next_log_time_{0};
// Main loop active-time stats (wall time per iteration, excluding yield/sleep).
// Counters are uint64_t — at sub-millisecond loop times a uint32_t can wrap in
// a few weeks of uptime, which is well within ESPHome device lifetimes.
uint64_t period_active_count_{0};
uint64_t period_active_time_us_{0};
uint32_t period_active_max_us_{0};
uint64_t total_active_count_{0};
uint64_t total_active_time_us_{0};
uint32_t total_active_max_us_{0};
// Split of overhead sections — accumulated per iteration.
uint64_t period_before_time_us_{0};
uint64_t total_before_time_us_{0};
uint64_t period_tail_time_us_{0};
uint64_t total_tail_time_us_{0};
};
} // namespace runtime_stats

View File

@@ -81,7 +81,7 @@ void RX8130Component::read_time() {
.year = static_cast<uint16_t>(bcd2dec(date[6]) + 2000),
};
rtc_time.recalc_timestamp_utc(false);
if (!rtc_time.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) {
if (!rtc_time.is_valid()) {
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
return;
}

View File

@@ -87,10 +87,7 @@ async def to_code(config):
config[CONF_REBOOT_TIMEOUT],
config[CONF_BOOT_IS_GOOD_AFTER],
)
# Wrap in IIFEUnsafeStatement so cpp_main_section emits this
# component's block flat rather than inside an IIFE lambda —
# the `return` must exit setup() itself, not just the lambda.
cg.add(cg.IIFEUnsafeStatement(RawExpression(f"if ({condition}) return")))
cg.add(RawExpression(f"if ({condition}) return"))
CORE.data[CONF_SAFE_MODE] = {}
CORE.data[CONF_SAFE_MODE][KEY_PAST_SAFE_MODE] = True

View File

@@ -84,7 +84,7 @@ def get_firmware(value):
req = requests.get(url, timeout=30)
req.raise_for_status()
except requests.exceptions.RequestException as e:
raise cv.Invalid(f"Could not download firmware file ({url}): {e}") from e
raise cv.Invalid(f"Could not download firmware file ({url}): {e}")
h = hashlib.new("sha256")
h.update(req.content)

View File

@@ -11,10 +11,6 @@
#include "esphome/core/wake.h"
#include "esphome/core/log.h"
#ifdef USE_OTA_PLATFORM_ESPHOME
extern "C" void esphome_wake_ota_component_any_context();
#endif
#ifdef USE_ESP8266
#include <coredecls.h> // For esp_schedule()
#elif defined(USE_RP2040)
@@ -858,10 +854,6 @@ err_t LWIPRawListenImpl::accept_fn_(struct tcp_pcb *newpcb, err_t err) {
tcp_err(newpcb, LWIPRawListenImpl::s_queued_err_fn);
tcp_recv(newpcb, LWIPRawListenImpl::s_queued_recv_fn);
LWIP_LOG("Accepted connection, queue size: %d", this->accepted_socket_count_);
#ifdef USE_OTA_PLATFORM_ESPHOME
// Must run before wake_loop_any_context() so flags are visible when the main task wakes.
esphome_wake_ota_component_any_context();
#endif
// Wake the main loop immediately so it can accept the new connection.
esphome::wake_loop_any_context();
return ERR_OK;

View File

@@ -173,7 +173,7 @@ def _read_audio_file_and_type(file_config):
raise cv.Invalid(
f"Unable to determine audio file type of '{path}'. "
f"Try re-encoding the file into a supported format. Details: {e}"
) from e
)
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["NONE"]
if file_type in ("wav"):

View File

@@ -45,8 +45,8 @@ MODELS = {
presets={
CONF_HEIGHT: 240,
CONF_WIDTH: 135,
CONF_OFFSET_HEIGHT: 40,
CONF_OFFSET_WIDTH: 52,
CONF_OFFSET_HEIGHT: 52,
CONF_OFFSET_WIDTH: 40,
CONF_CS_PIN: "GPIO5",
CONF_DC_PIN: "GPIO16",
CONF_RESET_PIN: "GPIO23",
@@ -68,8 +68,8 @@ MODELS = {
presets={
CONF_HEIGHT: 280,
CONF_WIDTH: 240,
CONF_OFFSET_HEIGHT: 20,
CONF_OFFSET_WIDTH: 0,
CONF_OFFSET_HEIGHT: 0,
CONF_OFFSET_WIDTH: 20,
}
),
"ADAFRUIT_S2_TFT_FEATHER_240X135": model_spec(
@@ -77,8 +77,8 @@ MODELS = {
presets={
CONF_HEIGHT: 240,
CONF_WIDTH: 135,
CONF_OFFSET_HEIGHT: 40,
CONF_OFFSET_WIDTH: 52,
CONF_OFFSET_HEIGHT: 52,
CONF_OFFSET_WIDTH: 40,
CONF_CS_PIN: "GPIO7",
CONF_DC_PIN: "GPIO39",
CONF_RESET_PIN: "GPIO40",
@@ -89,8 +89,8 @@ MODELS = {
presets={
CONF_HEIGHT: 320,
CONF_WIDTH: 170,
CONF_OFFSET_HEIGHT: 0,
CONF_OFFSET_WIDTH: 35,
CONF_OFFSET_HEIGHT: 35,
CONF_OFFSET_WIDTH: 0,
CONF_ROTATION: 270,
CONF_CS_PIN: "GPIO10",
CONF_DC_PIN: "GPIO13",
@@ -102,8 +102,8 @@ MODELS = {
presets={
CONF_HEIGHT: 320,
CONF_WIDTH: 172,
CONF_OFFSET_HEIGHT: 0,
CONF_OFFSET_WIDTH: 34,
CONF_OFFSET_HEIGHT: 34,
CONF_OFFSET_WIDTH: 0,
CONF_ROTATION: 90,
CONF_CS_PIN: "GPIO21",
CONF_DC_PIN: "GPIO22",

View File

@@ -7,11 +7,6 @@ namespace status_led {
static const char *const TAG = "status_led";
static constexpr uint32_t ERROR_PERIOD_MS = 250;
static constexpr uint32_t ERROR_ON_MS = 150;
static constexpr uint32_t WARNING_PERIOD_MS = 1500;
static constexpr uint32_t WARNING_ON_MS = 250;
StatusLED *global_status_led = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
StatusLED::StatusLED(GPIOPin *pin) : pin_(pin) { global_status_led = this; }
@@ -24,18 +19,12 @@ void StatusLED::dump_config() {
LOG_PIN(" Pin: ", this->pin_);
}
void StatusLED::loop() {
const uint32_t app_state = App.get_app_state();
// Use millis() rather than App.get_loop_component_start_time() because this loop is also
// dispatched from Application::feed_wdt() during long blocking operations, where the cached
// per-component timestamp doesn't advance and would freeze the blink pattern.
const uint32_t now = millis();
if ((app_state & STATUS_LED_ERROR) != 0u) {
this->pin_->digital_write(now % ERROR_PERIOD_MS < ERROR_ON_MS);
} else if ((app_state & STATUS_LED_WARNING) != 0u) {
this->pin_->digital_write(now % WARNING_PERIOD_MS < WARNING_ON_MS);
if ((App.get_app_state() & STATUS_LED_ERROR) != 0u) {
this->pin_->digital_write(millis() % 250u < 150u);
} else if ((App.get_app_state() & STATUS_LED_WARNING) != 0u) {
this->pin_->digital_write(millis() % 1500u < 250u);
} else {
this->pin_->digital_write(false);
this->disable_loop();
}
}
float StatusLED::get_setup_priority() const { return setup_priority::HARDWARE; }

View File

@@ -35,9 +35,8 @@ def validate_acceleration(value):
try:
value = float(value)
except ValueError:
raise cv.Invalid(
f"Expected acceleration as floating point number, got {value}"
) from None
# pylint: disable=raise-missing-from
raise cv.Invalid(f"Expected acceleration as floating point number, got {value}")
if value <= 0:
raise cv.Invalid("Acceleration must be larger than 0 steps/s^2!")
@@ -56,9 +55,8 @@ def validate_speed(value):
try:
value = float(value)
except ValueError:
raise cv.Invalid(
f"Expected speed as floating point number, got {value}"
) from None
# pylint: disable=raise-missing-from
raise cv.Invalid(f"Expected speed as floating point number, got {value}")
if value <= 0:
raise cv.Invalid("Speed must be larger than 0 steps/s!")

View File

@@ -188,7 +188,7 @@ def _expand_substitutions(
f"\nRelevant context:\n{err.context_trace_str()}"
f"\nSee {'->'.join(str(x) for x in path)}",
path,
) from err
)
else:
if isinstance(orig_value, ESPHomeDataBase):
value = _restore_data_base(value, orig_value)

View File

@@ -200,11 +200,11 @@ CONFIG_SCHEMA = (
cv.hex_int, cv.Range(min=0, max=0xFFFF)
),
cv.Optional(CONF_DEVIATION, default="5kHz"): cv.All(
cv.frequency, cv.int_range(min=0, max=100000)
cv.frequency, cv.float_range(min=0, max=100000)
),
cv.Required(CONF_DIO1_PIN): pins.gpio_input_pin_schema,
cv.Required(CONF_FREQUENCY): cv.All(
cv.frequency, cv.int_range(min=int(137e6), max=int(1020e6))
cv.frequency, cv.float_range(min=137.0e6, max=1020.0e6)
),
cv.Required(CONF_HW_VERSION): cv.one_of(
"sx1261", "sx1262", "sx1268", "llcc68", lower=True

View File

@@ -197,11 +197,11 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_CODING_RATE, default="CR_4_5"): cv.enum(CODING_RATE),
cv.Optional(CONF_CRC_ENABLE, default=False): cv.boolean,
cv.Optional(CONF_DEVIATION, default="5kHz"): cv.All(
cv.frequency, cv.int_range(min=0, max=100000)
cv.frequency, cv.float_range(min=0, max=100000)
),
cv.Optional(CONF_DIO0_PIN): pins.internal_gpio_input_pin_schema,
cv.Required(CONF_FREQUENCY): cv.All(
cv.frequency, cv.int_range(min=int(137e6), max=int(1020e6))
cv.frequency, cv.float_range(min=137.0e6, max=1020.0e6)
),
cv.Required(CONF_MODULATION): cv.enum(MOD),
cv.Optional(CONF_ON_PACKET): automation.validate_automation(single=True),

View File

@@ -315,7 +315,7 @@ void TCS34725Component::set_integration_time(TCS34725IntegrationTime integration
my_integration_time_regval = integration_time;
this->integration_time_auto_ = false;
}
this->integration_time_ = (256.f - (float) my_integration_time_regval) * 2.4f;
this->integration_time_ = (256.f - my_integration_time_regval) * 2.4f;
ESP_LOGI(TAG, "TCS34725I Integration time set to: %.1fms", this->integration_time_);
}
void TCS34725Component::set_gain(TCS34725Gain gain) {

View File

@@ -109,7 +109,8 @@ def _parse_cron_int(value, special_mapping, message):
try:
return int(value)
except ValueError:
raise cv.Invalid(message.format(value)) from None
# pylint: disable=raise-missing-from
raise cv.Invalid(message.format(value))
def _parse_cron_part(part, min_value, max_value, special_mapping):
@@ -133,9 +134,10 @@ def _parse_cron_part(part, min_value, max_value, special_mapping):
try:
repeat_n = int(repeat)
except ValueError:
# pylint: disable=raise-missing-from
raise cv.Invalid(
f"Repeat for '/' time expression must be an integer, got {repeat}"
) from None
)
return set(range(offset_n, max_value + 1, repeat_n))
if "-" in part:
data = part.split("-")

View File

@@ -347,13 +347,6 @@ uint8_t TM1637Display::print(uint8_t start_pos, const char *str) {
}
return pos - start_pos;
}
void TM1637Display::set_brightness(float brightness) {
auto intensity = clamp(brightness, 0.f, 1.f) * 7;
this->set_on(intensity > 0);
this->set_intensity(intensity);
}
uint8_t TM1637Display::print(const char *str) { return this->print(0, str); }
void TM1637Display::set_buffer(const uint8_t *data, uint8_t length) {

View File

@@ -50,9 +50,6 @@ class TM1637Display : public PollingComponent {
/// Set raw buffer bytes from data array up to length bytes.
void set_buffer(const uint8_t *data, uint8_t length);
/// Set the display brightness. Accepts a value between 0.0 and 1.0; 0 will turn off
/// the display and 1.0 will set it to the maximum brightness.
void set_brightness(float brightness);
void set_intensity(uint8_t intensity) { this->intensity_ = intensity; }
void set_inverted(bool inverted) { this->inverted_ = inverted; }
void set_length(uint8_t length) { this->length_ = length; }

View File

@@ -75,9 +75,6 @@ class ListEntitiesIterator final : public ComponentIterator {
#ifdef USE_VALVE
bool on_valve(valve::Valve *obj) override;
#endif
#ifdef USE_MEDIA_PLAYER
bool on_media_player(media_player::MediaPlayer *obj) override { return true; }
#endif
#ifdef USE_ALARM_CONTROL_PANEL
bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *obj) override;
#endif

View File

@@ -114,25 +114,7 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf
uint8_t *data, size_t len, bool final) {
ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_OK;
// First byte of a new upload: index==0 with actual data. (web_server_idf
// fires a separate start-marker call with data==nullptr/len==0 before the
// first real chunk; gate on len>0 so we only trigger once per upload.)
if (index == 0 && len > 0) {
// If a previous upload was interrupted (e.g. client closed the tab, TCP
// reset) the backend from that session may still be open. Tear it down
// so flash state doesn't get concatenated with the new image (which can
// produce a technically-valid-sized but corrupted firmware that bricks
// the device once it reboots).
if (this->ota_backend_) {
ESP_LOGW(TAG, "New OTA upload received while previous session was still open; aborting previous session");
this->ota_backend_->abort();
#ifdef USE_OTA_STATE_LISTENER
// Notify listeners that the previous session was aborted before the new one starts.
this->parent_->notify_state_deferred_(ota::OTA_ABORT, 0.0f, 0);
#endif
this->ota_backend_.reset();
}
if (index == 0 && !this->ota_backend_) {
// Initialize OTA on first call
this->ota_init_(filename.c_str());

View File

@@ -308,7 +308,6 @@ bool CompactString::operator==(const StringRef &other) const {
/// │ - Roaming fail (RECONNECTING on other AP): counter preserved │
/// └──────────────────────────────────────────────────────────────────────┘
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_INFO
// Use if-chain instead of switch to avoid jump table in RODATA (wastes RAM on ESP8266)
static const LogString *retry_phase_to_log_string(WiFiRetryPhase phase) {
if (phase == WiFiRetryPhase::INITIAL_CONNECT)
@@ -327,7 +326,6 @@ static const LogString *retry_phase_to_log_string(WiFiRetryPhase phase) {
return LOG_STR("RESTARTING");
return LOG_STR("UNKNOWN");
}
#endif // ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_INFO
bool WiFiComponent::went_through_explicit_hidden_phase_() const {
// If first configured network is marked hidden, we went through EXPLICIT_HIDDEN phase

View File

@@ -67,7 +67,7 @@ def _validate_load_certificate(value):
contents = read_relative_config_path(value)
return wrapped_load_pem_x509_certificate(contents)
except ValueError as err:
raise cv.Invalid(f"Invalid certificate: {err}") from err
raise cv.Invalid(f"Invalid certificate: {err}")
def validate_certificate(value):
@@ -86,9 +86,9 @@ def _validate_load_private_key(key, cert_pw):
except ValueError as e:
raise cv.Invalid(
f"There was an error with the EAP 'password:' provided for 'key' {e}"
) from e
)
except TypeError as e:
raise cv.Invalid(f"There was an error with the EAP 'key:' provided: {e}") from e
raise cv.Invalid(f"There was an error with the EAP 'key:' provided: {e}")
def _check_private_key_cert_match(key, cert):

View File

@@ -53,7 +53,7 @@ def _cidr_network(value):
try:
ipaddress.ip_network(value, strict=False)
except ValueError as err:
raise cv.Invalid(f"Invalid network in CIDR notation: {err}") from err
raise cv.Invalid(f"Invalid network in CIDR notation: {err}")
return value
@@ -137,7 +137,7 @@ async def to_code(config):
# the '+1' modifier is relative to the device's own address that will
# be automatically added to the provided list.
cg.add_build_flag(f"-DCONFIG_WIREGUARD_MAX_SRC_IPS={len(allowed_ips) + 1}")
cg.add_library("droscy/esp_wireguard", "0.4.5")
cg.add_library("droscy/esp_wireguard", "0.4.4")
await cg.register_component(var, config)

View File

@@ -99,6 +99,7 @@ int main() {
setup();
while (true) {
loop();
esphome::yield();
}
return 0;
}

View File

@@ -1,35 +1,28 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components.zephyr import zephyr_add_prj_conf
import esphome.config_validation as cv
from esphome.const import CONF_ID, Framework
from esphome.core import CORE
from esphome.const import CONF_ESPHOME, CONF_ID, CONF_NAME, Framework
import esphome.final_validate as fv
zephyr_ble_server_ns = cg.esphome_ns.namespace("zephyr_ble_server")
BLEServer = zephyr_ble_server_ns.class_("BLEServer", cg.Component)
CONF_ON_NUMERIC_COMPARISON_REQUEST = "on_numeric_comparison_request"
CONF_ACCEPT = "accept"
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(BLEServer),
cv.Optional(
CONF_ON_NUMERIC_COMPARISON_REQUEST
): automation.validate_automation({}),
}
).extend(cv.COMPONENT_SCHEMA),
cv.only_with_framework(Framework.ZEPHYR),
)
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_NUMERIC_COMPARISON_REQUEST,
"add_passkey_callback",
[(cg.uint32, "passkey")],
),
)
def _final_validate(_):
full_config = fv.full_config.get()
zephyr_add_prj_conf("BT_DEVICE_NAME", full_config[CONF_ESPHOME][CONF_NAME])
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config):
@@ -37,39 +30,5 @@ async def to_code(config):
zephyr_add_prj_conf("BT", True)
zephyr_add_prj_conf("BT_PERIPHERAL", True)
zephyr_add_prj_conf("BT_RX_STACK_SIZE", 1536)
zephyr_add_prj_conf("BT_DEVICE_NAME", CORE.name)
# zephyr_add_prj_conf("BT_LL_SW_SPLIT", True)
await cg.register_component(var, config)
if config.get(CONF_ON_NUMERIC_COMPARISON_REQUEST):
zephyr_add_prj_conf("BT_SMP", True)
zephyr_add_prj_conf("BT_SETTINGS", True)
zephyr_add_prj_conf("BT_SMP_SC_ONLY", True)
zephyr_add_prj_conf("BT_KEYS_OVERWRITE_OLDEST", True)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
BLENumericComparisonReplyAction = zephyr_ble_server_ns.class_(
"BLENumericComparisonReplyAction", automation.Action
)
BLE_NUMERIC_COMPARISON_REPLY_ACTION_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_ID): cv.use_id(BLEServer),
cv.Required(CONF_ACCEPT): cv.templatable(cv.boolean),
}
)
@automation.register_action(
"ble_server.numeric_comparison_reply",
BLENumericComparisonReplyAction,
BLE_NUMERIC_COMPARISON_REPLY_ACTION_SCHEMA,
synchronous=True,
)
async def numeric_comparison_reply_to_code(config, action_id, template_arg, args):
parent = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, parent)
templ = await cg.templatable(config[CONF_ACCEPT], args, cg.bool_)
cg.add(var.set_accept(templ))
return var

View File

@@ -3,34 +3,32 @@
#include "esphome/core/defines.h"
#include "esphome/core/log.h"
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/settings/settings.h>
#include <zephyr/bluetooth/conn.h>
namespace esphome::zephyr_ble_server {
static const char *const TAG = "zephyr_ble_server";
static k_work advertise_work; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
BLEServer *global_ble_server; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static struct k_work advertise_work; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
#define DEVICE_NAME CONFIG_BT_DEVICE_NAME
#define DEVICE_NAME_LEN (sizeof(DEVICE_NAME) - 1)
static const bt_data AD[] = {
static const struct bt_data AD[] = {
BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
BT_DATA(BT_DATA_NAME_COMPLETE, DEVICE_NAME, DEVICE_NAME_LEN),
};
static const bt_data SD[] = {
static const struct bt_data SD[] = {
#ifdef USE_OTA
BT_DATA_BYTES(BT_DATA_UUID128_ALL, 0x84, 0xaa, 0x60, 0x74, 0x52, 0x8a, 0x8b, 0x86, 0xd3, 0x4c, 0xb7, 0x1d, 0x1d,
0xdc, 0x53, 0x8d),
#endif
};
const bt_le_adv_param *const ADV_PARAM = BT_LE_ADV_CONN;
const struct bt_le_adv_param *const ADV_PARAM = BT_LE_ADV_CONN;
static void advertise(k_work *work) {
static void advertise(struct k_work *work) {
int rc = bt_le_adv_stop();
if (rc) {
ESP_LOGE(TAG, "Advertising failed to stop (rc %d)", rc);
@@ -44,276 +42,57 @@ static void advertise(k_work *work) {
ESP_LOGI(TAG, "Advertising successfully started");
}
void BLEServer::connected(bt_conn *conn, uint8_t err) {
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
static void connected(struct bt_conn *conn, uint8_t err) {
if (err) {
ESP_LOGE(TAG, "Failed to connect to %s (%u)", addr, err);
return;
ESP_LOGE(TAG, "Connection failed (err 0x%02x)", err);
} else {
ESP_LOGI(TAG, "Connected");
}
ESP_LOGI(TAG, "Connected %s", addr);
#ifdef CONFIG_BT_SMP
if (bt_conn_set_security(conn, BT_SECURITY_L4)) {
ESP_LOGE(TAG, "Failed to set security");
}
#endif
conn = bt_conn_ref(conn);
global_ble_server->defer([conn]() { global_ble_server->conn_ = conn; });
}
void BLEServer::disconnected(bt_conn *conn, uint8_t reason) {
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
ESP_LOGI(TAG, "Disconnected from %s (reason 0x%02x)", addr, reason);
global_ble_server->defer([]() {
if (global_ble_server->conn_) {
bt_conn_unref(global_ble_server->conn_);
global_ble_server->conn_ = nullptr;
}
});
static void disconnected(struct bt_conn *conn, uint8_t reason) {
ESP_LOGI(TAG, "Disconnected (reason 0x%02x)", reason);
k_work_submit(&advertise_work);
}
#ifdef CONFIG_BT_SMP
static void identity_resolved(bt_conn *conn, const bt_addr_le_t *rpa, const bt_addr_le_t *identity) {
char addr_identity[BT_ADDR_LE_STR_LEN];
char addr_rpa[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(identity, addr_identity, sizeof(addr_identity));
bt_addr_le_to_str(rpa, addr_rpa, sizeof(addr_rpa));
ESP_LOGD(TAG, "Identity resolved %s -> %s", addr_rpa, addr_identity);
}
static void security_changed(bt_conn *conn, bt_security_t level, bt_security_err err) {
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
if (!err) {
ESP_LOGD(TAG, "Security changed: %s level %u", addr, level);
static void bt_ready(int err) {
if (err != 0) {
ESP_LOGE(TAG, "Bluetooth failed to initialise: %d", err);
} else {
ESP_LOGE(TAG, "Security failed: %s level %u err %d", addr, level, err);
k_work_submit(&advertise_work);
}
}
static void pairing_complete(bt_conn *conn, bool bonded) {
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
ESP_LOGD(TAG, "Pairing completed: %s, bonded: %d", addr, bonded);
}
static void pairing_failed(bt_conn *conn, bt_security_err reason) {
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
ESP_LOGE(TAG, "Pairing failed conn: %s, reason %d", addr, reason);
bt_conn_disconnect(conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN);
}
static void bond_deleted(uint8_t id, const bt_addr_le_t *peer) {
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(peer, addr, sizeof(addr));
ESP_LOGD(TAG, "Bond deleted for %s, id %u", addr, id);
}
static void auth_passkey_display(bt_conn *conn, unsigned int passkey) {
char addr[BT_ADDR_LE_STR_LEN];
char passkey_str[7];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
snprintk(passkey_str, 7, "%06u", passkey);
ESP_LOGI(TAG, "Passkey for %s: %s", addr, passkey_str);
}
static void conn_addr_str(bt_conn *conn, char *addr, size_t len) {
struct bt_conn_info info;
if (bt_conn_get_info(conn, &info) < 0) {
addr[0] = '\0';
return;
}
switch (info.type) {
case BT_CONN_TYPE_LE:
bt_addr_le_to_str(info.le.dst, addr, len);
break;
default:
ESP_LOGE(TAG, "Not implemented");
addr[0] = '\0';
break;
}
}
static void auth_cancel(bt_conn *conn) {
char addr[BT_ADDR_LE_STR_LEN];
conn_addr_str(conn, addr, sizeof(addr));
ESP_LOGI(TAG, "Pairing cancelled: %s", addr);
}
void BLEServer::auth_passkey_confirm(bt_conn *conn, unsigned int passkey) {
char addr[BT_ADDR_LE_STR_LEN];
char passkey_str[7];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
snprintk(passkey_str, 7, "%06u", passkey);
ESP_LOGI(TAG, "Confirm passkey for %s: %s", addr, passkey_str);
global_ble_server->defer([passkey]() { global_ble_server->passkey_cb_(passkey); });
}
static void auth_pairing_confirm(bt_conn *conn) {
/* Automatically confirm pairing request from the device side. */
auto err = bt_conn_auth_pairing_confirm(conn);
if (err) {
ESP_LOGE(TAG, "Can't confirm pairing (err: %d)", err);
return;
}
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
ESP_LOGI(TAG, "Pairing confirmed: %s", addr);
}
#endif
BT_CONN_CB_DEFINE(conn_callbacks) = {
.connected = connected,
.disconnected = disconnected,
};
void BLEServer::setup() {
global_ble_server = this;
int err = 0;
k_work_init(&advertise_work, advertise);
resume_();
}
static bt_conn_cb conn_callbacks = {
.connected = connected,
.disconnected = disconnected,
#ifdef CONFIG_BT_SMP
.identity_resolved = identity_resolved,
.security_changed = security_changed,
#endif
};
void BLEServer::loop() {
if (this->suspended_) {
resume_();
this->suspended_ = false;
}
}
bt_conn_cb_register(&conn_callbacks);
#ifdef CONFIG_BT_SMP
static struct bt_conn_auth_info_cb conn_auth_info_callbacks = {
.pairing_complete = pairing_complete, .pairing_failed = pairing_failed, .bond_deleted = bond_deleted};
err = bt_conn_auth_info_cb_register(&conn_auth_info_callbacks);
if (err) {
ESP_LOGE(TAG, "Failed to register authorization info callbacks.");
}
static struct bt_conn_auth_cb auth_cb = {
.passkey_display = auth_passkey_display,
.passkey_confirm = auth_passkey_confirm,
.cancel = auth_cancel,
.pairing_confirm = auth_pairing_confirm,
};
err = bt_conn_auth_cb_register(&auth_cb);
if (err) {
ESP_LOGE(TAG, "Failed to set auth handlers (%d)", err);
}
#endif
// callback cannot be used to start scanning due to race conditions with BT_SETTINGS
err = bt_enable(nullptr);
if (err) {
ESP_LOGE(TAG, "Bluetooth enable failed: %d", err);
void BLEServer::resume_() {
int rc = bt_enable(bt_ready);
if (rc != 0) {
ESP_LOGE(TAG, "Bluetooth enable failed: %d", rc);
return;
}
#ifdef CONFIG_BT_SETTINGS
err = settings_load();
if (err) {
ESP_LOGE(TAG, "Cannot load settings, err: %d", err);
}
#endif
k_work_submit(&advertise_work);
}
#ifdef ESPHOME_LOG_HAS_DEBUG
static const char *role_str(uint8_t role) {
switch (role) {
case BT_CONN_ROLE_CENTRAL:
return "Central";
case BT_CONN_ROLE_PERIPHERAL:
return "Peripheral";
}
return "Unknown";
}
static void connection_info(bt_conn *conn, void *user_data) {
char addr[BT_ADDR_LE_STR_LEN];
struct bt_conn_info info;
if (bt_conn_get_info(conn, &info) < 0) {
ESP_LOGE(TAG, "Unable to get info: conn %p", conn);
return;
}
switch (info.type) {
case BT_CONN_TYPE_LE:
bt_addr_le_to_str(info.le.dst, addr, sizeof(addr));
ESP_LOGD(TAG, " %u [LE][%s] %s: Interval %u latency %u timeout %u security L%u", info.id, role_str(info.role),
addr, info.le.interval, info.le.latency, info.le.timeout, info.security.level);
break;
default:
ESP_LOGE(TAG, "Not implemented");
break;
}
}
#ifdef CONFIG_BT_BONDABLE
static void bond_info(const struct bt_bond_info *info, void *user_data) {
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(&info->addr, addr, sizeof(addr));
ESP_LOGD(TAG, " Bond remote identity: %s", addr);
}
#endif
#endif
void BLEServer::dump_config() {
ESP_LOGCONFIG(TAG,
"ble server:\n"
" connected: %s\n"
" name: %s\n"
" appearance: %u\n"
" ready: %s\n"
#ifdef CONFIG_BT_SMP
" security manager: YES",
#else
" security manager: NO",
#endif
YESNO(this->conn_), bt_get_name(), bt_get_appearance(), YESNO(bt_is_ready()));
#ifdef ESPHOME_LOG_HAS_DEBUG
bt_conn_foreach(BT_CONN_TYPE_ALL, connection_info, nullptr);
#ifdef CONFIG_BT_BONDABLE
bt_foreach_bond(BT_ID_DEFAULT, bond_info, nullptr);
#endif
#endif
}
void BLEServer::numeric_comparison_reply(bool accept) {
if (this->conn_ == nullptr) {
ESP_LOGE(TAG, "Not connected");
return;
}
ESP_LOGD(TAG, "Numeric comparison %s", accept ? "accepted" : "rejected");
if (accept) {
bt_conn_auth_passkey_confirm(this->conn_);
} else {
bt_conn_auth_cancel(this->conn_);
}
void BLEServer::on_shutdown() {
struct k_work_sync sync;
k_work_cancel_sync(&advertise_work, &sync);
bt_disable();
this->suspended_ = true;
}
} // namespace esphome::zephyr_ble_server

View File

@@ -1,36 +1,18 @@
#pragma once
#ifdef USE_ZEPHYR
#include "esphome/core/component.h"
#include <zephyr/bluetooth/conn.h>
#include "esphome/core/automation.h"
namespace esphome::zephyr_ble_server {
class BLEServer : public Component {
public:
void setup() override;
void dump_config() override;
template<typename F> void add_passkey_callback(F &&callback) { this->passkey_cb_.add(std::forward<F>(callback)); }
void numeric_comparison_reply(bool accept);
void loop() override;
void on_shutdown() override;
protected:
static void connected(bt_conn *conn, uint8_t err);
static void disconnected(bt_conn *conn, uint8_t reason);
static void auth_passkey_confirm(bt_conn *conn, unsigned int passkey);
bt_conn *conn_{};
CallbackManager<void(uint32_t)> passkey_cb_;
};
template<typename... Ts> class BLENumericComparisonReplyAction : public Action<Ts...> {
public:
explicit BLENumericComparisonReplyAction(BLEServer *parent) : parent_(parent) {}
TEMPLATABLE_VALUE(bool, accept)
void play(const Ts &...x) override { this->parent_->numeric_comparison_reply(this->accept_.value(x...)); }
protected:
BLEServer *parent_;
void resume_();
bool suspended_ = false;
};
} // namespace esphome::zephyr_ble_server

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