1
0
mirror of https://github.com/esphome/esphome.git synced 2026-02-10 09:42:01 +00:00

Compare commits

..

3 Commits

Author SHA1 Message Date
J. Nick Koston
06c5b06f74 [esp32_ble] Work around gatt_main.c compile bug and add IDF to Arduino platformio.ini
Always enable CONFIG_BT_GATTS_ENABLE to work around an ESP-IDF 5.5.2.260206
bug in gatt_main.c where GATT_TRACE_DEBUG references 'msg_len' outside the
GATTS_INCLUDED/GATTC_INCLUDED preprocessor guard, causing a compile error
when both are disabled.

Also add framework-espidf to the Arduino platform_packages in platformio.ini
since Arduino builds use ESP-IDF as a component (framework = arduino, espidf).
2026-02-09 08:52:42 -06:00
J. Nick Koston
c4a5df08c6 [esp32] Fix P4 linker mismatch and pass release to Arduino IDF path
The ESP-IDF 5.5.2.260206 release changed the default of
ESP32P4_SELECTS_REV_LESS_V3 from y to n. PlatformIO always uses
sections.ld.in (for rev <3) rather than sections.rev3.ld.in
(for rev >=3), causing a linker script mismatch with the generated
memory.ld. Restore the previous default until PlatformIO handles
this properly.

Also centralize the ESP-IDF release lookup into
_format_framework_espidf_version so both the IDF and Arduino
code paths use the same release.
2026-02-09 08:05:21 -06:00
J. Nick Koston
5e50716deb [esp32] Bump ESP-IDF 5.5.2 to release 260206
The base ESP-IDF 5.5.2 release contains a broken BT controller
binary (libbtdm_app.a) that causes BLE scanning to silently die
after hours/days with a HCI command timeout (opcode 0x200c).

pioarduino's v5.5.2.260206 release includes the upstream fix
from espressif/esp32-bt-lib commit 06dc4667 which fixes the
BLE enable scan timeout and crash in btdm_controller_task.

Fixes https://github.com/esphome/esphome/issues/13560
2026-02-09 07:10:26 -06:00
221 changed files with 3023 additions and 7535 deletions

View File

@@ -1 +1 @@
0f2b9a65dce7c59289d3aeb40936360a62a7be937b585147b45bb1509eaafb36
7b63ea33897064554efa88382ff41c34a55ea58e1161667093b97d8427f7f957

View File

@@ -43,7 +43,6 @@ _READELF_SECTION_PATTERN = re.compile(
# Component category prefixes
_COMPONENT_PREFIX_ESPHOME = "[esphome]"
_COMPONENT_PREFIX_EXTERNAL = "[external]"
_COMPONENT_PREFIX_LIB = "[lib]"
_COMPONENT_CORE = f"{_COMPONENT_PREFIX_ESPHOME}core"
_COMPONENT_API = f"{_COMPONENT_PREFIX_ESPHOME}api"
@@ -57,16 +56,6 @@ SymbolInfoType = tuple[str, int, str]
# RAM sections - symbols in these sections consume RAM
RAM_SECTIONS = frozenset([".data", ".bss"])
# nm symbol types for global/weak defined symbols (used for library symbol mapping)
# Only global (uppercase) and weak symbols are safe to use - local symbols (lowercase)
# can have name collisions across compilation units
_NM_DEFINED_GLOBAL_TYPES = frozenset({"T", "D", "B", "R", "W", "V"})
# Pattern matching compiler-generated local names that can collide across compilation
# units (e.g., packet$19, buf$20, flag$5261). These are unsafe for name-based lookup.
# Does NOT match mangled C++ names with optimization suffixes (e.g., func$isra$0).
_COMPILER_LOCAL_PATTERN = re.compile(r"^[a-zA-Z_]\w*\$\d+$")
@dataclass
class MemorySection:
@@ -190,19 +179,11 @@ class MemoryAnalyzer:
self._sdk_symbols: list[SDKSymbol] = []
# CSWTCH symbols: list of (name, size, source_file, component)
self._cswtch_symbols: list[tuple[str, int, str, str]] = []
# Library symbol mapping: symbol_name -> library_name
self._lib_symbol_map: dict[str, str] = {}
# Library dir to name mapping: "lib641" -> "espsoftwareserial",
# "espressif__mdns" -> "mdns"
self._lib_hash_to_name: dict[str, str] = {}
# Heuristic category to library redirect: "mdns_lib" -> "[lib]mdns"
self._heuristic_to_lib: dict[str, str] = {}
def analyze(self) -> dict[str, ComponentMemory]:
"""Analyze the ELF file and return component memory usage."""
self._parse_sections()
self._parse_symbols()
self._scan_libraries()
self._categorize_symbols()
self._analyze_cswtch_symbols()
self._analyze_sdk_libraries()
@@ -347,19 +328,15 @@ class MemoryAnalyzer:
# If no component match found, it's core
return _COMPONENT_CORE
# Check library symbol map (more accurate than heuristic patterns)
if lib_name := self._lib_symbol_map.get(symbol_name):
return f"{_COMPONENT_PREFIX_LIB}{lib_name}"
# Check against symbol patterns
for component, patterns in SYMBOL_PATTERNS.items():
if any(pattern in symbol_name for pattern in patterns):
return self._heuristic_to_lib.get(component, component)
return component
# Check against demangled patterns
for component, patterns in DEMANGLED_PATTERNS.items():
if any(pattern in demangled for pattern in patterns):
return self._heuristic_to_lib.get(component, component)
return component
# Special cases that need more complex logic
@@ -407,327 +384,6 @@ class MemoryAnalyzer:
return "Other Core"
def _discover_pio_libraries(
self,
libraries: dict[str, list[Path]],
hash_to_name: dict[str, str],
) -> None:
"""Discover PlatformIO third-party libraries from the build directory.
Scans ``lib<hex>/`` directories under ``.pioenvs/<env>/`` to find
library names and their ``.a`` archive or ``.o`` file paths.
Args:
libraries: Dict to populate with library name -> file path list mappings.
Prefers ``.a`` archives when available, falls back to ``.o`` files
(e.g., pioarduino ESP32 Arduino builds only produce ``.o`` files).
hash_to_name: Dict to populate with dir name -> library name mappings
for CSWTCH attribution (e.g., ``lib641`` -> ``espsoftwareserial``).
"""
build_dir = self.elf_path.parent
for entry in build_dir.iterdir():
if not entry.is_dir() or not entry.name.startswith("lib"):
continue
# Validate that the suffix after "lib" is a hex hash
hex_part = entry.name[3:]
if not hex_part:
continue
try:
int(hex_part, 16)
except ValueError:
continue
# Each lib<hex>/ directory contains a subdirectory named after the library
for lib_subdir in entry.iterdir():
if not lib_subdir.is_dir():
continue
lib_name = lib_subdir.name.lower()
# Prefer .a archive (lib<LibraryName>.a), fall back to .o files
# e.g., lib72a/ESPAsyncTCP/... has lib72a/libESPAsyncTCP.a
archive = entry / f"lib{lib_subdir.name}.a"
if archive.exists():
file_paths = [archive]
elif archives := list(entry.glob("*.a")):
# Case-insensitive fallback
file_paths = [archives[0]]
else:
# No .a archive (e.g., pioarduino CMake builds) - use .o files
file_paths = sorted(lib_subdir.rglob("*.o"))
if file_paths:
libraries[lib_name] = file_paths
hash_to_name[entry.name] = lib_name
_LOGGER.debug(
"Discovered PlatformIO library: %s -> %s",
lib_subdir.name,
file_paths[0],
)
def _discover_idf_managed_components(
self,
libraries: dict[str, list[Path]],
hash_to_name: dict[str, str],
) -> None:
"""Discover ESP-IDF managed component libraries from the build directory.
ESP-IDF managed components (from the IDF component registry) use a
``<vendor>__<name>`` naming convention. Source files live under
``managed_components/<vendor>__<name>/`` and the compiled archives are at
``esp-idf/<vendor>__<name>/lib<vendor>__<name>.a``.
Args:
libraries: Dict to populate with library name -> file path list mappings.
hash_to_name: Dict to populate with dir name -> library name mappings
for CSWTCH attribution (e.g., ``espressif__mdns`` -> ``mdns``).
"""
build_dir = self.elf_path.parent
managed_dir = build_dir / "managed_components"
if not managed_dir.is_dir():
return
espidf_dir = build_dir / "esp-idf"
for entry in managed_dir.iterdir():
if not entry.is_dir() or "__" not in entry.name:
continue
# Extract the short name: espressif__mdns -> mdns
full_name = entry.name # e.g., espressif__mdns
short_name = full_name.split("__", 1)[1].lower()
# Find the .a archive under esp-idf/<vendor>__<name>/
archive = espidf_dir / full_name / f"lib{full_name}.a"
if archive.exists():
libraries[short_name] = [archive]
hash_to_name[full_name] = short_name
_LOGGER.debug(
"Discovered IDF managed component: %s -> %s",
short_name,
archive,
)
def _build_library_symbol_map(
self, libraries: dict[str, list[Path]]
) -> dict[str, str]:
"""Build a symbol-to-library mapping from library archives or object files.
Runs ``nm --defined-only`` on each ``.a`` or ``.o`` file to collect
global and weak defined symbols.
Args:
libraries: Dictionary mapping library name to list of file paths
(``.a`` archives or ``.o`` object files).
Returns:
Dictionary mapping symbol name to library name.
"""
symbol_map: dict[str, str] = {}
if not self.nm_path:
return symbol_map
for lib_name, file_paths in libraries.items():
result = run_tool(
[self.nm_path, "--defined-only", *(str(p) for p in file_paths)],
timeout=10,
)
if result is None or result.returncode != 0:
continue
for line in result.stdout.splitlines():
parts = line.split()
if len(parts) < 3:
continue
sym_type = parts[-2]
sym_name = parts[-1]
# Include global defined symbols (uppercase) and weak symbols (W/V)
if sym_type in _NM_DEFINED_GLOBAL_TYPES:
symbol_map[sym_name] = lib_name
return symbol_map
@staticmethod
def _build_heuristic_to_lib_mapping(
library_names: set[str],
) -> dict[str, str]:
"""Build mapping from heuristic pattern categories to discovered libraries.
Heuristic categories like ``mdns_lib``, ``web_server_lib``, ``async_tcp``
exist as approximations for library attribution. When we discover the
actual library, symbols matching those heuristics should be redirected
to the ``[lib]`` category instead.
The mapping is built by checking if the normalized category name
(stripped of ``_lib`` suffix and underscores) appears as a substring
of any discovered library name.
Examples::
mdns_lib -> mdns -> in "mdns" or "esp8266mdns" -> [lib]mdns
web_server_lib -> webserver -> in "espasyncwebserver" -> [lib]espasyncwebserver
async_tcp -> asynctcp -> in "espasynctcp" -> [lib]espasynctcp
Args:
library_names: Set of discovered library names (lowercase).
Returns:
Dictionary mapping heuristic category to ``[lib]<name>`` string.
"""
mapping: dict[str, str] = {}
all_categories = set(SYMBOL_PATTERNS) | set(DEMANGLED_PATTERNS)
for category in all_categories:
base = category.removesuffix("_lib").replace("_", "")
# Collect all libraries whose name contains the base string
candidates = [lib_name for lib_name in library_names if base in lib_name]
if not candidates:
continue
# Choose a deterministic "best" match:
# 1. Prefer exact name matches over substring matches.
# 2. Among non-exact matches, prefer the shortest library name.
# 3. Break remaining ties lexicographically.
best_lib = min(
candidates,
key=lambda lib_name, _base=base: (
lib_name != _base,
len(lib_name),
lib_name,
),
)
mapping[category] = f"{_COMPONENT_PREFIX_LIB}{best_lib}"
if mapping:
_LOGGER.debug(
"Heuristic-to-library redirects: %s",
", ".join(f"{k} -> {v}" for k, v in sorted(mapping.items())),
)
return mapping
def _parse_map_file(self) -> dict[str, str] | None:
"""Parse linker map file to build authoritative symbol-to-library mapping.
The linker map file contains the definitive source attribution for every
symbol, including local/static ones that ``nm`` cannot safely export.
Map file format (GNU ld)::
.text._mdns_service_task
0x400e9fdc 0x65c .pioenvs/env/esp-idf/espressif__mdns/libespressif__mdns.a(mdns.c.o)
Each section entry has a ``.section.symbol_name`` line followed by an
indented line with address, size, and source path.
Returns:
Symbol-to-library dict, or ``None`` if no usable map file exists.
"""
map_path = self.elf_path.with_suffix(".map")
if not map_path.exists() or map_path.stat().st_size < 10000:
return None
_LOGGER.info("Parsing linker map file: %s", map_path.name)
try:
map_text = map_path.read_text(encoding="utf-8", errors="replace")
except OSError as err:
_LOGGER.warning("Failed to read map file: %s", err)
return None
symbol_map: dict[str, str] = {}
current_symbol: str | None = None
section_prefixes = (".text.", ".rodata.", ".data.", ".bss.", ".literal.")
for line in map_text.splitlines():
# Match section.symbol line: " .text.symbol_name"
# Single space indent, starts with dot
if len(line) > 2 and line[0] == " " and line[1] == ".":
stripped = line.strip()
for prefix in section_prefixes:
if stripped.startswith(prefix):
current_symbol = stripped[len(prefix) :]
break
else:
current_symbol = None
continue
# Match source attribution line: " 0xADDR 0xSIZE source_path"
if current_symbol is None:
continue
fields = line.split()
# Skip compiler-generated local names (e.g., packet$19, buf$20)
# that can collide across compilation units
if (
len(fields) >= 3
and fields[0].startswith("0x")
and fields[1].startswith("0x")
and not _COMPILER_LOCAL_PATTERN.match(current_symbol)
):
source_path = fields[2]
# Check if source path contains a known library directory
for dir_key, lib_name in self._lib_hash_to_name.items():
if dir_key in source_path:
symbol_map[current_symbol] = lib_name
break
current_symbol = None
return symbol_map or None
def _scan_libraries(self) -> None:
"""Discover third-party libraries and build symbol mapping.
Scans both PlatformIO ``lib<hex>/`` directories (Arduino builds) and
ESP-IDF ``managed_components/`` (IDF builds) to find library archives.
Uses the linker map file for authoritative symbol attribution when
available, falling back to ``nm`` scanning with heuristic redirects.
"""
libraries: dict[str, list[Path]] = {}
self._discover_pio_libraries(libraries, self._lib_hash_to_name)
self._discover_idf_managed_components(libraries, self._lib_hash_to_name)
if not libraries:
_LOGGER.debug("No third-party libraries found")
return
_LOGGER.info(
"Scanning %d libraries: %s",
len(libraries),
", ".join(sorted(libraries)),
)
# Heuristic redirect catches local symbols (e.g., mdns_task_buffer$14)
# that can't be safely added to the symbol map due to name collisions
self._heuristic_to_lib = self._build_heuristic_to_lib_mapping(
set(libraries.keys())
)
# Try linker map file first (authoritative, includes local symbols)
map_symbols = self._parse_map_file()
if map_symbols is not None:
self._lib_symbol_map = map_symbols
_LOGGER.info(
"Built library symbol map from linker map: %d symbols",
len(self._lib_symbol_map),
)
return
# Fall back to nm scanning (global symbols only)
self._lib_symbol_map = self._build_library_symbol_map(libraries)
_LOGGER.info(
"Built library symbol map from nm: %d symbols from %d libraries",
len(self._lib_symbol_map),
len(libraries),
)
def _find_object_files_dir(self) -> Path | None:
"""Find the directory containing object files for this build.
@@ -903,21 +559,9 @@ class MemoryAnalyzer:
if "esphome" in parts and "components" not in parts:
return _COMPONENT_CORE
# Framework/library files - check for PlatformIO library hash dirs
# e.g., lib65b/ESPAsyncTCP/... -> [lib]espasynctcp
if parts and parts[0] in self._lib_hash_to_name:
return f"{_COMPONENT_PREFIX_LIB}{self._lib_hash_to_name[parts[0]]}"
# ESP-IDF managed components: managed_components/espressif__mdns/... -> [lib]mdns
if (
len(parts) >= 2
and parts[0] == "managed_components"
and parts[1] in self._lib_hash_to_name
):
return f"{_COMPONENT_PREFIX_LIB}{self._lib_hash_to_name[parts[1]]}"
# Other framework/library files - return the first path component
# e.g., FrameworkArduino/... -> FrameworkArduino
# Framework/library files - return the first path component
# e.g., lib65b/ESPAsyncTCP/... -> lib65b
# FrameworkArduino/... -> FrameworkArduino
return parts[0] if parts else source_file
def _analyze_cswtch_symbols(self) -> None:

View File

@@ -5,7 +5,6 @@ from __future__ import annotations
from collections import defaultdict
from collections.abc import Callable
import heapq
import json
from operator import itemgetter
import sys
from typing import TYPE_CHECKING
@@ -15,7 +14,6 @@ from . import (
_COMPONENT_CORE,
_COMPONENT_PREFIX_ESPHOME,
_COMPONENT_PREFIX_EXTERNAL,
_COMPONENT_PREFIX_LIB,
RAM_SECTIONS,
MemoryAnalyzer,
)
@@ -409,11 +407,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
for name, mem in components
if name.startswith(_COMPONENT_PREFIX_EXTERNAL)
]
library_components = [
(name, mem)
for name, mem in components
if name.startswith(_COMPONENT_PREFIX_LIB)
]
top_esphome_components = sorted(
esphome_components, key=lambda x: x[1].flash_total, reverse=True
@@ -424,11 +417,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
external_components, key=lambda x: x[1].flash_total, reverse=True
)
# Include all library components
top_library_components = sorted(
library_components, key=lambda x: x[1].flash_total, reverse=True
)
# Check if API component exists and ensure it's included
api_component = None
for name, mem in components:
@@ -447,11 +435,10 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
if name in system_components_to_include
]
# Combine all components to analyze: top ESPHome + all external + libraries + API if not already included + system components
# Combine all components to analyze: top ESPHome + all external + API if not already included + system components
components_to_analyze = (
list(top_esphome_components)
+ list(top_external_components)
+ list(top_library_components)
+ system_components
)
if api_component and api_component not in components_to_analyze:
@@ -541,28 +528,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
return "\n".join(lines)
def to_json(self) -> str:
"""Export analysis results as JSON."""
data = {
"components": {
name: {
"text": mem.text_size,
"rodata": mem.rodata_size,
"data": mem.data_size,
"bss": mem.bss_size,
"flash_total": mem.flash_total,
"ram_total": mem.ram_total,
"symbol_count": mem.symbol_count,
}
for name, mem in self.components.items()
},
"totals": {
"flash": sum(c.flash_total for c in self.components.values()),
"ram": sum(c.ram_total for c in self.components.values()),
},
}
return json.dumps(data, indent=2)
def dump_uncategorized_symbols(self, output_file: str | None = None) -> None:
"""Dump uncategorized symbols for analysis."""
# Sort by size descending

View File

@@ -11,7 +11,6 @@
from esphome.cpp_generator import ( # noqa: F401
ArrayInitializer,
Expression,
FlashStringLiteral,
LineComment,
LogStringLiteral,
MockObj,

View File

@@ -524,24 +524,24 @@ async def homeassistant_service_to_code(
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
serv = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, serv, False)
templ = await cg.templatable(config[CONF_ACTION], args, cg.std_string)
templ = await cg.templatable(config[CONF_ACTION], args, None)
cg.add(var.set_service(templ))
# Initialize FixedVectors with exact sizes from config
cg.add(var.init_data(len(config[CONF_DATA])))
for key, value in config[CONF_DATA].items():
templ = await cg.templatable(value, args, cg.std_string)
cg.add(var.add_data(cg.FlashStringLiteral(key), templ))
templ = await cg.templatable(value, args, None)
cg.add(var.add_data(key, templ))
cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE])))
for key, value in config[CONF_DATA_TEMPLATE].items():
templ = await cg.templatable(value, args, cg.std_string)
cg.add(var.add_data_template(cg.FlashStringLiteral(key), templ))
templ = await cg.templatable(value, args, None)
cg.add(var.add_data_template(key, templ))
cg.add(var.init_variables(len(config[CONF_VARIABLES])))
for key, value in config[CONF_VARIABLES].items():
templ = await cg.templatable(value, args, cg.std_string)
cg.add(var.add_variable(cg.FlashStringLiteral(key), templ))
templ = await cg.templatable(value, args, None)
cg.add(var.add_variable(key, templ))
if on_error := config.get(CONF_ON_ERROR):
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES")
@@ -609,24 +609,24 @@ async def homeassistant_event_to_code(config, action_id, template_arg, args):
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
serv = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, serv, True)
templ = await cg.templatable(config[CONF_EVENT], args, cg.std_string)
templ = await cg.templatable(config[CONF_EVENT], args, None)
cg.add(var.set_service(templ))
# Initialize FixedVectors with exact sizes from config
cg.add(var.init_data(len(config[CONF_DATA])))
for key, value in config[CONF_DATA].items():
templ = await cg.templatable(value, args, cg.std_string)
cg.add(var.add_data(cg.FlashStringLiteral(key), templ))
templ = await cg.templatable(value, args, None)
cg.add(var.add_data(key, templ))
cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE])))
for key, value in config[CONF_DATA_TEMPLATE].items():
templ = await cg.templatable(value, args, cg.std_string)
cg.add(var.add_data_template(cg.FlashStringLiteral(key), templ))
templ = await cg.templatable(value, args, None)
cg.add(var.add_data_template(key, templ))
cg.add(var.init_variables(len(config[CONF_VARIABLES])))
for key, value in config[CONF_VARIABLES].items():
templ = await cg.templatable(value, args, cg.std_string)
cg.add(var.add_variable(cg.FlashStringLiteral(key), templ))
templ = await cg.templatable(value, args, None)
cg.add(var.add_variable(key, templ))
return var
@@ -649,11 +649,11 @@ async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, arg
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
serv = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, serv, True)
cg.add(var.set_service(cg.FlashStringLiteral("esphome.tag_scanned")))
cg.add(var.set_service("esphome.tag_scanned"))
# Initialize FixedVector with exact size (1 data field)
cg.add(var.init_data(1))
templ = await cg.templatable(config[CONF_TAG], args, cg.std_string)
cg.add(var.add_data(cg.FlashStringLiteral("tag_id"), templ))
cg.add(var.add_data("tag_id", templ))
return var

View File

@@ -133,8 +133,8 @@ void APIConnection::start() {
return;
}
// Initialize client name with peername (IP address) until Hello message provides actual name
char peername[socket::SOCKADDR_STR_LEN];
this->helper_->set_client_name(this->helper_->get_peername_to(peername), strlen(peername));
const char *peername = this->helper_->get_client_peername();
this->helper_->set_client_name(peername, strlen(peername));
}
APIConnection::~APIConnection() {
@@ -179,8 +179,8 @@ void APIConnection::begin_iterator_(ActiveIterator type) {
void APIConnection::loop() {
if (this->flags_.next_close) {
// requested a disconnect - don't close socket here, let APIServer::loop() do it
// so getpeername() still works for the disconnect trigger
// requested a disconnect
this->helper_->close();
this->flags_.remove = true;
return;
}
@@ -219,8 +219,35 @@ void APIConnection::loop() {
this->process_batch_();
}
if (this->active_iterator_ != ActiveIterator::NONE) {
this->process_active_iterator_();
switch (this->active_iterator_) {
case ActiveIterator::LIST_ENTITIES:
if (this->iterator_storage_.list_entities.completed()) {
this->destroy_active_iterator_();
if (this->flags_.state_subscription) {
this->begin_iterator_(ActiveIterator::INITIAL_STATE);
}
} else {
this->process_iterator_batch_(this->iterator_storage_.list_entities);
}
break;
case ActiveIterator::INITIAL_STATE:
if (this->iterator_storage_.initial_state.completed()) {
this->destroy_active_iterator_();
// Process any remaining batched messages immediately
if (!this->deferred_batch_.empty()) {
this->process_batch_();
}
// Now that everything is sent, enable immediate sending for future state changes
this->flags_.should_try_send_immediately = true;
// Release excess memory from buffers that grew during initial sync
this->deferred_batch_.release_buffer();
this->helper_->release_buffers();
} else {
this->process_iterator_batch_(this->iterator_storage_.initial_state);
}
break;
case ActiveIterator::NONE:
break;
}
if (this->flags_.sent_ping) {
@@ -256,50 +283,7 @@ void APIConnection::loop() {
#endif
}
void APIConnection::process_active_iterator_() {
// Caller ensures active_iterator_ != NONE
if (this->active_iterator_ == ActiveIterator::LIST_ENTITIES) {
if (this->iterator_storage_.list_entities.completed()) {
this->destroy_active_iterator_();
if (this->flags_.state_subscription) {
this->begin_iterator_(ActiveIterator::INITIAL_STATE);
}
} else {
this->process_iterator_batch_(this->iterator_storage_.list_entities);
}
} else { // INITIAL_STATE
if (this->iterator_storage_.initial_state.completed()) {
this->destroy_active_iterator_();
// Process any remaining batched messages immediately
if (!this->deferred_batch_.empty()) {
this->process_batch_();
}
// Now that everything is sent, enable immediate sending for future state changes
this->flags_.should_try_send_immediately = true;
// Release excess memory from buffers that grew during initial sync
this->deferred_batch_.release_buffer();
this->helper_->release_buffers();
} else {
this->process_iterator_batch_(this->iterator_storage_.initial_state);
}
}
}
void APIConnection::process_iterator_batch_(ComponentIterator &iterator) {
size_t initial_size = this->deferred_batch_.size();
size_t max_batch = this->get_max_batch_size_();
while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < max_batch) {
iterator.advance();
}
// If the batch is full, process it immediately
// Note: iterator.advance() already calls schedule_batch_() via schedule_message_()
if (this->deferred_batch_.size() >= max_batch) {
this->process_batch_();
}
}
bool APIConnection::send_disconnect_response_() {
bool APIConnection::send_disconnect_response() {
// remote initiated disconnect_client
// don't close yet, we still need to send the disconnect response
// close will happen on next loop
@@ -309,8 +293,7 @@ bool APIConnection::send_disconnect_response_() {
return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE);
}
void APIConnection::on_disconnect_response() {
// Don't close socket here, let APIServer::loop() do it
// so getpeername() still works for the disconnect trigger
this->helper_->close();
this->flags_.remove = true;
}
@@ -423,7 +406,7 @@ uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *c
msg.device_class = cover->get_device_class_ref();
return fill_and_encode_entity_info(cover, msg, ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size);
}
void APIConnection::on_cover_command_request(const CoverCommandRequest &msg) {
void APIConnection::cover_command(const CoverCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(cover::Cover, cover, cover)
if (msg.has_position)
call.set_position(msg.position);
@@ -466,7 +449,7 @@ uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *con
msg.supported_preset_modes = &traits.supported_preset_modes();
return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size);
}
void APIConnection::on_fan_command_request(const FanCommandRequest &msg) {
void APIConnection::fan_command(const FanCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(fan::Fan, fan, fan)
if (msg.has_state)
call.set_state(msg.state);
@@ -534,7 +517,7 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c
msg.effects = &effects_list;
return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size);
}
void APIConnection::on_light_command_request(const LightCommandRequest &msg) {
void APIConnection::light_command(const LightCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(light::LightState, light, light)
if (msg.has_state)
call.set_state(msg.state);
@@ -611,7 +594,7 @@ uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection *
msg.device_class = a_switch->get_device_class_ref();
return fill_and_encode_entity_info(a_switch, msg, ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size);
}
void APIConnection::on_switch_command_request(const SwitchCommandRequest &msg) {
void APIConnection::switch_command(const SwitchCommandRequest &msg) {
ENTITY_COMMAND_GET(switch_::Switch, a_switch, switch)
if (msg.state) {
@@ -709,7 +692,7 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection
msg.supported_swing_modes = &traits.get_supported_swing_modes();
return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size);
}
void APIConnection::on_climate_command_request(const ClimateCommandRequest &msg) {
void APIConnection::climate_command(const ClimateCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(climate::Climate, climate, climate)
if (msg.has_mode)
call.set_mode(static_cast<climate::ClimateMode>(msg.mode));
@@ -759,7 +742,7 @@ uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection *
msg.step = number->traits.get_step();
return fill_and_encode_entity_info(number, msg, ListEntitiesNumberResponse::MESSAGE_TYPE, conn, remaining_size);
}
void APIConnection::on_number_command_request(const NumberCommandRequest &msg) {
void APIConnection::number_command(const NumberCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(number::Number, number, number)
call.set_value(msg.state);
call.perform();
@@ -784,7 +767,7 @@ uint16_t APIConnection::try_send_date_info(EntityBase *entity, APIConnection *co
ListEntitiesDateResponse msg;
return fill_and_encode_entity_info(date, msg, ListEntitiesDateResponse::MESSAGE_TYPE, conn, remaining_size);
}
void APIConnection::on_date_command_request(const DateCommandRequest &msg) {
void APIConnection::date_command(const DateCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(datetime::DateEntity, date, date)
call.set_date(msg.year, msg.month, msg.day);
call.perform();
@@ -809,7 +792,7 @@ uint16_t APIConnection::try_send_time_info(EntityBase *entity, APIConnection *co
ListEntitiesTimeResponse msg;
return fill_and_encode_entity_info(time, msg, ListEntitiesTimeResponse::MESSAGE_TYPE, conn, remaining_size);
}
void APIConnection::on_time_command_request(const TimeCommandRequest &msg) {
void APIConnection::time_command(const TimeCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(datetime::TimeEntity, time, time)
call.set_time(msg.hour, msg.minute, msg.second);
call.perform();
@@ -836,7 +819,7 @@ uint16_t APIConnection::try_send_datetime_info(EntityBase *entity, APIConnection
ListEntitiesDateTimeResponse msg;
return fill_and_encode_entity_info(datetime, msg, ListEntitiesDateTimeResponse::MESSAGE_TYPE, conn, remaining_size);
}
void APIConnection::on_date_time_command_request(const DateTimeCommandRequest &msg) {
void APIConnection::datetime_command(const DateTimeCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(datetime::DateTimeEntity, datetime, datetime)
call.set_datetime(msg.epoch_seconds);
call.perform();
@@ -865,7 +848,7 @@ uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *co
msg.pattern = text->traits.get_pattern_ref();
return fill_and_encode_entity_info(text, msg, ListEntitiesTextResponse::MESSAGE_TYPE, conn, remaining_size);
}
void APIConnection::on_text_command_request(const TextCommandRequest &msg) {
void APIConnection::text_command(const TextCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(text::Text, text, text)
call.set_value(msg.state);
call.perform();
@@ -891,7 +874,7 @@ uint16_t APIConnection::try_send_select_info(EntityBase *entity, APIConnection *
msg.options = &select->traits.get_options();
return fill_and_encode_entity_info(select, msg, ListEntitiesSelectResponse::MESSAGE_TYPE, conn, remaining_size);
}
void APIConnection::on_select_command_request(const SelectCommandRequest &msg) {
void APIConnection::select_command(const SelectCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(select::Select, select, select)
call.set_option(msg.state.c_str(), msg.state.size());
call.perform();
@@ -905,7 +888,7 @@ uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection *
msg.device_class = button->get_device_class_ref();
return fill_and_encode_entity_info(button, msg, ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size);
}
void esphome::api::APIConnection::on_button_command_request(const ButtonCommandRequest &msg) {
void esphome::api::APIConnection::button_command(const ButtonCommandRequest &msg) {
ENTITY_COMMAND_GET(button::Button, button, button)
button->press();
}
@@ -931,7 +914,7 @@ uint16_t APIConnection::try_send_lock_info(EntityBase *entity, APIConnection *co
msg.requires_code = a_lock->traits.get_requires_code();
return fill_and_encode_entity_info(a_lock, msg, ListEntitiesLockResponse::MESSAGE_TYPE, conn, remaining_size);
}
void APIConnection::on_lock_command_request(const LockCommandRequest &msg) {
void APIConnection::lock_command(const LockCommandRequest &msg) {
ENTITY_COMMAND_GET(lock::Lock, a_lock, lock)
switch (msg.command) {
@@ -969,7 +952,7 @@ uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *c
msg.supports_stop = traits.get_supports_stop();
return fill_and_encode_entity_info(valve, msg, ListEntitiesValveResponse::MESSAGE_TYPE, conn, remaining_size);
}
void APIConnection::on_valve_command_request(const ValveCommandRequest &msg) {
void APIConnection::valve_command(const ValveCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(valve::Valve, valve, valve)
if (msg.has_position)
call.set_position(msg.position);
@@ -1013,7 +996,7 @@ uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnec
return fill_and_encode_entity_info(media_player, msg, ListEntitiesMediaPlayerResponse::MESSAGE_TYPE, conn,
remaining_size);
}
void APIConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) {
void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(media_player::MediaPlayer, media_player, media_player)
if (msg.has_command) {
call.set_command(static_cast<media_player::MediaPlayerCommand>(msg.command));
@@ -1080,7 +1063,7 @@ uint16_t APIConnection::try_send_camera_info(EntityBase *entity, APIConnection *
ListEntitiesCameraResponse msg;
return fill_and_encode_entity_info(camera, msg, ListEntitiesCameraResponse::MESSAGE_TYPE, conn, remaining_size);
}
void APIConnection::on_camera_image_request(const CameraImageRequest &msg) {
void APIConnection::camera_image(const CameraImageRequest &msg) {
if (camera::Camera::instance() == nullptr)
return;
@@ -1109,47 +1092,41 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) {
#endif
#ifdef USE_BLUETOOTH_PROXY
void APIConnection::on_subscribe_bluetooth_le_advertisements_request(
const SubscribeBluetoothLEAdvertisementsRequest &msg) {
void APIConnection::subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) {
bluetooth_proxy::global_bluetooth_proxy->subscribe_api_connection(this, msg.flags);
}
void APIConnection::on_unsubscribe_bluetooth_le_advertisements_request() {
void APIConnection::unsubscribe_bluetooth_le_advertisements() {
bluetooth_proxy::global_bluetooth_proxy->unsubscribe_api_connection(this);
}
void APIConnection::on_bluetooth_device_request(const BluetoothDeviceRequest &msg) {
void APIConnection::bluetooth_device_request(const BluetoothDeviceRequest &msg) {
bluetooth_proxy::global_bluetooth_proxy->bluetooth_device_request(msg);
}
void APIConnection::on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg) {
void APIConnection::bluetooth_gatt_read(const BluetoothGATTReadRequest &msg) {
bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_read(msg);
}
void APIConnection::on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg) {
void APIConnection::bluetooth_gatt_write(const BluetoothGATTWriteRequest &msg) {
bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_write(msg);
}
void APIConnection::on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg) {
void APIConnection::bluetooth_gatt_read_descriptor(const BluetoothGATTReadDescriptorRequest &msg) {
bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_read_descriptor(msg);
}
void APIConnection::on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg) {
void APIConnection::bluetooth_gatt_write_descriptor(const BluetoothGATTWriteDescriptorRequest &msg) {
bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_write_descriptor(msg);
}
void APIConnection::on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg) {
void APIConnection::bluetooth_gatt_get_services(const BluetoothGATTGetServicesRequest &msg) {
bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_send_services(msg);
}
void APIConnection::on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) {
void APIConnection::bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) {
bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_notify(msg);
}
bool APIConnection::send_subscribe_bluetooth_connections_free_response_() {
bool APIConnection::send_subscribe_bluetooth_connections_free_response() {
bluetooth_proxy::global_bluetooth_proxy->send_connections_free(this);
return true;
}
void APIConnection::on_subscribe_bluetooth_connections_free_request() {
if (!this->send_subscribe_bluetooth_connections_free_response_()) {
this->on_fatal_error();
}
}
void APIConnection::on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) {
void APIConnection::bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) {
bluetooth_proxy::global_bluetooth_proxy->bluetooth_scanner_set_mode(
msg.mode == enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE);
}
@@ -1161,7 +1138,7 @@ bool APIConnection::check_voice_assistant_api_connection_() const {
voice_assistant::global_voice_assistant->get_api_connection() == this;
}
void APIConnection::on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) {
void APIConnection::subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) {
if (voice_assistant::global_voice_assistant != nullptr) {
voice_assistant::global_voice_assistant->client_subscription(this, msg.subscribe);
}
@@ -1207,7 +1184,7 @@ void APIConnection::on_voice_assistant_announce_request(const VoiceAssistantAnno
}
}
bool APIConnection::send_voice_assistant_get_configuration_response_(const VoiceAssistantConfigurationRequest &msg) {
bool APIConnection::send_voice_assistant_get_configuration_response(const VoiceAssistantConfigurationRequest &msg) {
VoiceAssistantConfigurationResponse resp;
if (!this->check_voice_assistant_api_connection_()) {
return this->send_message(resp, VoiceAssistantConfigurationResponse::MESSAGE_TYPE);
@@ -1244,13 +1221,8 @@ bool APIConnection::send_voice_assistant_get_configuration_response_(const Voice
resp.max_active_wake_words = config.max_active_wake_words;
return this->send_message(resp, VoiceAssistantConfigurationResponse::MESSAGE_TYPE);
}
void APIConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) {
if (!this->send_voice_assistant_get_configuration_response_(msg)) {
this->on_fatal_error();
}
}
void APIConnection::on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) {
void APIConnection::voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) {
if (this->check_voice_assistant_api_connection_()) {
voice_assistant::global_voice_assistant->on_set_configuration(msg.active_wake_words);
}
@@ -1258,11 +1230,11 @@ void APIConnection::on_voice_assistant_set_configuration(const VoiceAssistantSet
#endif
#ifdef USE_ZWAVE_PROXY
void APIConnection::on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) {
void APIConnection::zwave_proxy_frame(const ZWaveProxyFrame &msg) {
zwave_proxy::global_zwave_proxy->send_frame(msg.data, msg.data_len);
}
void APIConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) {
void APIConnection::zwave_proxy_request(const ZWaveProxyRequest &msg) {
zwave_proxy::global_zwave_proxy->zwave_proxy_request(this, msg.type);
}
#endif
@@ -1290,7 +1262,7 @@ uint16_t APIConnection::try_send_alarm_control_panel_info(EntityBase *entity, AP
return fill_and_encode_entity_info(a_alarm_control_panel, msg, ListEntitiesAlarmControlPanelResponse::MESSAGE_TYPE,
conn, remaining_size);
}
void APIConnection::on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) {
void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(alarm_control_panel::AlarmControlPanel, a_alarm_control_panel, alarm_control_panel)
switch (msg.command) {
case enums::ALARM_CONTROL_PANEL_DISARM:
@@ -1350,7 +1322,7 @@ uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnec
return fill_and_encode_entity_info(wh, msg, ListEntitiesWaterHeaterResponse::MESSAGE_TYPE, conn, remaining_size);
}
void APIConnection::on_water_heater_command_request(const WaterHeaterCommandRequest &msg) {
void APIConnection::water_heater_command(const WaterHeaterCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(water_heater::WaterHeater, water_heater, water_heater)
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_MODE)
call.set_mode(static_cast<water_heater::WaterHeaterMode>(msg.mode));
@@ -1392,7 +1364,7 @@ uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *c
#endif
#ifdef USE_IR_RF
void APIConnection::on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) {
void APIConnection::infrared_rf_transmit_raw_timings(const InfraredRFTransmitRawTimingsRequest &msg) {
// TODO: When RF is implemented, add a field to the message to distinguish IR vs RF
// and dispatch to the appropriate entity type based on that field.
#ifdef USE_INFRARED
@@ -1446,7 +1418,7 @@ uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection *
msg.device_class = update->get_device_class_ref();
return fill_and_encode_entity_info(update, msg, ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size);
}
void APIConnection::on_update_command_request(const UpdateCommandRequest &msg) {
void APIConnection::update_command(const UpdateCommandRequest &msg) {
ENTITY_COMMAND_GET(update::UpdateEntity, update, update)
switch (msg.command) {
@@ -1482,11 +1454,8 @@ void APIConnection::complete_authentication_() {
this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED);
this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("connected"));
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
{
char peername[socket::SOCKADDR_STR_LEN];
this->parent_->get_client_connected_trigger()->trigger(std::string(this->helper_->get_client_name()),
std::string(this->helper_->get_peername_to(peername)));
}
this->parent_->get_client_connected_trigger()->trigger(std::string(this->helper_->get_client_name()),
std::string(this->helper_->get_client_peername()));
#endif
#ifdef USE_HOMEASSISTANT_TIME
if (homeassistant::global_homeassistant_time != nullptr) {
@@ -1500,14 +1469,13 @@ void APIConnection::complete_authentication_() {
#endif
}
bool APIConnection::send_hello_response_(const HelloRequest &msg) {
bool APIConnection::send_hello_response(const HelloRequest &msg) {
// Copy client name with truncation if needed (set_client_name handles truncation)
this->helper_->set_client_name(msg.client_info.c_str(), msg.client_info.size());
this->client_api_version_major_ = msg.api_version_major;
this->client_api_version_minor_ = msg.api_version_minor;
char peername[socket::SOCKADDR_STR_LEN];
ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->helper_->get_client_name(),
this->helper_->get_peername_to(peername), this->client_api_version_major_, this->client_api_version_minor_);
this->helper_->get_client_peername(), this->client_api_version_major_, this->client_api_version_minor_);
HelloResponse resp;
resp.api_version_major = 1;
@@ -1522,12 +1490,12 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) {
return this->send_message(resp, HelloResponse::MESSAGE_TYPE);
}
bool APIConnection::send_ping_response_() {
bool APIConnection::send_ping_response() {
PingResponse resp;
return this->send_message(resp, PingResponse::MESSAGE_TYPE);
}
bool APIConnection::send_device_info_response_() {
bool APIConnection::send_device_info_response() {
DeviceInfoResponse resp{};
resp.name = StringRef(App.get_name());
resp.friendly_name = StringRef(App.get_friendly_name());
@@ -1650,26 +1618,6 @@ bool APIConnection::send_device_info_response_() {
return this->send_message(resp, DeviceInfoResponse::MESSAGE_TYPE);
}
void APIConnection::on_hello_request(const HelloRequest &msg) {
if (!this->send_hello_response_(msg)) {
this->on_fatal_error();
}
}
void APIConnection::on_disconnect_request() {
if (!this->send_disconnect_response_()) {
this->on_fatal_error();
}
}
void APIConnection::on_ping_request() {
if (!this->send_ping_response_()) {
this->on_fatal_error();
}
}
void APIConnection::on_device_info_request() {
if (!this->send_device_info_response_()) {
this->on_fatal_error();
}
}
#ifdef USE_API_HOMEASSISTANT_STATES
void APIConnection::on_home_assistant_state_response(const HomeAssistantStateResponse &msg) {
@@ -1708,7 +1656,7 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes
}
#endif
#ifdef USE_API_USER_DEFINED_ACTIONS
void APIConnection::on_execute_service_request(const ExecuteServiceRequest &msg) {
void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
bool found = false;
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
// Register the call and get a unique server-generated action_call_id
@@ -1774,7 +1722,7 @@ void APIConnection::on_homeassistant_action_response(const HomeassistantActionRe
};
#endif
#ifdef USE_API_NOISE
bool APIConnection::send_noise_encryption_set_key_response_(const NoiseEncryptionSetKeyRequest &msg) {
bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) {
NoiseEncryptionSetKeyResponse resp;
resp.success = false;
@@ -1795,14 +1743,9 @@ bool APIConnection::send_noise_encryption_set_key_response_(const NoiseEncryptio
return this->send_message(resp, NoiseEncryptionSetKeyResponse::MESSAGE_TYPE);
}
void APIConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) {
if (!this->send_noise_encryption_set_key_response_(msg)) {
this->on_fatal_error();
}
}
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
void APIConnection::on_subscribe_home_assistant_states_request() { state_subs_at_ = 0; }
void APIConnection::subscribe_home_assistant_states() { state_subs_at_ = 0; }
#endif
bool APIConnection::try_to_clear_buffer(bool log_out_of_space) {
if (this->flags_.remove)
@@ -1855,8 +1798,7 @@ void APIConnection::on_no_setup_connection() {
this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("no connection setup"));
}
void APIConnection::on_fatal_error() {
// Don't close socket here - keep it open so getpeername() works for logging
// Socket will be closed when client is removed from the list in APIServer::loop()
this->helper_->close();
this->flags_.remove = true;
}
@@ -2213,14 +2155,12 @@ void APIConnection::process_state_subscriptions_() {
#endif // USE_API_HOMEASSISTANT_STATES
void APIConnection::log_client_(int level, const LogString *message) {
char peername[socket::SOCKADDR_STR_LEN];
esp_log_printf_(level, TAG, __LINE__, ESPHOME_LOG_FORMAT("%s (%s): %s"), this->helper_->get_client_name(),
this->helper_->get_peername_to(peername), LOG_STR_ARG(message));
this->helper_->get_client_peername(), LOG_STR_ARG(message));
}
void APIConnection::log_warning_(const LogString *message, APIError err) {
char peername[socket::SOCKADDR_STR_LEN];
ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->helper_->get_client_name(), this->helper_->get_peername_to(peername),
ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->helper_->get_client_name(), this->helper_->get_client_peername(),
LOG_STR_ARG(message), LOG_STR_ARG(api_error_to_logstr(err)), errno);
}

View File

@@ -15,10 +15,6 @@
#include <limits>
#include <vector>
namespace esphome {
class ComponentIterator;
} // namespace esphome
namespace esphome::api {
// Keepalive timeout in milliseconds
@@ -32,7 +28,7 @@ static constexpr size_t MAX_INITIAL_PER_BATCH = 34; // For clients >= AP
static_assert(MAX_MESSAGES_PER_BATCH >= MAX_INITIAL_PER_BATCH,
"MAX_MESSAGES_PER_BATCH must be >= MAX_INITIAL_PER_BATCH");
class APIConnection final : public APIServerConnectionBase {
class APIConnection final : public APIServerConnection {
public:
friend class APIServer;
friend class ListEntitiesIterator;
@@ -51,72 +47,72 @@ class APIConnection final : public APIServerConnectionBase {
#endif
#ifdef USE_COVER
bool send_cover_state(cover::Cover *cover);
void on_cover_command_request(const CoverCommandRequest &msg) override;
void cover_command(const CoverCommandRequest &msg) override;
#endif
#ifdef USE_FAN
bool send_fan_state(fan::Fan *fan);
void on_fan_command_request(const FanCommandRequest &msg) override;
void fan_command(const FanCommandRequest &msg) override;
#endif
#ifdef USE_LIGHT
bool send_light_state(light::LightState *light);
void on_light_command_request(const LightCommandRequest &msg) override;
void light_command(const LightCommandRequest &msg) override;
#endif
#ifdef USE_SENSOR
bool send_sensor_state(sensor::Sensor *sensor);
#endif
#ifdef USE_SWITCH
bool send_switch_state(switch_::Switch *a_switch);
void on_switch_command_request(const SwitchCommandRequest &msg) override;
void switch_command(const SwitchCommandRequest &msg) override;
#endif
#ifdef USE_TEXT_SENSOR
bool send_text_sensor_state(text_sensor::TextSensor *text_sensor);
#endif
#ifdef USE_CAMERA
void set_camera_state(std::shared_ptr<camera::CameraImage> image);
void on_camera_image_request(const CameraImageRequest &msg) override;
void camera_image(const CameraImageRequest &msg) override;
#endif
#ifdef USE_CLIMATE
bool send_climate_state(climate::Climate *climate);
void on_climate_command_request(const ClimateCommandRequest &msg) override;
void climate_command(const ClimateCommandRequest &msg) override;
#endif
#ifdef USE_NUMBER
bool send_number_state(number::Number *number);
void on_number_command_request(const NumberCommandRequest &msg) override;
void number_command(const NumberCommandRequest &msg) override;
#endif
#ifdef USE_DATETIME_DATE
bool send_date_state(datetime::DateEntity *date);
void on_date_command_request(const DateCommandRequest &msg) override;
void date_command(const DateCommandRequest &msg) override;
#endif
#ifdef USE_DATETIME_TIME
bool send_time_state(datetime::TimeEntity *time);
void on_time_command_request(const TimeCommandRequest &msg) override;
void time_command(const TimeCommandRequest &msg) override;
#endif
#ifdef USE_DATETIME_DATETIME
bool send_datetime_state(datetime::DateTimeEntity *datetime);
void on_date_time_command_request(const DateTimeCommandRequest &msg) override;
void datetime_command(const DateTimeCommandRequest &msg) override;
#endif
#ifdef USE_TEXT
bool send_text_state(text::Text *text);
void on_text_command_request(const TextCommandRequest &msg) override;
void text_command(const TextCommandRequest &msg) override;
#endif
#ifdef USE_SELECT
bool send_select_state(select::Select *select);
void on_select_command_request(const SelectCommandRequest &msg) override;
void select_command(const SelectCommandRequest &msg) override;
#endif
#ifdef USE_BUTTON
void on_button_command_request(const ButtonCommandRequest &msg) override;
void button_command(const ButtonCommandRequest &msg) override;
#endif
#ifdef USE_LOCK
bool send_lock_state(lock::Lock *a_lock);
void on_lock_command_request(const LockCommandRequest &msg) override;
void lock_command(const LockCommandRequest &msg) override;
#endif
#ifdef USE_VALVE
bool send_valve_state(valve::Valve *valve);
void on_valve_command_request(const ValveCommandRequest &msg) override;
void valve_command(const ValveCommandRequest &msg) override;
#endif
#ifdef USE_MEDIA_PLAYER
bool send_media_player_state(media_player::MediaPlayer *media_player);
void on_media_player_command_request(const MediaPlayerCommandRequest &msg) override;
void media_player_command(const MediaPlayerCommandRequest &msg) override;
#endif
bool try_send_log_message(int level, const char *tag, const char *line, size_t message_len);
#ifdef USE_API_HOMEASSISTANT_SERVICES
@@ -130,18 +126,18 @@ class APIConnection final : public APIServerConnectionBase {
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
#endif // USE_API_HOMEASSISTANT_SERVICES
#ifdef USE_BLUETOOTH_PROXY
void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
void on_unsubscribe_bluetooth_le_advertisements_request() override;
void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
void unsubscribe_bluetooth_le_advertisements() override;
void on_bluetooth_device_request(const BluetoothDeviceRequest &msg) override;
void on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg) override;
void on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg) override;
void on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg) override;
void on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg) override;
void on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg) override;
void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) override;
void on_subscribe_bluetooth_connections_free_request() override;
void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) override;
void bluetooth_device_request(const BluetoothDeviceRequest &msg) override;
void bluetooth_gatt_read(const BluetoothGATTReadRequest &msg) override;
void bluetooth_gatt_write(const BluetoothGATTWriteRequest &msg) override;
void bluetooth_gatt_read_descriptor(const BluetoothGATTReadDescriptorRequest &msg) override;
void bluetooth_gatt_write_descriptor(const BluetoothGATTWriteDescriptorRequest &msg) override;
void bluetooth_gatt_get_services(const BluetoothGATTGetServicesRequest &msg) override;
void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) override;
bool send_subscribe_bluetooth_connections_free_response() override;
void bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) override;
#endif
#ifdef USE_HOMEASSISTANT_TIME
@@ -152,33 +148,33 @@ class APIConnection final : public APIServerConnectionBase {
#endif
#ifdef USE_VOICE_ASSISTANT
void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) override;
void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) override;
void on_voice_assistant_response(const VoiceAssistantResponse &msg) override;
void on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) override;
void on_voice_assistant_audio(const VoiceAssistantAudio &msg) override;
void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg) override;
void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg) override;
void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) override;
void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override;
bool send_voice_assistant_get_configuration_response(const VoiceAssistantConfigurationRequest &msg) override;
void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override;
#endif
#ifdef USE_ZWAVE_PROXY
void on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) override;
void on_z_wave_proxy_request(const ZWaveProxyRequest &msg) override;
void zwave_proxy_frame(const ZWaveProxyFrame &msg) override;
void zwave_proxy_request(const ZWaveProxyRequest &msg) override;
#endif
#ifdef USE_ALARM_CONTROL_PANEL
bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) override;
void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override;
#endif
#ifdef USE_WATER_HEATER
bool send_water_heater_state(water_heater::WaterHeater *water_heater);
void on_water_heater_command_request(const WaterHeaterCommandRequest &msg) override;
void water_heater_command(const WaterHeaterCommandRequest &msg) override;
#endif
#ifdef USE_IR_RF
void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) override;
void infrared_rf_transmit_raw_timings(const InfraredRFTransmitRawTimingsRequest &msg) override;
void send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg);
#endif
@@ -188,7 +184,7 @@ class APIConnection final : public APIServerConnectionBase {
#ifdef USE_UPDATE
bool send_update_state(update::UpdateEntity *update);
void on_update_command_request(const UpdateCommandRequest &msg) override;
void update_command(const UpdateCommandRequest &msg) override;
#endif
void on_disconnect_response() override;
@@ -202,12 +198,12 @@ class APIConnection final : public APIServerConnectionBase {
#ifdef USE_HOMEASSISTANT_TIME
void on_get_time_response(const GetTimeResponse &value) override;
#endif
void on_hello_request(const HelloRequest &msg) override;
void on_disconnect_request() override;
void on_ping_request() override;
void on_device_info_request() override;
void on_list_entities_request() override { this->begin_iterator_(ActiveIterator::LIST_ENTITIES); }
void on_subscribe_states_request() override {
bool send_hello_response(const HelloRequest &msg) override;
bool send_disconnect_response() override;
bool send_ping_response() override;
bool send_device_info_response() override;
void list_entities() override { this->begin_iterator_(ActiveIterator::LIST_ENTITIES); }
void subscribe_states() override {
this->flags_.state_subscription = true;
// Start initial state iterator only if no iterator is active
// If list_entities is running, we'll start initial_state when it completes
@@ -215,19 +211,19 @@ class APIConnection final : public APIServerConnectionBase {
this->begin_iterator_(ActiveIterator::INITIAL_STATE);
}
}
void on_subscribe_logs_request(const SubscribeLogsRequest &msg) override {
void subscribe_logs(const SubscribeLogsRequest &msg) override {
this->flags_.log_subscription = msg.level;
if (msg.dump_config)
App.schedule_dump_config();
}
#ifdef USE_API_HOMEASSISTANT_SERVICES
void on_subscribe_homeassistant_services_request() override { this->flags_.service_call_subscription = true; }
void subscribe_homeassistant_services() override { this->flags_.service_call_subscription = true; }
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
void on_subscribe_home_assistant_states_request() override;
void subscribe_home_assistant_states() override;
#endif
#ifdef USE_API_USER_DEFINED_ACTIONS
void on_execute_service_request(const ExecuteServiceRequest &msg) override;
void execute_service(const ExecuteServiceRequest &msg) override;
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
void send_execute_service_response(uint32_t call_id, bool success, StringRef error_message);
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
@@ -237,7 +233,7 @@ class APIConnection final : public APIServerConnectionBase {
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
#endif
#ifdef USE_API_NOISE
void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) override;
bool send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) override;
#endif
bool is_authenticated() override {
@@ -280,30 +276,13 @@ class APIConnection final : public APIServerConnectionBase {
bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override;
const char *get_name() const { return this->helper_->get_client_name(); }
/// Get peer name (IP address) into caller-provided buffer, returns buf for convenience
const char *get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const {
return this->helper_->get_peername_to(buf);
}
/// Get peer name (IP address) - cached at connection init time
const char *get_peername() const { return this->helper_->get_client_peername(); }
protected:
// Helper function to handle authentication completion
void complete_authentication_();
// Pattern B helpers: send response and return success/failure
bool send_hello_response_(const HelloRequest &msg);
bool send_disconnect_response_();
bool send_ping_response_();
bool send_device_info_response_();
#ifdef USE_API_NOISE
bool send_noise_encryption_set_key_response_(const NoiseEncryptionSetKeyRequest &msg);
#endif
#ifdef USE_BLUETOOTH_PROXY
bool send_subscribe_bluetooth_connections_free_response_();
#endif
#ifdef USE_VOICE_ASSISTANT
bool send_voice_assistant_get_configuration_response_(const VoiceAssistantConfigurationRequest &msg);
#endif
#ifdef USE_CAMERA
void try_send_camera_image_();
#endif
@@ -370,13 +349,20 @@ class APIConnection final : public APIServerConnectionBase {
return this->client_supports_api_version(1, 14) ? MAX_INITIAL_PER_BATCH : MAX_INITIAL_PER_BATCH_LEGACY;
}
// Process active iterator (list_entities/initial_state) during connection setup.
// Extracted from loop() — only runs during initial handshake, NONE in steady state.
void __attribute__((noinline)) process_active_iterator_();
// Helper method to process multiple entities from an iterator in a batch
template<typename Iterator> void process_iterator_batch_(Iterator &iterator) {
size_t initial_size = this->deferred_batch_.size();
size_t max_batch = this->get_max_batch_size_();
while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < max_batch) {
iterator.advance();
}
// Helper method to process multiple entities from an iterator in a batch.
// Takes ComponentIterator base class reference to avoid duplicate template instantiations.
void process_iterator_batch_(ComponentIterator &iterator);
// If the batch is full, process it immediately
// Note: iterator.advance() already calls schedule_batch_() via schedule_message_()
if (this->deferred_batch_.size() >= max_batch) {
this->process_batch_();
}
}
#ifdef USE_BINARY_SENSOR
static uint16_t try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);

View File

@@ -16,12 +16,7 @@ static const char *const TAG = "api.frame_helper";
static constexpr size_t API_MAX_LOG_BYTES = 168;
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
#define HELPER_LOG(msg, ...) \
do { \
char peername_buf[socket::SOCKADDR_STR_LEN]; \
this->get_peername_to(peername_buf); \
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, peername_buf, ##__VA_ARGS__); \
} while (0)
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
#else
#define HELPER_LOG(msg, ...) ((void) 0)
#endif
@@ -245,20 +240,13 @@ APIError APIFrameHelper::try_send_tx_buf_() {
return APIError::OK; // All buffers sent successfully
}
const char *APIFrameHelper::get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const {
if (this->socket_) {
this->socket_->getpeername_to(buf);
} else {
buf[0] = '\0';
}
return buf.data();
}
APIError APIFrameHelper::init_common_() {
if (state_ != State::INITIALIZE || this->socket_ == nullptr) {
HELPER_LOG("Bad state for init %d", (int) state_);
return APIError::BAD_STATE;
}
// Cache peername now while socket is valid - needed for error logging after socket failure
this->socket_->getpeername_to(this->client_peername_);
int err = this->socket_->setblocking(false);
if (err != 0) {
state_ = State::FAILED;

View File

@@ -90,9 +90,8 @@ class APIFrameHelper {
// Get client name (null-terminated)
const char *get_client_name() const { return this->client_name_; }
// Get client peername/IP into caller-provided buffer (fetches on-demand from socket)
// Returns pointer to buf for convenience in printf-style calls
const char *get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const;
// Get client peername/IP (null-terminated, cached at init time for availability after socket failure)
const char *get_client_peername() const { return this->client_peername_; }
// Set client name from buffer with length (truncates if needed)
void set_client_name(const char *name, size_t len) {
size_t copy_len = std::min(len, sizeof(this->client_name_) - 1);
@@ -106,8 +105,6 @@ class APIFrameHelper {
bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 0; }
int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); }
APIError close() {
if (state_ == State::CLOSED)
return APIError::OK; // Already closed
state_ = State::CLOSED;
int err = this->socket_->close();
if (err == -1)
@@ -234,6 +231,8 @@ class APIFrameHelper {
// Client name buffer - stores name from Hello message or initial peername
char client_name_[CLIENT_INFO_NAME_MAX_LEN]{};
// Cached peername/IP address - captured at init time for availability after socket failure
char client_peername_[socket::SOCKADDR_STR_LEN]{};
// Group smaller types together
uint16_t rx_buf_len_ = 0;

View File

@@ -29,12 +29,7 @@ static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit")
static constexpr size_t API_MAX_LOG_BYTES = 168;
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
#define HELPER_LOG(msg, ...) \
do { \
char peername_buf[socket::SOCKADDR_STR_LEN]; \
this->get_peername_to(peername_buf); \
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, peername_buf, ##__VA_ARGS__); \
} while (0)
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
#else
#define HELPER_LOG(msg, ...) ((void) 0)
#endif

View File

@@ -21,12 +21,7 @@ static const char *const TAG = "api.plaintext";
static constexpr size_t API_MAX_LOG_BYTES = 168;
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
#define HELPER_LOG(msg, ...) \
do { \
char peername_buf[socket::SOCKADDR_STR_LEN]; \
this->get_peername_to(peername_buf); \
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, peername_buf, ##__VA_ARGS__); \
} while (0)
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
#else
#define HELPER_LOG(msg, ...) ((void) 0)
#endif

View File

@@ -440,6 +440,19 @@ class PingResponse final : public ProtoMessage {
protected:
};
class DeviceInfoRequest final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 9;
static constexpr uint8_t ESTIMATED_SIZE = 0;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "device_info_request"; }
#endif
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
#ifdef USE_AREAS
class AreaInfo final : public ProtoMessage {
public:
@@ -533,6 +546,19 @@ class DeviceInfoResponse final : public ProtoMessage {
protected:
};
class ListEntitiesRequest final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 11;
static constexpr uint8_t ESTIMATED_SIZE = 0;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_request"; }
#endif
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
class ListEntitiesDoneResponse final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 19;
@@ -546,6 +572,19 @@ class ListEntitiesDoneResponse final : public ProtoMessage {
protected:
};
class SubscribeStatesRequest final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 20;
static constexpr uint8_t ESTIMATED_SIZE = 0;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "subscribe_states_request"; }
#endif
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
#ifdef USE_BINARY_SENSOR
class ListEntitiesBinarySensorResponse final : public InfoResponseProtoMessage {
public:
@@ -998,6 +1037,19 @@ class NoiseEncryptionSetKeyResponse final : public ProtoMessage {
};
#endif
#ifdef USE_API_HOMEASSISTANT_SERVICES
class SubscribeHomeassistantServicesRequest final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 34;
static constexpr uint8_t ESTIMATED_SIZE = 0;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "subscribe_homeassistant_services_request"; }
#endif
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
class HomeassistantServiceMap final : public ProtoMessage {
public:
StringRef key{};
@@ -1065,6 +1117,19 @@ class HomeassistantActionResponse final : public ProtoDecodableMessage {
};
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
class SubscribeHomeAssistantStatesRequest final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 38;
static constexpr uint8_t ESTIMATED_SIZE = 0;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "subscribe_home_assistant_states_request"; }
#endif
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
class SubscribeHomeAssistantStateResponse final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 39;
@@ -2095,6 +2160,19 @@ class BluetoothGATTNotifyDataResponse final : public ProtoMessage {
protected:
};
class SubscribeBluetoothConnectionsFreeRequest final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 80;
static constexpr uint8_t ESTIMATED_SIZE = 0;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "subscribe_bluetooth_connections_free_request"; }
#endif
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
class BluetoothConnectionsFreeResponse final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 81;
@@ -2201,6 +2279,19 @@ class BluetoothDeviceUnpairingResponse final : public ProtoMessage {
protected:
};
class UnsubscribeBluetoothLEAdvertisementsRequest final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 87;
static constexpr uint8_t ESTIMATED_SIZE = 0;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "unsubscribe_bluetooth_le_advertisements_request"; }
#endif
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
class BluetoothDeviceClearCacheResponse final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 88;

View File

@@ -764,6 +764,10 @@ const char *PingResponse::dump_to(DumpBuffer &out) const {
out.append("PingResponse {}");
return out.c_str();
}
const char *DeviceInfoRequest::dump_to(DumpBuffer &out) const {
out.append("DeviceInfoRequest {}");
return out.c_str();
}
#ifdef USE_AREAS
const char *AreaInfo::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "AreaInfo");
@@ -844,10 +848,18 @@ const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const {
#endif
return out.c_str();
}
const char *ListEntitiesRequest::dump_to(DumpBuffer &out) const {
out.append("ListEntitiesRequest {}");
return out.c_str();
}
const char *ListEntitiesDoneResponse::dump_to(DumpBuffer &out) const {
out.append("ListEntitiesDoneResponse {}");
return out.c_str();
}
const char *SubscribeStatesRequest::dump_to(DumpBuffer &out) const {
out.append("SubscribeStatesRequest {}");
return out.c_str();
}
#ifdef USE_BINARY_SENSOR
const char *ListEntitiesBinarySensorResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "ListEntitiesBinarySensorResponse");
@@ -1179,6 +1191,10 @@ const char *NoiseEncryptionSetKeyResponse::dump_to(DumpBuffer &out) const {
}
#endif
#ifdef USE_API_HOMEASSISTANT_SERVICES
const char *SubscribeHomeassistantServicesRequest::dump_to(DumpBuffer &out) const {
out.append("SubscribeHomeassistantServicesRequest {}");
return out.c_str();
}
const char *HomeassistantServiceMap::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "HomeassistantServiceMap");
dump_field(out, "key", this->key);
@@ -1229,6 +1245,10 @@ const char *HomeassistantActionResponse::dump_to(DumpBuffer &out) const {
}
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
const char *SubscribeHomeAssistantStatesRequest::dump_to(DumpBuffer &out) const {
out.append("SubscribeHomeAssistantStatesRequest {}");
return out.c_str();
}
const char *SubscribeHomeAssistantStateResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "SubscribeHomeAssistantStateResponse");
dump_field(out, "entity_id", this->entity_id);
@@ -1904,6 +1924,10 @@ const char *BluetoothGATTNotifyDataResponse::dump_to(DumpBuffer &out) const {
dump_bytes_field(out, "data", this->data_ptr_, this->data_len_);
return out.c_str();
}
const char *SubscribeBluetoothConnectionsFreeRequest::dump_to(DumpBuffer &out) const {
out.append("SubscribeBluetoothConnectionsFreeRequest {}");
return out.c_str();
}
const char *BluetoothConnectionsFreeResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "BluetoothConnectionsFreeResponse");
dump_field(out, "free", this->free);
@@ -1946,6 +1970,10 @@ const char *BluetoothDeviceUnpairingResponse::dump_to(DumpBuffer &out) const {
dump_field(out, "error", this->error);
return out.c_str();
}
const char *UnsubscribeBluetoothLEAdvertisementsRequest::dump_to(DumpBuffer &out) const {
out.append("UnsubscribeBluetoothLEAdvertisementsRequest {}");
return out.c_str();
}
const char *BluetoothDeviceClearCacheResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "BluetoothDeviceClearCacheResponse");
dump_field(out, "address", this->address);

View File

@@ -21,23 +21,6 @@ void APIServerConnectionBase::log_receive_message_(const LogString *name) {
#endif
void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {
// Check authentication/connection requirements
switch (msg_type) {
case HelloRequest::MESSAGE_TYPE: // No setup required
case DisconnectRequest::MESSAGE_TYPE: // No setup required
case PingRequest::MESSAGE_TYPE: // No setup required
break;
case 9 /* DeviceInfoRequest is empty */: // Connection setup only
if (!this->check_connection_setup_()) {
return;
}
break;
default:
if (!this->check_authenticated_()) {
return;
}
break;
}
switch (msg_type) {
case HelloRequest::MESSAGE_TYPE: {
HelloRequest msg;
@@ -76,21 +59,21 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
this->on_ping_response();
break;
}
case 9 /* DeviceInfoRequest is empty */: {
case DeviceInfoRequest::MESSAGE_TYPE: {
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_device_info_request"));
#endif
this->on_device_info_request();
break;
}
case 11 /* ListEntitiesRequest is empty */: {
case ListEntitiesRequest::MESSAGE_TYPE: {
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_list_entities_request"));
#endif
this->on_list_entities_request();
break;
}
case 20 /* SubscribeStatesRequest is empty */: {
case SubscribeStatesRequest::MESSAGE_TYPE: {
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_subscribe_states_request"));
#endif
@@ -151,7 +134,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
}
#endif
#ifdef USE_API_HOMEASSISTANT_SERVICES
case 34 /* SubscribeHomeassistantServicesRequest is empty */: {
case SubscribeHomeassistantServicesRequest::MESSAGE_TYPE: {
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_subscribe_homeassistant_services_request"));
#endif
@@ -169,7 +152,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
break;
}
#ifdef USE_API_HOMEASSISTANT_STATES
case 38 /* SubscribeHomeAssistantStatesRequest is empty */: {
case SubscribeHomeAssistantStatesRequest::MESSAGE_TYPE: {
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_subscribe_home_assistant_states_request"));
#endif
@@ -376,7 +359,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
}
#endif
#ifdef USE_BLUETOOTH_PROXY
case 80 /* SubscribeBluetoothConnectionsFreeRequest is empty */: {
case SubscribeBluetoothConnectionsFreeRequest::MESSAGE_TYPE: {
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_subscribe_bluetooth_connections_free_request"));
#endif
@@ -385,7 +368,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
}
#endif
#ifdef USE_BLUETOOTH_PROXY
case 87 /* UnsubscribeBluetoothLEAdvertisementsRequest is empty */: {
case UnsubscribeBluetoothLEAdvertisementsRequest::MESSAGE_TYPE: {
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_unsubscribe_bluetooth_le_advertisements_request"));
#endif
@@ -640,4 +623,222 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
}
}
void APIServerConnection::on_hello_request(const HelloRequest &msg) {
if (!this->send_hello_response(msg)) {
this->on_fatal_error();
}
}
void APIServerConnection::on_disconnect_request() {
if (!this->send_disconnect_response()) {
this->on_fatal_error();
}
}
void APIServerConnection::on_ping_request() {
if (!this->send_ping_response()) {
this->on_fatal_error();
}
}
void APIServerConnection::on_device_info_request() {
if (!this->send_device_info_response()) {
this->on_fatal_error();
}
}
void APIServerConnection::on_list_entities_request() { this->list_entities(); }
void APIServerConnection::on_subscribe_states_request() { this->subscribe_states(); }
void APIServerConnection::on_subscribe_logs_request(const SubscribeLogsRequest &msg) { this->subscribe_logs(msg); }
#ifdef USE_API_HOMEASSISTANT_SERVICES
void APIServerConnection::on_subscribe_homeassistant_services_request() { this->subscribe_homeassistant_services(); }
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
void APIServerConnection::on_subscribe_home_assistant_states_request() { this->subscribe_home_assistant_states(); }
#endif
#ifdef USE_API_USER_DEFINED_ACTIONS
void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) { this->execute_service(msg); }
#endif
#ifdef USE_API_NOISE
void APIServerConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) {
if (!this->send_noise_encryption_set_key_response(msg)) {
this->on_fatal_error();
}
}
#endif
#ifdef USE_BUTTON
void APIServerConnection::on_button_command_request(const ButtonCommandRequest &msg) { this->button_command(msg); }
#endif
#ifdef USE_CAMERA
void APIServerConnection::on_camera_image_request(const CameraImageRequest &msg) { this->camera_image(msg); }
#endif
#ifdef USE_CLIMATE
void APIServerConnection::on_climate_command_request(const ClimateCommandRequest &msg) { this->climate_command(msg); }
#endif
#ifdef USE_COVER
void APIServerConnection::on_cover_command_request(const CoverCommandRequest &msg) { this->cover_command(msg); }
#endif
#ifdef USE_DATETIME_DATE
void APIServerConnection::on_date_command_request(const DateCommandRequest &msg) { this->date_command(msg); }
#endif
#ifdef USE_DATETIME_DATETIME
void APIServerConnection::on_date_time_command_request(const DateTimeCommandRequest &msg) {
this->datetime_command(msg);
}
#endif
#ifdef USE_FAN
void APIServerConnection::on_fan_command_request(const FanCommandRequest &msg) { this->fan_command(msg); }
#endif
#ifdef USE_LIGHT
void APIServerConnection::on_light_command_request(const LightCommandRequest &msg) { this->light_command(msg); }
#endif
#ifdef USE_LOCK
void APIServerConnection::on_lock_command_request(const LockCommandRequest &msg) { this->lock_command(msg); }
#endif
#ifdef USE_MEDIA_PLAYER
void APIServerConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) {
this->media_player_command(msg);
}
#endif
#ifdef USE_NUMBER
void APIServerConnection::on_number_command_request(const NumberCommandRequest &msg) { this->number_command(msg); }
#endif
#ifdef USE_SELECT
void APIServerConnection::on_select_command_request(const SelectCommandRequest &msg) { this->select_command(msg); }
#endif
#ifdef USE_SIREN
void APIServerConnection::on_siren_command_request(const SirenCommandRequest &msg) { this->siren_command(msg); }
#endif
#ifdef USE_SWITCH
void APIServerConnection::on_switch_command_request(const SwitchCommandRequest &msg) { this->switch_command(msg); }
#endif
#ifdef USE_TEXT
void APIServerConnection::on_text_command_request(const TextCommandRequest &msg) { this->text_command(msg); }
#endif
#ifdef USE_DATETIME_TIME
void APIServerConnection::on_time_command_request(const TimeCommandRequest &msg) { this->time_command(msg); }
#endif
#ifdef USE_UPDATE
void APIServerConnection::on_update_command_request(const UpdateCommandRequest &msg) { this->update_command(msg); }
#endif
#ifdef USE_VALVE
void APIServerConnection::on_valve_command_request(const ValveCommandRequest &msg) { this->valve_command(msg); }
#endif
#ifdef USE_WATER_HEATER
void APIServerConnection::on_water_heater_command_request(const WaterHeaterCommandRequest &msg) {
this->water_heater_command(msg);
}
#endif
#ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_subscribe_bluetooth_le_advertisements_request(
const SubscribeBluetoothLEAdvertisementsRequest &msg) {
this->subscribe_bluetooth_le_advertisements(msg);
}
#endif
#ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_bluetooth_device_request(const BluetoothDeviceRequest &msg) {
this->bluetooth_device_request(msg);
}
#endif
#ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg) {
this->bluetooth_gatt_get_services(msg);
}
#endif
#ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg) {
this->bluetooth_gatt_read(msg);
}
#endif
#ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg) {
this->bluetooth_gatt_write(msg);
}
#endif
#ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg) {
this->bluetooth_gatt_read_descriptor(msg);
}
#endif
#ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg) {
this->bluetooth_gatt_write_descriptor(msg);
}
#endif
#ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) {
this->bluetooth_gatt_notify(msg);
}
#endif
#ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_subscribe_bluetooth_connections_free_request() {
if (!this->send_subscribe_bluetooth_connections_free_response()) {
this->on_fatal_error();
}
}
#endif
#ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_unsubscribe_bluetooth_le_advertisements_request() {
this->unsubscribe_bluetooth_le_advertisements();
}
#endif
#ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) {
this->bluetooth_scanner_set_mode(msg);
}
#endif
#ifdef USE_VOICE_ASSISTANT
void APIServerConnection::on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) {
this->subscribe_voice_assistant(msg);
}
#endif
#ifdef USE_VOICE_ASSISTANT
void APIServerConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) {
if (!this->send_voice_assistant_get_configuration_response(msg)) {
this->on_fatal_error();
}
}
#endif
#ifdef USE_VOICE_ASSISTANT
void APIServerConnection::on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) {
this->voice_assistant_set_configuration(msg);
}
#endif
#ifdef USE_ALARM_CONTROL_PANEL
void APIServerConnection::on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) {
this->alarm_control_panel_command(msg);
}
#endif
#ifdef USE_ZWAVE_PROXY
void APIServerConnection::on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) { this->zwave_proxy_frame(msg); }
#endif
#ifdef USE_ZWAVE_PROXY
void APIServerConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) { this->zwave_proxy_request(msg); }
#endif
#ifdef USE_IR_RF
void APIServerConnection::on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) {
this->infrared_rf_transmit_raw_timings(msg);
}
#endif
void APIServerConnection::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {
// Check authentication/connection requirements for messages
switch (msg_type) {
case HelloRequest::MESSAGE_TYPE: // No setup required
case DisconnectRequest::MESSAGE_TYPE: // No setup required
case PingRequest::MESSAGE_TYPE: // No setup required
break; // Skip all checks for these messages
case DeviceInfoRequest::MESSAGE_TYPE: // Connection setup only
if (!this->check_connection_setup_()) {
return; // Connection not setup
}
break;
default:
// All other messages require authentication (which includes connection check)
if (!this->check_authenticated_()) {
return; // Authentication failed
}
break;
}
// Call base implementation to process the message
APIServerConnectionBase::read_message(msg_size, msg_type, msg_data);
}
} // namespace esphome::api

View File

@@ -228,4 +228,270 @@ class APIServerConnectionBase : public ProtoService {
void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;
};
class APIServerConnection : public APIServerConnectionBase {
public:
virtual bool send_hello_response(const HelloRequest &msg) = 0;
virtual bool send_disconnect_response() = 0;
virtual bool send_ping_response() = 0;
virtual bool send_device_info_response() = 0;
virtual void list_entities() = 0;
virtual void subscribe_states() = 0;
virtual void subscribe_logs(const SubscribeLogsRequest &msg) = 0;
#ifdef USE_API_HOMEASSISTANT_SERVICES
virtual void subscribe_homeassistant_services() = 0;
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
virtual void subscribe_home_assistant_states() = 0;
#endif
#ifdef USE_API_USER_DEFINED_ACTIONS
virtual void execute_service(const ExecuteServiceRequest &msg) = 0;
#endif
#ifdef USE_API_NOISE
virtual bool send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) = 0;
#endif
#ifdef USE_BUTTON
virtual void button_command(const ButtonCommandRequest &msg) = 0;
#endif
#ifdef USE_CAMERA
virtual void camera_image(const CameraImageRequest &msg) = 0;
#endif
#ifdef USE_CLIMATE
virtual void climate_command(const ClimateCommandRequest &msg) = 0;
#endif
#ifdef USE_COVER
virtual void cover_command(const CoverCommandRequest &msg) = 0;
#endif
#ifdef USE_DATETIME_DATE
virtual void date_command(const DateCommandRequest &msg) = 0;
#endif
#ifdef USE_DATETIME_DATETIME
virtual void datetime_command(const DateTimeCommandRequest &msg) = 0;
#endif
#ifdef USE_FAN
virtual void fan_command(const FanCommandRequest &msg) = 0;
#endif
#ifdef USE_LIGHT
virtual void light_command(const LightCommandRequest &msg) = 0;
#endif
#ifdef USE_LOCK
virtual void lock_command(const LockCommandRequest &msg) = 0;
#endif
#ifdef USE_MEDIA_PLAYER
virtual void media_player_command(const MediaPlayerCommandRequest &msg) = 0;
#endif
#ifdef USE_NUMBER
virtual void number_command(const NumberCommandRequest &msg) = 0;
#endif
#ifdef USE_SELECT
virtual void select_command(const SelectCommandRequest &msg) = 0;
#endif
#ifdef USE_SIREN
virtual void siren_command(const SirenCommandRequest &msg) = 0;
#endif
#ifdef USE_SWITCH
virtual void switch_command(const SwitchCommandRequest &msg) = 0;
#endif
#ifdef USE_TEXT
virtual void text_command(const TextCommandRequest &msg) = 0;
#endif
#ifdef USE_DATETIME_TIME
virtual void time_command(const TimeCommandRequest &msg) = 0;
#endif
#ifdef USE_UPDATE
virtual void update_command(const UpdateCommandRequest &msg) = 0;
#endif
#ifdef USE_VALVE
virtual void valve_command(const ValveCommandRequest &msg) = 0;
#endif
#ifdef USE_WATER_HEATER
virtual void water_heater_command(const WaterHeaterCommandRequest &msg) = 0;
#endif
#ifdef USE_BLUETOOTH_PROXY
virtual void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) = 0;
#endif
#ifdef USE_BLUETOOTH_PROXY
virtual void bluetooth_device_request(const BluetoothDeviceRequest &msg) = 0;
#endif
#ifdef USE_BLUETOOTH_PROXY
virtual void bluetooth_gatt_get_services(const BluetoothGATTGetServicesRequest &msg) = 0;
#endif
#ifdef USE_BLUETOOTH_PROXY
virtual void bluetooth_gatt_read(const BluetoothGATTReadRequest &msg) = 0;
#endif
#ifdef USE_BLUETOOTH_PROXY
virtual void bluetooth_gatt_write(const BluetoothGATTWriteRequest &msg) = 0;
#endif
#ifdef USE_BLUETOOTH_PROXY
virtual void bluetooth_gatt_read_descriptor(const BluetoothGATTReadDescriptorRequest &msg) = 0;
#endif
#ifdef USE_BLUETOOTH_PROXY
virtual void bluetooth_gatt_write_descriptor(const BluetoothGATTWriteDescriptorRequest &msg) = 0;
#endif
#ifdef USE_BLUETOOTH_PROXY
virtual void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) = 0;
#endif
#ifdef USE_BLUETOOTH_PROXY
virtual bool send_subscribe_bluetooth_connections_free_response() = 0;
#endif
#ifdef USE_BLUETOOTH_PROXY
virtual void unsubscribe_bluetooth_le_advertisements() = 0;
#endif
#ifdef USE_BLUETOOTH_PROXY
virtual void bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) = 0;
#endif
#ifdef USE_VOICE_ASSISTANT
virtual void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) = 0;
#endif
#ifdef USE_VOICE_ASSISTANT
virtual bool send_voice_assistant_get_configuration_response(const VoiceAssistantConfigurationRequest &msg) = 0;
#endif
#ifdef USE_VOICE_ASSISTANT
virtual void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) = 0;
#endif
#ifdef USE_ALARM_CONTROL_PANEL
virtual void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) = 0;
#endif
#ifdef USE_ZWAVE_PROXY
virtual void zwave_proxy_frame(const ZWaveProxyFrame &msg) = 0;
#endif
#ifdef USE_ZWAVE_PROXY
virtual void zwave_proxy_request(const ZWaveProxyRequest &msg) = 0;
#endif
#ifdef USE_IR_RF
virtual void infrared_rf_transmit_raw_timings(const InfraredRFTransmitRawTimingsRequest &msg) = 0;
#endif
protected:
void on_hello_request(const HelloRequest &msg) override;
void on_disconnect_request() override;
void on_ping_request() override;
void on_device_info_request() override;
void on_list_entities_request() override;
void on_subscribe_states_request() override;
void on_subscribe_logs_request(const SubscribeLogsRequest &msg) override;
#ifdef USE_API_HOMEASSISTANT_SERVICES
void on_subscribe_homeassistant_services_request() override;
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
void on_subscribe_home_assistant_states_request() override;
#endif
#ifdef USE_API_USER_DEFINED_ACTIONS
void on_execute_service_request(const ExecuteServiceRequest &msg) override;
#endif
#ifdef USE_API_NOISE
void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) override;
#endif
#ifdef USE_BUTTON
void on_button_command_request(const ButtonCommandRequest &msg) override;
#endif
#ifdef USE_CAMERA
void on_camera_image_request(const CameraImageRequest &msg) override;
#endif
#ifdef USE_CLIMATE
void on_climate_command_request(const ClimateCommandRequest &msg) override;
#endif
#ifdef USE_COVER
void on_cover_command_request(const CoverCommandRequest &msg) override;
#endif
#ifdef USE_DATETIME_DATE
void on_date_command_request(const DateCommandRequest &msg) override;
#endif
#ifdef USE_DATETIME_DATETIME
void on_date_time_command_request(const DateTimeCommandRequest &msg) override;
#endif
#ifdef USE_FAN
void on_fan_command_request(const FanCommandRequest &msg) override;
#endif
#ifdef USE_LIGHT
void on_light_command_request(const LightCommandRequest &msg) override;
#endif
#ifdef USE_LOCK
void on_lock_command_request(const LockCommandRequest &msg) override;
#endif
#ifdef USE_MEDIA_PLAYER
void on_media_player_command_request(const MediaPlayerCommandRequest &msg) override;
#endif
#ifdef USE_NUMBER
void on_number_command_request(const NumberCommandRequest &msg) override;
#endif
#ifdef USE_SELECT
void on_select_command_request(const SelectCommandRequest &msg) override;
#endif
#ifdef USE_SIREN
void on_siren_command_request(const SirenCommandRequest &msg) override;
#endif
#ifdef USE_SWITCH
void on_switch_command_request(const SwitchCommandRequest &msg) override;
#endif
#ifdef USE_TEXT
void on_text_command_request(const TextCommandRequest &msg) override;
#endif
#ifdef USE_DATETIME_TIME
void on_time_command_request(const TimeCommandRequest &msg) override;
#endif
#ifdef USE_UPDATE
void on_update_command_request(const UpdateCommandRequest &msg) override;
#endif
#ifdef USE_VALVE
void on_valve_command_request(const ValveCommandRequest &msg) override;
#endif
#ifdef USE_WATER_HEATER
void on_water_heater_command_request(const WaterHeaterCommandRequest &msg) override;
#endif
#ifdef USE_BLUETOOTH_PROXY
void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
#endif
#ifdef USE_BLUETOOTH_PROXY
void on_bluetooth_device_request(const BluetoothDeviceRequest &msg) override;
#endif
#ifdef USE_BLUETOOTH_PROXY
void on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg) override;
#endif
#ifdef USE_BLUETOOTH_PROXY
void on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg) override;
#endif
#ifdef USE_BLUETOOTH_PROXY
void on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg) override;
#endif
#ifdef USE_BLUETOOTH_PROXY
void on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg) override;
#endif
#ifdef USE_BLUETOOTH_PROXY
void on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg) override;
#endif
#ifdef USE_BLUETOOTH_PROXY
void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) override;
#endif
#ifdef USE_BLUETOOTH_PROXY
void on_subscribe_bluetooth_connections_free_request() override;
#endif
#ifdef USE_BLUETOOTH_PROXY
void on_unsubscribe_bluetooth_le_advertisements_request() override;
#endif
#ifdef USE_BLUETOOTH_PROXY
void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) override;
#endif
#ifdef USE_VOICE_ASSISTANT
void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) override;
#endif
#ifdef USE_VOICE_ASSISTANT
void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) override;
#endif
#ifdef USE_VOICE_ASSISTANT
void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override;
#endif
#ifdef USE_ALARM_CONTROL_PANEL
void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) override;
#endif
#ifdef USE_ZWAVE_PROXY
void on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) override;
#endif
#ifdef USE_ZWAVE_PROXY
void on_z_wave_proxy_request(const ZWaveProxyRequest &msg) override;
#endif
#ifdef USE_IR_RF
void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) override;
#endif
void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;
};
} // namespace esphome::api

View File

@@ -117,7 +117,37 @@ void APIServer::setup() {
void APIServer::loop() {
// Accept new clients only if the socket exists and has incoming connections
if (this->socket_ && this->socket_->ready()) {
this->accept_new_connections_();
while (true) {
struct sockaddr_storage source_addr;
socklen_t addr_len = sizeof(source_addr);
auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len);
if (!sock)
break;
char peername[socket::SOCKADDR_STR_LEN];
sock->getpeername_to(peername);
// Check if we're at the connection limit
if (this->clients_.size() >= this->max_connections_) {
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, peername);
// Immediately close - socket destructor will handle cleanup
sock.reset();
continue;
}
ESP_LOGD(TAG, "Accept %s", peername);
auto *conn = new APIConnection(std::move(sock), this);
this->clients_.emplace_back(conn);
conn->start();
// First client connected - clear warning and update timestamp
if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) {
this->status_clear_warning();
this->last_connected_ = App.get_loop_component_start_time();
}
}
}
if (this->clients_.empty()) {
@@ -148,84 +178,42 @@ void APIServer::loop() {
while (client_index < this->clients_.size()) {
auto &client = this->clients_[client_index];
if (client->flags_.remove) {
// Rare case: handle disconnection (don't increment - swapped element needs processing)
this->remove_client_(client_index);
} else {
if (!client->flags_.remove) {
// Common case: process active client
client->loop();
client_index++;
}
}
}
void APIServer::remove_client_(size_t client_index) {
auto &client = this->clients_[client_index];
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
this->unregister_active_action_calls_for_connection(client.get());
#endif
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
// Save client info before closing socket and removal for the trigger
char peername_buf[socket::SOCKADDR_STR_LEN];
std::string client_name(client->get_name());
std::string client_peername(client->get_peername_to(peername_buf));
#endif
// Close socket now (was deferred from on_fatal_error to allow getpeername)
client->helper_->close();
// Swap with the last element and pop (avoids expensive vector shifts)
if (client_index < this->clients_.size() - 1) {
std::swap(this->clients_[client_index], this->clients_.back());
}
this->clients_.pop_back();
// Last client disconnected - set warning and start tracking for reboot timeout
if (this->clients_.empty() && this->reboot_timeout_ != 0) {
this->status_set_warning();
this->last_connected_ = App.get_loop_component_start_time();
}
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
// Fire trigger after client is removed so api.connected reflects the true state
this->client_disconnected_trigger_.trigger(client_name, client_peername);
#endif
}
void APIServer::accept_new_connections_() {
while (true) {
struct sockaddr_storage source_addr;
socklen_t addr_len = sizeof(source_addr);
auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len);
if (!sock)
break;
char peername[socket::SOCKADDR_STR_LEN];
sock->getpeername_to(peername);
// Check if we're at the connection limit
if (this->clients_.size() >= this->max_connections_) {
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, peername);
// Immediately close - socket destructor will handle cleanup
sock.reset();
continue;
}
ESP_LOGD(TAG, "Accept %s", peername);
// Rare case: handle disconnection
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
this->unregister_active_action_calls_for_connection(client.get());
#endif
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
auto *conn = new APIConnection(std::move(sock), this);
this->clients_.emplace_back(conn);
conn->start();
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
// Save client info before removal for the trigger
std::string client_name(client->get_name());
std::string client_peername(client->get_peername());
#endif
// First client connected - clear warning and update timestamp
if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) {
this->status_clear_warning();
// Swap with the last element and pop (avoids expensive vector shifts)
if (client_index < this->clients_.size() - 1) {
std::swap(this->clients_[client_index], this->clients_.back());
}
this->clients_.pop_back();
// Last client disconnected - set warning and start tracking for reboot timeout
if (this->clients_.empty() && this->reboot_timeout_ != 0) {
this->status_set_warning();
this->last_connected_ = App.get_loop_component_start_time();
}
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
// Fire trigger after client is removed so api.connected reflects the true state
this->client_disconnected_trigger_.trigger(client_name, client_peername);
#endif
// Don't increment client_index since we need to process the swapped element
}
}

View File

@@ -234,11 +234,6 @@ class APIServer : public Component,
#endif
protected:
// Accept incoming socket connections. Only called when socket has pending connections.
void __attribute__((noinline)) accept_new_connections_();
// Remove a disconnected client by index. Swaps with last element and pops.
void __attribute__((noinline)) remove_client_(size_t client_index);
#ifdef USE_API_NOISE
bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg,
const psk_t &active_psk, bool make_active);

View File

@@ -25,9 +25,7 @@ template<typename... X> class TemplatableStringValue : public TemplatableValue<s
private:
// Helper to convert value to string - handles the case where value is already a string
template<typename T> static std::string value_to_string(T &&val) {
return to_string(std::forward<T>(val)); // NOLINT
}
template<typename T> static std::string value_to_string(T &&val) { return to_string(std::forward<T>(val)); }
// Overloads for string types - needed because std::to_string doesn't support them
static std::string value_to_string(char *val) {
@@ -128,20 +126,6 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
this->add_kv_(this->variables_, key, std::forward<V>(value));
}
#ifdef USE_ESP8266
// On ESP8266, ESPHOME_F() returns __FlashStringHelper* (PROGMEM pointer).
// Store as const char* — populate_service_map copies from PROGMEM at play() time.
template<typename V> void add_data(const __FlashStringHelper *key, V &&value) {
this->add_kv_(this->data_, reinterpret_cast<const char *>(key), std::forward<V>(value));
}
template<typename V> void add_data_template(const __FlashStringHelper *key, V &&value) {
this->add_kv_(this->data_template_, reinterpret_cast<const char *>(key), std::forward<V>(value));
}
template<typename V> void add_variable(const __FlashStringHelper *key, V &&value) {
this->add_kv_(this->variables_, reinterpret_cast<const char *>(key), std::forward<V>(value));
}
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
template<typename T> void set_response_template(T response_template) {
this->response_template_ = response_template;
@@ -233,31 +217,7 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
Ts... x) {
dest.init(source.size());
#ifdef USE_ESP8266
// On ESP8266, keys may be in PROGMEM (from ESPHOME_F in codegen) and
// FLASH_STRING values need copying via _P functions.
// Allocate storage for all keys + all values (2 entries per source item).
// strlen_P/memcpy_P handle both RAM and PROGMEM pointers safely.
value_storage.init(source.size() * 2);
for (auto &it : source) {
auto &kv = dest.emplace_back();
// Key: copy from possible PROGMEM
{
size_t key_len = strlen_P(it.key);
value_storage.push_back(std::string(key_len, '\0'));
memcpy_P(value_storage.back().data(), it.key, key_len);
kv.key = StringRef(value_storage.back());
}
// Value: value() handles FLASH_STRING via _P functions internally
value_storage.push_back(it.value.value(x...));
kv.value = StringRef(value_storage.back());
}
#else
// On non-ESP8266, strings are directly readable from flash-mapped memory.
// Count non-static strings to allocate exact storage needed.
// Count non-static strings to allocate exact storage needed
size_t lambda_count = 0;
for (const auto &it : source) {
if (!it.value.is_static_string()) {
@@ -271,15 +231,14 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
kv.key = StringRef(it.key);
if (it.value.is_static_string()) {
// Static string — pointer directly readable, zero allocation
// Static string from YAML - zero allocation
kv.value = StringRef(it.value.get_static_string());
} else {
// Lambda evaluate and store result
// Lambda evaluation - store result, reference it
value_storage.push_back(it.value.value(x...));
kv.value = StringRef(value_storage.back());
}
}
#endif
}
APIServer *parent_;

View File

@@ -94,6 +94,7 @@ class ListEntitiesIterator : public ComponentIterator {
bool on_update(update::UpdateEntity *entity) override;
#endif
bool on_end() override;
bool completed() { return this->state_ == IteratorState::NONE; }
protected:
APIConnection *client_;

View File

@@ -88,6 +88,7 @@ class InitialStateIterator : public ComponentIterator {
#ifdef USE_UPDATE
bool on_update(update::UpdateEntity *entity) override;
#endif
bool completed() { return this->state_ == IteratorState::NONE; }
protected:
APIConnection *client_;

View File

@@ -264,9 +264,9 @@ template<typename... Ts> class APIRespondAction : public Action<Ts...> {
// Build and send JSON response
json::JsonBuilder builder;
this->json_builder_(x..., builder.root());
auto json_buf = builder.serialize();
std::string json_str = builder.serialize();
this->parent_->send_action_response(call_id, success, StringRef(error_message),
reinterpret_cast<const uint8_t *>(json_buf.data()), json_buf.size());
reinterpret_cast<const uint8_t *>(json_str.data()), json_str.size());
return;
}
#endif

View File

@@ -1,6 +1,5 @@
#pragma once
#include <algorithm>
#include <cmath>
#include <limits>
#include "abstract_aqi_calculator.h"
@@ -15,11 +14,7 @@ class AQICalculator : public AbstractAQICalculator {
float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID);
float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID);
float aqi = std::max(pm2_5_index, pm10_0_index);
if (aqi < 0.0f) {
aqi = 0.0f;
}
return static_cast<uint16_t>(std::lround(aqi));
return static_cast<uint16_t>(std::round((pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index));
}
protected:
@@ -27,27 +22,13 @@ class AQICalculator : public AbstractAQICalculator {
static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 50}, {51, 100}, {101, 150}, {151, 200}, {201, 300}, {301, 500}};
static constexpr float PM2_5_GRID[NUM_LEVELS][2] = {
// clang-format off
{0.0f, 9.1f},
{9.1f, 35.5f},
{35.5f, 55.5f},
{55.5f, 125.5f},
{125.5f, 225.5f},
{225.5f, std::numeric_limits<float>::max()}
// clang-format on
};
static constexpr float PM2_5_GRID[NUM_LEVELS][2] = {{0.0f, 9.0f}, {9.1f, 35.4f},
{35.5f, 55.4f}, {55.5f, 125.4f},
{125.5f, 225.4f}, {225.5f, std::numeric_limits<float>::max()}};
static constexpr float PM10_0_GRID[NUM_LEVELS][2] = {
// clang-format off
{0.0f, 55.0f},
{55.0f, 155.0f},
{155.0f, 255.0f},
{255.0f, 355.0f},
{355.0f, 425.0f},
{425.0f, std::numeric_limits<float>::max()}
// clang-format on
};
static constexpr float PM10_0_GRID[NUM_LEVELS][2] = {{0.0f, 54.0f}, {55.0f, 154.0f},
{155.0f, 254.0f}, {255.0f, 354.0f},
{355.0f, 424.0f}, {425.0f, std::numeric_limits<float>::max()}};
static float calculate_index(float value, const float array[NUM_LEVELS][2]) {
int grid_index = get_grid_index(value, array);
@@ -64,10 +45,7 @@ class AQICalculator : public AbstractAQICalculator {
static int get_grid_index(float value, const float array[NUM_LEVELS][2]) {
for (int i = 0; i < NUM_LEVELS; i++) {
const bool in_range =
(value >= array[i][0]) && ((i == NUM_LEVELS - 1) ? (value <= array[i][1]) // last bucket inclusive
: (value < array[i][1])); // others exclusive on hi
if (in_range) {
if (value >= array[i][0] && value <= array[i][1]) {
return i;
}
}

View File

@@ -1,6 +1,5 @@
#pragma once
#include <algorithm>
#include <cmath>
#include <limits>
#include "abstract_aqi_calculator.h"
@@ -13,11 +12,7 @@ class CAQICalculator : public AbstractAQICalculator {
float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID);
float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID);
float aqi = std::max(pm2_5_index, pm10_0_index);
if (aqi < 0.0f) {
aqi = 0.0f;
}
return static_cast<uint16_t>(std::lround(aqi));
return static_cast<uint16_t>(std::round((pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index));
}
protected:
@@ -26,24 +21,10 @@ class CAQICalculator : public AbstractAQICalculator {
static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 25}, {26, 50}, {51, 75}, {76, 100}, {101, 400}};
static constexpr float PM2_5_GRID[NUM_LEVELS][2] = {
// clang-format off
{0.0f, 15.1f},
{15.1f, 30.1f},
{30.1f, 55.1f},
{55.1f, 110.1f},
{110.1f, std::numeric_limits<float>::max()}
// clang-format on
};
{0.0f, 15.0f}, {15.1f, 30.0f}, {30.1f, 55.0f}, {55.1f, 110.0f}, {110.1f, std::numeric_limits<float>::max()}};
static constexpr float PM10_0_GRID[NUM_LEVELS][2] = {
// clang-format off
{0.0f, 25.1f},
{25.1f, 50.1f},
{50.1f, 90.1f},
{90.1f, 180.1f},
{180.1f, std::numeric_limits<float>::max()}
// clang-format on
};
{0.0f, 25.0f}, {25.1f, 50.0f}, {50.1f, 90.0f}, {90.1f, 180.0f}, {180.1f, std::numeric_limits<float>::max()}};
static float calculate_index(float value, const float array[NUM_LEVELS][2]) {
int grid_index = get_grid_index(value, array);
@@ -61,10 +42,7 @@ class CAQICalculator : public AbstractAQICalculator {
static int get_grid_index(float value, const float array[NUM_LEVELS][2]) {
for (int i = 0; i < NUM_LEVELS; i++) {
const bool in_range =
(value >= array[i][0]) && ((i == NUM_LEVELS - 1) ? (value <= array[i][1]) // last bucket inclusive
: (value < array[i][1])); // others exclusive on hi
if (in_range) {
if (value >= array[i][0] && value <= array[i][1]) {
return i;
}
}

View File

@@ -3,7 +3,6 @@
#include "bedjet_hub.h"
#include "bedjet_child.h"
#include "bedjet_const.h"
#include "esphome/components/esp32_ble/ble_uuid.h"
#include "esphome/core/application.h"
#include <cinttypes>

View File

@@ -5,14 +5,6 @@ namespace esphome::binary_sensor {
static const char *const TAG = "binary_sensor.automation";
// MultiClickTrigger timeout IDs.
// MultiClickTrigger is its own Component instance, so the scheduler scopes
// IDs by component pointer — no risk of collisions between instances.
constexpr uint32_t MULTICLICK_TRIGGER_ID = 0;
constexpr uint32_t MULTICLICK_COOLDOWN_ID = 1;
constexpr uint32_t MULTICLICK_IS_VALID_ID = 2;
constexpr uint32_t MULTICLICK_IS_NOT_VALID_ID = 3;
void MultiClickTrigger::on_state_(bool state) {
// Handle duplicate events
if (state == this->last_state_) {
@@ -35,7 +27,7 @@ void MultiClickTrigger::on_state_(bool state) {
evt.min_length, evt.max_length);
this->at_index_ = 1;
if (this->timing_.size() == 1 && evt.max_length == 4294967294UL) {
this->set_timeout(MULTICLICK_TRIGGER_ID, evt.min_length, [this]() { this->trigger_(); });
this->set_timeout("trigger", evt.min_length, [this]() { this->trigger_(); });
} else {
this->schedule_is_valid_(evt.min_length);
this->schedule_is_not_valid_(evt.max_length);
@@ -65,13 +57,13 @@ void MultiClickTrigger::on_state_(bool state) {
this->schedule_is_not_valid_(evt.max_length);
} else if (*this->at_index_ + 1 != this->timing_.size()) {
ESP_LOGV(TAG, "B i=%zu min=%" PRIu32, *this->at_index_, evt.min_length); // NOLINT
this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID);
this->cancel_timeout("is_not_valid");
this->schedule_is_valid_(evt.min_length);
} else {
ESP_LOGV(TAG, "C i=%zu min=%" PRIu32, *this->at_index_, evt.min_length); // NOLINT
this->is_valid_ = false;
this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID);
this->set_timeout(MULTICLICK_TRIGGER_ID, evt.min_length, [this]() { this->trigger_(); });
this->cancel_timeout("is_not_valid");
this->set_timeout("trigger", evt.min_length, [this]() { this->trigger_(); });
}
*this->at_index_ = *this->at_index_ + 1;
@@ -79,14 +71,14 @@ void MultiClickTrigger::on_state_(bool state) {
void MultiClickTrigger::schedule_cooldown_() {
ESP_LOGV(TAG, "Multi Click: Invalid length of press, starting cooldown of %" PRIu32 " ms", this->invalid_cooldown_);
this->is_in_cooldown_ = true;
this->set_timeout(MULTICLICK_COOLDOWN_ID, this->invalid_cooldown_, [this]() {
this->set_timeout("cooldown", this->invalid_cooldown_, [this]() {
ESP_LOGV(TAG, "Multi Click: Cooldown ended, matching is now enabled again.");
this->is_in_cooldown_ = false;
});
this->at_index_.reset();
this->cancel_timeout(MULTICLICK_TRIGGER_ID);
this->cancel_timeout(MULTICLICK_IS_VALID_ID);
this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID);
this->cancel_timeout("trigger");
this->cancel_timeout("is_valid");
this->cancel_timeout("is_not_valid");
}
void MultiClickTrigger::schedule_is_valid_(uint32_t min_length) {
if (min_length == 0) {
@@ -94,13 +86,13 @@ void MultiClickTrigger::schedule_is_valid_(uint32_t min_length) {
return;
}
this->is_valid_ = false;
this->set_timeout(MULTICLICK_IS_VALID_ID, min_length, [this]() {
this->set_timeout("is_valid", min_length, [this]() {
ESP_LOGV(TAG, "Multi Click: You can now %s the button.", this->parent_->state ? "RELEASE" : "PRESS");
this->is_valid_ = true;
});
}
void MultiClickTrigger::schedule_is_not_valid_(uint32_t max_length) {
this->set_timeout(MULTICLICK_IS_NOT_VALID_ID, max_length, [this]() {
this->set_timeout("is_not_valid", max_length, [this]() {
ESP_LOGV(TAG, "Multi Click: You waited too long to %s.", this->parent_->state ? "RELEASE" : "PRESS");
this->is_valid_ = false;
this->schedule_cooldown_();
@@ -114,9 +106,9 @@ void MultiClickTrigger::cancel() {
void MultiClickTrigger::trigger_() {
ESP_LOGV(TAG, "Multi Click: Hooray, multi click is valid. Triggering!");
this->at_index_.reset();
this->cancel_timeout(MULTICLICK_TRIGGER_ID);
this->cancel_timeout(MULTICLICK_IS_VALID_ID);
this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID);
this->cancel_timeout("trigger");
this->cancel_timeout("is_valid");
this->cancel_timeout("is_not_valid");
this->trigger();
}

View File

@@ -6,14 +6,6 @@ namespace esphome::binary_sensor {
static const char *const TAG = "sensor.filter";
// Timeout IDs for filter classes.
// Each filter is its own Component instance, so the scheduler scopes
// IDs by component pointer — no risk of collisions between instances.
constexpr uint32_t FILTER_TIMEOUT_ID = 0;
// AutorepeatFilter needs two distinct IDs (both timeouts on the same component)
constexpr uint32_t AUTOREPEAT_TIMING_ID = 0;
constexpr uint32_t AUTOREPEAT_ON_OFF_ID = 1;
void Filter::output(bool value) {
if (this->next_ == nullptr) {
this->parent_->send_state_internal(value);
@@ -31,16 +23,16 @@ void Filter::input(bool value) {
}
void TimeoutFilter::input(bool value) {
this->set_timeout(FILTER_TIMEOUT_ID, this->timeout_delay_.value(), [this]() { this->parent_->invalidate_state(); });
this->set_timeout("timeout", this->timeout_delay_.value(), [this]() { this->parent_->invalidate_state(); });
// we do not de-dup here otherwise changes from invalid to valid state will not be output
this->output(value);
}
optional<bool> DelayedOnOffFilter::new_value(bool value) {
if (value) {
this->set_timeout(FILTER_TIMEOUT_ID, this->on_delay_.value(), [this]() { this->output(true); });
this->set_timeout("ON_OFF", this->on_delay_.value(), [this]() { this->output(true); });
} else {
this->set_timeout(FILTER_TIMEOUT_ID, this->off_delay_.value(), [this]() { this->output(false); });
this->set_timeout("ON_OFF", this->off_delay_.value(), [this]() { this->output(false); });
}
return {};
}
@@ -49,10 +41,10 @@ float DelayedOnOffFilter::get_setup_priority() const { return setup_priority::HA
optional<bool> DelayedOnFilter::new_value(bool value) {
if (value) {
this->set_timeout(FILTER_TIMEOUT_ID, this->delay_.value(), [this]() { this->output(true); });
this->set_timeout("ON", this->delay_.value(), [this]() { this->output(true); });
return {};
} else {
this->cancel_timeout(FILTER_TIMEOUT_ID);
this->cancel_timeout("ON");
return false;
}
}
@@ -61,10 +53,10 @@ float DelayedOnFilter::get_setup_priority() const { return setup_priority::HARDW
optional<bool> DelayedOffFilter::new_value(bool value) {
if (!value) {
this->set_timeout(FILTER_TIMEOUT_ID, this->delay_.value(), [this]() { this->output(false); });
this->set_timeout("OFF", this->delay_.value(), [this]() { this->output(false); });
return {};
} else {
this->cancel_timeout(FILTER_TIMEOUT_ID);
this->cancel_timeout("OFF");
return true;
}
}
@@ -84,8 +76,8 @@ optional<bool> AutorepeatFilter::new_value(bool value) {
this->next_timing_();
return true;
} else {
this->cancel_timeout(AUTOREPEAT_TIMING_ID);
this->cancel_timeout(AUTOREPEAT_ON_OFF_ID);
this->cancel_timeout("TIMING");
this->cancel_timeout("ON_OFF");
this->active_timing_ = 0;
return false;
}
@@ -96,10 +88,8 @@ void AutorepeatFilter::next_timing_() {
// 1st time: starts waiting the first delay
// 2nd time: starts waiting the second delay and starts toggling with the first time_off / _on
// last time: no delay to start but have to bump the index to reflect the last
if (this->active_timing_ < this->timings_.size()) {
this->set_timeout(AUTOREPEAT_TIMING_ID, this->timings_[this->active_timing_].delay,
[this]() { this->next_timing_(); });
}
if (this->active_timing_ < this->timings_.size())
this->set_timeout("TIMING", this->timings_[this->active_timing_].delay, [this]() { this->next_timing_(); });
if (this->active_timing_ <= this->timings_.size()) {
this->active_timing_++;
@@ -114,8 +104,7 @@ void AutorepeatFilter::next_timing_() {
void AutorepeatFilter::next_value_(bool val) {
const AutorepeatFilterTiming &timing = this->timings_[this->active_timing_ - 2];
this->output(val); // This is at least the second one so not initial
this->set_timeout(AUTOREPEAT_ON_OFF_ID, val ? timing.time_on : timing.time_off,
[this, val]() { this->next_value_(!val); });
this->set_timeout("ON_OFF", val ? timing.time_on : timing.time_off, [this, val]() { this->next_value_(!val); });
}
float AutorepeatFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
@@ -126,7 +115,7 @@ optional<bool> LambdaFilter::new_value(bool value) { return this->f_(value); }
optional<bool> SettleFilter::new_value(bool value) {
if (!this->steady_) {
this->set_timeout(FILTER_TIMEOUT_ID, this->delay_.value(), [this, value]() {
this->set_timeout("SETTLE", this->delay_.value(), [this, value]() {
this->steady_ = true;
this->output(value);
});
@@ -134,7 +123,7 @@ optional<bool> SettleFilter::new_value(bool value) {
} else {
this->steady_ = false;
this->output(value);
this->set_timeout(FILTER_TIMEOUT_ID, this->delay_.value(), [this]() { this->steady_ = true; });
this->set_timeout("SETTLE", this->delay_.value(), [this]() { this->steady_ = true; });
return value;
}
}

View File

@@ -159,10 +159,6 @@ BK72XX_BOARD_PINS = {
"A0": 23,
},
"cbu": {
"SPI0_CS": 15,
"SPI0_MISO": 17,
"SPI0_MOSI": 16,
"SPI0_SCK": 14,
"WIRE1_SCL": 20,
"WIRE1_SDA": 21,
"WIRE2_SCL": 0,
@@ -231,10 +227,6 @@ BK72XX_BOARD_PINS = {
"A0": 23,
},
"generic-bk7231t-qfn32-tuya": {
"SPI0_CS": 15,
"SPI0_MISO": 17,
"SPI0_MOSI": 16,
"SPI0_SCK": 14,
"WIRE1_SCL": 20,
"WIRE1_SDA": 21,
"WIRE2_SCL": 0,
@@ -303,10 +295,6 @@ BK72XX_BOARD_PINS = {
"A0": 23,
},
"generic-bk7231n-qfn32-tuya": {
"SPI0_CS": 15,
"SPI0_MISO": 17,
"SPI0_MOSI": 16,
"SPI0_SCK": 14,
"WIRE1_SCL": 20,
"WIRE1_SDA": 21,
"WIRE2_SCL": 0,
@@ -497,7 +485,8 @@ BK72XX_BOARD_PINS = {
},
"cb3s": {
"WIRE1_SCL": 20,
"WIRE1_SDA": 21,
"WIRE1_SDA_0": 21,
"WIRE1_SDA_1": 21,
"SERIAL1_RX": 10,
"SERIAL1_TX": 11,
"SERIAL2_TX": 0,
@@ -658,10 +647,6 @@ BK72XX_BOARD_PINS = {
"A0": 23,
},
"generic-bk7252": {
"SPI0_CS": 15,
"SPI0_MISO": 17,
"SPI0_MOSI": 16,
"SPI0_SCK": 14,
"WIRE1_SCL": 20,
"WIRE1_SDA": 21,
"WIRE2_SCL": 0,
@@ -1111,10 +1096,6 @@ BK72XX_BOARD_PINS = {
"A0": 23,
},
"cb3se": {
"SPI0_CS": 15,
"SPI0_MISO": 17,
"SPI0_MOSI": 16,
"SPI0_SCK": 14,
"WIRE2_SCL": 0,
"WIRE2_SDA": 1,
"SERIAL1_RX": 10,

View File

@@ -46,16 +46,16 @@ static const uint32_t PKT_TIMEOUT_MS = 200;
void BL0942::loop() {
DataPacket buffer;
size_t avail = this->available();
int avail = this->available();
if (!avail) {
return;
}
if (avail < sizeof(buffer)) {
if (static_cast<size_t>(avail) < sizeof(buffer)) {
if (!this->rx_start_) {
this->rx_start_ = millis();
} else if (millis() > this->rx_start_ + PKT_TIMEOUT_MS) {
ESP_LOGW(TAG, "Junk on wire. Throwing away partial message (%zu bytes)", avail);
ESP_LOGW(TAG, "Junk on wire. Throwing away partial message (%d bytes)", avail);
this->read_array((uint8_t *) &buffer, avail);
this->rx_start_ = 0;
}

View File

@@ -6,9 +6,8 @@
*/
#include "bmp3xx_base.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include "esphome/core/hal.h"
#include <cinttypes>
namespace esphome {
@@ -27,18 +26,46 @@ static const LogString *chip_type_to_str(uint8_t chip_type) {
}
}
// Oversampling strings indexed by Oversampling enum (0-5): NONE, X2, X4, X8, X16, X32
PROGMEM_STRING_TABLE(OversamplingStrings, "None", "2x", "4x", "8x", "16x", "32x", "");
static const LogString *oversampling_to_str(Oversampling oversampling) {
return OversamplingStrings::get_log_str(static_cast<uint8_t>(oversampling), OversamplingStrings::LAST_INDEX);
switch (oversampling) {
case Oversampling::OVERSAMPLING_NONE:
return LOG_STR("None");
case Oversampling::OVERSAMPLING_X2:
return LOG_STR("2x");
case Oversampling::OVERSAMPLING_X4:
return LOG_STR("4x");
case Oversampling::OVERSAMPLING_X8:
return LOG_STR("8x");
case Oversampling::OVERSAMPLING_X16:
return LOG_STR("16x");
case Oversampling::OVERSAMPLING_X32:
return LOG_STR("32x");
default:
return LOG_STR("");
}
}
// IIR filter strings indexed by IIRFilter enum (0-7): OFF, 2, 4, 8, 16, 32, 64, 128
PROGMEM_STRING_TABLE(IIRFilterStrings, "OFF", "2x", "4x", "8x", "16x", "32x", "64x", "128x", "");
static const LogString *iir_filter_to_str(IIRFilter filter) {
return IIRFilterStrings::get_log_str(static_cast<uint8_t>(filter), IIRFilterStrings::LAST_INDEX);
switch (filter) {
case IIRFilter::IIR_FILTER_OFF:
return LOG_STR("OFF");
case IIRFilter::IIR_FILTER_2:
return LOG_STR("2x");
case IIRFilter::IIR_FILTER_4:
return LOG_STR("4x");
case IIRFilter::IIR_FILTER_8:
return LOG_STR("8x");
case IIRFilter::IIR_FILTER_16:
return LOG_STR("16x");
case IIRFilter::IIR_FILTER_32:
return LOG_STR("32x");
case IIRFilter::IIR_FILTER_64:
return LOG_STR("64x");
case IIRFilter::IIR_FILTER_128:
return LOG_STR("128x");
default:
return LOG_STR("");
}
}
void BMP3XXComponent::setup() {

View File

@@ -11,26 +11,57 @@
*/
#include "bmp581_base.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include "esphome/core/hal.h"
namespace esphome::bmp581_base {
static const char *const TAG = "bmp581";
// Oversampling strings indexed by Oversampling enum (0-7): NONE, X2, X4, X8, X16, X32, X64, X128
PROGMEM_STRING_TABLE(OversamplingStrings, "None", "2x", "4x", "8x", "16x", "32x", "64x", "128x", "");
static const LogString *oversampling_to_str(Oversampling oversampling) {
return OversamplingStrings::get_log_str(static_cast<uint8_t>(oversampling), OversamplingStrings::LAST_INDEX);
switch (oversampling) {
case Oversampling::OVERSAMPLING_NONE:
return LOG_STR("None");
case Oversampling::OVERSAMPLING_X2:
return LOG_STR("2x");
case Oversampling::OVERSAMPLING_X4:
return LOG_STR("4x");
case Oversampling::OVERSAMPLING_X8:
return LOG_STR("8x");
case Oversampling::OVERSAMPLING_X16:
return LOG_STR("16x");
case Oversampling::OVERSAMPLING_X32:
return LOG_STR("32x");
case Oversampling::OVERSAMPLING_X64:
return LOG_STR("64x");
case Oversampling::OVERSAMPLING_X128:
return LOG_STR("128x");
default:
return LOG_STR("");
}
}
// IIR filter strings indexed by IIRFilter enum (0-7): OFF, 2, 4, 8, 16, 32, 64, 128
PROGMEM_STRING_TABLE(IIRFilterStrings, "OFF", "2x", "4x", "8x", "16x", "32x", "64x", "128x", "");
static const LogString *iir_filter_to_str(IIRFilter filter) {
return IIRFilterStrings::get_log_str(static_cast<uint8_t>(filter), IIRFilterStrings::LAST_INDEX);
switch (filter) {
case IIRFilter::IIR_FILTER_OFF:
return LOG_STR("OFF");
case IIRFilter::IIR_FILTER_2:
return LOG_STR("2x");
case IIRFilter::IIR_FILTER_4:
return LOG_STR("4x");
case IIRFilter::IIR_FILTER_8:
return LOG_STR("8x");
case IIRFilter::IIR_FILTER_16:
return LOG_STR("16x");
case IIRFilter::IIR_FILTER_32:
return LOG_STR("32x");
case IIRFilter::IIR_FILTER_64:
return LOG_STR("64x");
case IIRFilter::IIR_FILTER_128:
return LOG_STR("128x");
default:
return LOG_STR("");
}
}
void BMP581Component::dump_config() {

View File

@@ -7,6 +7,7 @@ namespace esphome {
namespace cse7766 {
static const char *const TAG = "cse7766";
static constexpr size_t CSE7766_RAW_DATA_SIZE = 24;
void CSE7766Component::loop() {
const uint32_t now = App.get_loop_component_start_time();
@@ -15,39 +16,25 @@ void CSE7766Component::loop() {
this->raw_data_index_ = 0;
}
// Early return prevents updating last_transmission_ when no data is available.
size_t avail = this->available();
if (avail == 0) {
if (this->available() == 0) {
return;
}
this->last_transmission_ = now;
// Read all available bytes in batches to reduce UART call overhead.
// At 4800 baud (~480 bytes/sec) with ~122 Hz loop rate, typically ~4 bytes per call.
uint8_t buf[CSE7766_RAW_DATA_SIZE];
while (avail > 0) {
size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
while (this->available() != 0) {
this->read_byte(&this->raw_data_[this->raw_data_index_]);
if (!this->check_byte_()) {
this->raw_data_index_ = 0;
this->status_set_warning();
continue;
}
avail -= to_read;
for (size_t i = 0; i < to_read; i++) {
this->raw_data_[this->raw_data_index_] = buf[i];
if (!this->check_byte_()) {
this->raw_data_index_ = 0;
this->status_set_warning();
continue;
}
if (this->raw_data_index_ == CSE7766_RAW_DATA_SIZE - 1) {
this->parse_data_();
this->status_clear_warning();
}
this->raw_data_index_ = (this->raw_data_index_ + 1) % CSE7766_RAW_DATA_SIZE;
if (this->raw_data_index_ == 23) {
this->parse_data_();
this->status_clear_warning();
}
this->raw_data_index_ = (this->raw_data_index_ + 1) % 24;
}
}
@@ -66,15 +53,14 @@ bool CSE7766Component::check_byte_() {
return true;
}
if (index == CSE7766_RAW_DATA_SIZE - 1) {
if (index == 23) {
uint8_t checksum = 0;
for (uint8_t i = 2; i < CSE7766_RAW_DATA_SIZE - 1; i++) {
for (uint8_t i = 2; i < 23; i++) {
checksum += this->raw_data_[i];
}
if (checksum != this->raw_data_[CSE7766_RAW_DATA_SIZE - 1]) {
ESP_LOGW(TAG, "Invalid checksum from CSE7766: 0x%02X != 0x%02X", checksum,
this->raw_data_[CSE7766_RAW_DATA_SIZE - 1]);
if (checksum != this->raw_data_[23]) {
ESP_LOGW(TAG, "Invalid checksum from CSE7766: 0x%02X != 0x%02X", checksum, this->raw_data_[23]);
return false;
}
return true;

View File

@@ -8,8 +8,6 @@
namespace esphome {
namespace cse7766 {
static constexpr size_t CSE7766_RAW_DATA_SIZE = 24;
class CSE7766Component : public Component, public uart::UARTDevice {
public:
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; }
@@ -35,7 +33,7 @@ class CSE7766Component : public Component, public uart::UARTDevice {
this->raw_data_[start_index + 2]);
}
uint8_t raw_data_[CSE7766_RAW_DATA_SIZE];
uint8_t raw_data_[24];
uint8_t raw_data_index_{0};
uint32_t last_transmission_{0};
sensor::Sensor *voltage_sensor_{nullptr};

View File

@@ -1,7 +1,6 @@
#include "debug_component.h"
#ifdef USE_ESP8266
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include <Esp.h>
extern "C" {
@@ -20,31 +19,27 @@ namespace debug {
static const char *const TAG = "debug";
// PROGMEM string table for reset reasons, indexed by reason code (0-6), with "Unknown" as fallback
// clang-format off
PROGMEM_STRING_TABLE(ResetReasonStrings,
"Power On", // 0 = REASON_DEFAULT_RST
"Hardware Watchdog", // 1 = REASON_WDT_RST
"Exception", // 2 = REASON_EXCEPTION_RST
"Software Watchdog", // 3 = REASON_SOFT_WDT_RST
"Software/System restart", // 4 = REASON_SOFT_RESTART
"Deep-Sleep Wake", // 5 = REASON_DEEP_SLEEP_AWAKE
"External System", // 6 = REASON_EXT_SYS_RST
"Unknown" // 7 = fallback
);
// clang-format on
static_assert(REASON_DEFAULT_RST == 0, "Reset reason enum values must match table indices");
static_assert(REASON_EXT_SYS_RST == 6, "Reset reason enum values must match table indices");
// PROGMEM string table for flash chip modes, indexed by mode code (0-3), with "UNKNOWN" as fallback
PROGMEM_STRING_TABLE(FlashModeStrings, "QIO", "QOUT", "DIO", "DOUT", "UNKNOWN");
static_assert(FM_QIO == 0, "Flash mode enum values must match table indices");
static_assert(FM_DOUT == 3, "Flash mode enum values must match table indices");
// Get reset reason string from reason code (no heap allocation)
// Returns LogString* pointing to flash (PROGMEM) on ESP8266
static const LogString *get_reset_reason_str(uint32_t reason) {
return ResetReasonStrings::get_log_str(reason, ResetReasonStrings::LAST_INDEX);
switch (reason) {
case REASON_DEFAULT_RST:
return LOG_STR("Power On");
case REASON_WDT_RST:
return LOG_STR("Hardware Watchdog");
case REASON_EXCEPTION_RST:
return LOG_STR("Exception");
case REASON_SOFT_WDT_RST:
return LOG_STR("Software Watchdog");
case REASON_SOFT_RESTART:
return LOG_STR("Software/System restart");
case REASON_DEEP_SLEEP_AWAKE:
return LOG_STR("Deep-Sleep Wake");
case REASON_EXT_SYS_RST:
return LOG_STR("External System");
default:
return LOG_STR("Unknown");
}
}
// Size for core version hex buffer
@@ -97,8 +92,23 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
constexpr size_t size = DEVICE_INFO_BUFFER_SIZE;
char *buf = buffer.data();
// NOLINTNEXTLINE(readability-static-accessed-through-instance)
const LogString *flash_mode = FlashModeStrings::get_log_str(ESP.getFlashChipMode(), FlashModeStrings::LAST_INDEX);
const LogString *flash_mode;
switch (ESP.getFlashChipMode()) { // NOLINT(readability-static-accessed-through-instance)
case FM_QIO:
flash_mode = LOG_STR("QIO");
break;
case FM_QOUT:
flash_mode = LOG_STR("QOUT");
break;
case FM_DIO:
flash_mode = LOG_STR("DIO");
break;
case FM_DOUT:
flash_mode = LOG_STR("DOUT");
break;
default:
flash_mode = LOG_STR("UNKNOWN");
}
uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT(readability-static-accessed-through-instance)
uint32_t flash_speed = ESP.getFlashChipSpeed() / 1000000; // NOLINT(readability-static-accessed-through-instance)
ESP_LOGD(TAG, "Flash Chip: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed,

View File

@@ -1,5 +1,4 @@
#include "dfplayer.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
@@ -132,149 +131,140 @@ void DFPlayer::send_cmd_(uint8_t cmd, uint16_t argument) {
}
void DFPlayer::loop() {
// Read all available bytes in batches to reduce UART call overhead.
size_t avail = this->available();
uint8_t buf[64];
while (avail > 0) {
size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}
avail -= to_read;
for (size_t bi = 0; bi < to_read; bi++) {
uint8_t byte = buf[bi];
// Read message
while (this->available()) {
uint8_t byte;
this->read_byte(&byte);
if (this->read_pos_ == DFPLAYER_READ_BUFFER_LENGTH)
this->read_pos_ = 0;
if (this->read_pos_ == DFPLAYER_READ_BUFFER_LENGTH)
this->read_pos_ = 0;
switch (this->read_pos_) {
case 0: // Start mark
if (byte != 0x7E)
continue;
break;
case 1: // Version
if (byte != 0xFF) {
ESP_LOGW(TAG, "Expected Version 0xFF, got %#02x", byte);
this->read_pos_ = 0;
continue;
}
break;
case 2: // Buffer length
if (byte != 0x06) {
ESP_LOGW(TAG, "Expected Buffer length 0x06, got %#02x", byte);
this->read_pos_ = 0;
continue;
}
break;
case 9: // End byte
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
char byte_sequence[100];
byte_sequence[0] = '\0';
for (size_t i = 0; i < this->read_pos_ + 1; ++i) {
snprintf(byte_sequence + strlen(byte_sequence), sizeof(byte_sequence) - strlen(byte_sequence), "%02X ",
this->read_buffer_[i]);
}
ESP_LOGVV(TAG, "Received byte sequence: %s", byte_sequence);
#endif
if (byte != 0xEF) {
ESP_LOGW(TAG, "Expected end byte 0xEF, got %#02x", byte);
this->read_pos_ = 0;
continue;
}
// Parse valid received command
uint8_t cmd = this->read_buffer_[3];
uint16_t argument = (this->read_buffer_[5] << 8) | this->read_buffer_[6];
ESP_LOGV(TAG, "Received message cmd: %#02x arg %#04x", cmd, argument);
switch (cmd) {
case 0x3A:
if (argument == 1) {
ESP_LOGI(TAG, "USB loaded");
} else if (argument == 2) {
ESP_LOGI(TAG, "TF Card loaded");
}
break;
case 0x3B:
if (argument == 1) {
ESP_LOGI(TAG, "USB unloaded");
} else if (argument == 2) {
ESP_LOGI(TAG, "TF Card unloaded");
}
break;
case 0x3F:
if (argument == 1) {
ESP_LOGI(TAG, "USB available");
} else if (argument == 2) {
ESP_LOGI(TAG, "TF Card available");
} else if (argument == 3) {
ESP_LOGI(TAG, "USB, TF Card available");
}
break;
case 0x40:
ESP_LOGV(TAG, "Nack");
this->ack_set_is_playing_ = false;
this->ack_reset_is_playing_ = false;
switch (argument) {
case 0x01:
ESP_LOGE(TAG, "Module is busy or uninitialized");
break;
case 0x02:
ESP_LOGE(TAG, "Module is in sleep mode");
break;
case 0x03:
ESP_LOGE(TAG, "Serial receive error");
break;
case 0x04:
ESP_LOGE(TAG, "Checksum incorrect");
break;
case 0x05:
ESP_LOGE(TAG, "Specified track is out of current track scope");
this->is_playing_ = false;
break;
case 0x06:
ESP_LOGE(TAG, "Specified track is not found");
this->is_playing_ = false;
break;
case 0x07:
ESP_LOGE(TAG,
"Insertion error (an inserting operation only can be done when a track is being played)");
break;
case 0x08:
ESP_LOGE(TAG, "SD card reading failed (SD card pulled out or damaged)");
break;
case 0x09:
ESP_LOGE(TAG, "Entered into sleep mode");
this->is_playing_ = false;
break;
}
break;
case 0x41:
ESP_LOGV(TAG, "Ack ok");
this->is_playing_ |= this->ack_set_is_playing_;
this->is_playing_ &= !this->ack_reset_is_playing_;
this->ack_set_is_playing_ = false;
this->ack_reset_is_playing_ = false;
break;
case 0x3C:
ESP_LOGV(TAG, "Playback finished (USB drive)");
this->is_playing_ = false;
this->on_finished_playback_callback_.call();
case 0x3D:
ESP_LOGV(TAG, "Playback finished (SD card)");
this->is_playing_ = false;
this->on_finished_playback_callback_.call();
break;
default:
ESP_LOGE(TAG, "Received unknown cmd %#02x arg %#04x", cmd, argument);
}
this->sent_cmd_ = 0;
switch (this->read_pos_) {
case 0: // Start mark
if (byte != 0x7E)
continue;
break;
case 1: // Version
if (byte != 0xFF) {
ESP_LOGW(TAG, "Expected Version 0xFF, got %#02x", byte);
this->read_pos_ = 0;
continue;
}
this->read_buffer_[this->read_pos_] = byte;
this->read_pos_++;
}
break;
case 2: // Buffer length
if (byte != 0x06) {
ESP_LOGW(TAG, "Expected Buffer length 0x06, got %#02x", byte);
this->read_pos_ = 0;
continue;
}
break;
case 9: // End byte
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
char byte_sequence[100];
byte_sequence[0] = '\0';
for (size_t i = 0; i < this->read_pos_ + 1; ++i) {
snprintf(byte_sequence + strlen(byte_sequence), sizeof(byte_sequence) - strlen(byte_sequence), "%02X ",
this->read_buffer_[i]);
}
ESP_LOGVV(TAG, "Received byte sequence: %s", byte_sequence);
#endif
if (byte != 0xEF) {
ESP_LOGW(TAG, "Expected end byte 0xEF, got %#02x", byte);
this->read_pos_ = 0;
continue;
}
// Parse valid received command
uint8_t cmd = this->read_buffer_[3];
uint16_t argument = (this->read_buffer_[5] << 8) | this->read_buffer_[6];
ESP_LOGV(TAG, "Received message cmd: %#02x arg %#04x", cmd, argument);
switch (cmd) {
case 0x3A:
if (argument == 1) {
ESP_LOGI(TAG, "USB loaded");
} else if (argument == 2) {
ESP_LOGI(TAG, "TF Card loaded");
}
break;
case 0x3B:
if (argument == 1) {
ESP_LOGI(TAG, "USB unloaded");
} else if (argument == 2) {
ESP_LOGI(TAG, "TF Card unloaded");
}
break;
case 0x3F:
if (argument == 1) {
ESP_LOGI(TAG, "USB available");
} else if (argument == 2) {
ESP_LOGI(TAG, "TF Card available");
} else if (argument == 3) {
ESP_LOGI(TAG, "USB, TF Card available");
}
break;
case 0x40:
ESP_LOGV(TAG, "Nack");
this->ack_set_is_playing_ = false;
this->ack_reset_is_playing_ = false;
switch (argument) {
case 0x01:
ESP_LOGE(TAG, "Module is busy or uninitialized");
break;
case 0x02:
ESP_LOGE(TAG, "Module is in sleep mode");
break;
case 0x03:
ESP_LOGE(TAG, "Serial receive error");
break;
case 0x04:
ESP_LOGE(TAG, "Checksum incorrect");
break;
case 0x05:
ESP_LOGE(TAG, "Specified track is out of current track scope");
this->is_playing_ = false;
break;
case 0x06:
ESP_LOGE(TAG, "Specified track is not found");
this->is_playing_ = false;
break;
case 0x07:
ESP_LOGE(TAG, "Insertion error (an inserting operation only can be done when a track is being played)");
break;
case 0x08:
ESP_LOGE(TAG, "SD card reading failed (SD card pulled out or damaged)");
break;
case 0x09:
ESP_LOGE(TAG, "Entered into sleep mode");
this->is_playing_ = false;
break;
}
break;
case 0x41:
ESP_LOGV(TAG, "Ack ok");
this->is_playing_ |= this->ack_set_is_playing_;
this->is_playing_ &= !this->ack_reset_is_playing_;
this->ack_set_is_playing_ = false;
this->ack_reset_is_playing_ = false;
break;
case 0x3C:
ESP_LOGV(TAG, "Playback finished (USB drive)");
this->is_playing_ = false;
this->on_finished_playback_callback_.call();
case 0x3D:
ESP_LOGV(TAG, "Playback finished (SD card)");
this->is_playing_ = false;
this->on_finished_playback_callback_.call();
break;
default:
ESP_LOGE(TAG, "Received unknown cmd %#02x arg %#04x", cmd, argument);
}
this->sent_cmd_ = 0;
this->read_pos_ = 0;
continue;
}
this->read_buffer_[this->read_pos_] = byte;
this->read_pos_++;
}
}
void DFPlayer::dump_config() {

View File

@@ -63,13 +63,11 @@ def validate_auto_clear(value):
return cv.boolean(value)
def basic_display_schema(default_update_interval: str = "1s") -> cv.Schema:
"""Create a basic display schema with configurable default update interval."""
return cv.Schema(
{
cv.Exclusive(CONF_LAMBDA, CONF_LAMBDA): cv.lambda_,
}
).extend(cv.polling_component_schema(default_update_interval))
BASIC_DISPLAY_SCHEMA = cv.Schema(
{
cv.Exclusive(CONF_LAMBDA, CONF_LAMBDA): cv.lambda_,
}
).extend(cv.polling_component_schema("1s"))
def _validate_test_card(config):
@@ -83,41 +81,34 @@ def _validate_test_card(config):
return config
def full_display_schema(default_update_interval: str = "1s") -> cv.Schema:
"""Create a full display schema with configurable default update interval."""
schema = basic_display_schema(default_update_interval).extend(
{
cv.Optional(CONF_ROTATION): validate_rotation,
cv.Exclusive(CONF_PAGES, CONF_LAMBDA): cv.All(
cv.ensure_list(
{
cv.GenerateID(): cv.declare_id(DisplayPage),
cv.Required(CONF_LAMBDA): cv.lambda_,
}
),
cv.Length(min=1),
),
cv.Optional(CONF_ON_PAGE_CHANGE): automation.validate_automation(
FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend(
{
cv.Optional(CONF_ROTATION): validate_rotation,
cv.Exclusive(CONF_PAGES, CONF_LAMBDA): cv.All(
cv.ensure_list(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DisplayOnPageChangeTrigger
),
cv.Optional(CONF_FROM): cv.use_id(DisplayPage),
cv.Optional(CONF_TO): cv.use_id(DisplayPage),
cv.GenerateID(): cv.declare_id(DisplayPage),
cv.Required(CONF_LAMBDA): cv.lambda_,
}
),
cv.Optional(
CONF_AUTO_CLEAR_ENABLED, default=CONF_UNSPECIFIED
): validate_auto_clear,
cv.Optional(CONF_SHOW_TEST_CARD): cv.boolean,
}
)
schema.add_extra(_validate_test_card)
return schema
BASIC_DISPLAY_SCHEMA = basic_display_schema("1s")
FULL_DISPLAY_SCHEMA = full_display_schema("1s")
cv.Length(min=1),
),
cv.Optional(CONF_ON_PAGE_CHANGE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DisplayOnPageChangeTrigger
),
cv.Optional(CONF_FROM): cv.use_id(DisplayPage),
cv.Optional(CONF_TO): cv.use_id(DisplayPage),
}
),
cv.Optional(
CONF_AUTO_CLEAR_ENABLED, default=CONF_UNSPECIFIED
): validate_auto_clear,
cv.Optional(CONF_SHOW_TEST_CARD): cv.boolean,
}
)
FULL_DISPLAY_SCHEMA.add_extra(_validate_test_card)
async def setup_display_core_(var, config):

View File

@@ -28,28 +28,15 @@ void DlmsMeterComponent::dump_config() {
void DlmsMeterComponent::loop() {
// Read while data is available, netznoe uses two frames so allow 2x max frame length
size_t avail = this->available();
if (avail > 0) {
size_t remaining = MBUS_MAX_FRAME_LENGTH * 2 - this->receive_buffer_.size();
if (remaining == 0) {
while (this->available()) {
if (this->receive_buffer_.size() >= MBUS_MAX_FRAME_LENGTH * 2) {
ESP_LOGW(TAG, "Receive buffer full, dropping remaining bytes");
} else {
// Read all available bytes in batches to reduce UART call overhead.
// Cap reads to remaining buffer capacity.
if (avail > remaining) {
avail = remaining;
}
uint8_t buf[64];
while (avail > 0) {
size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}
avail -= to_read;
this->receive_buffer_.insert(this->receive_buffer_.end(), buf, buf + to_read);
this->last_read_ = millis();
}
break;
}
uint8_t c;
this->read_byte(&c);
this->receive_buffer_.push_back(c);
this->last_read_ = millis();
}
if (!this->receive_buffer_.empty() && millis() - this->last_read_ > this->read_timeout_) {

View File

@@ -40,7 +40,9 @@ bool Dsmr::ready_to_request_data_() {
this->start_requesting_data_();
}
if (!this->requesting_data_) {
this->drain_rx_buffer_();
while (this->available()) {
this->read();
}
}
}
return this->requesting_data_;
@@ -113,18 +115,10 @@ void Dsmr::stop_requesting_data_() {
} else {
ESP_LOGV(TAG, "Stop reading data from P1 port");
}
this->drain_rx_buffer_();
this->requesting_data_ = false;
}
}
void Dsmr::drain_rx_buffer_() {
uint8_t buf[64];
size_t avail;
while ((avail = this->available()) > 0) {
if (!this->read_array(buf, std::min(avail, sizeof(buf)))) {
break;
while (this->available()) {
this->read();
}
this->requesting_data_ = false;
}
}
@@ -134,148 +128,125 @@ void Dsmr::reset_telegram_() {
this->bytes_read_ = 0;
this->crypt_bytes_read_ = 0;
this->crypt_telegram_len_ = 0;
this->last_read_time_ = 0;
}
void Dsmr::receive_telegram_() {
while (this->available_within_timeout_()) {
// Read all available bytes in batches to reduce UART call overhead.
uint8_t buf[64];
size_t avail = this->available();
while (avail > 0) {
size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read))
return;
avail -= to_read;
const char c = this->read();
for (size_t i = 0; i < to_read; i++) {
const char c = static_cast<char>(buf[i]);
// Find a new telegram header, i.e. forward slash.
if (c == '/') {
ESP_LOGV(TAG, "Header of telegram found");
this->reset_telegram_();
this->header_found_ = true;
}
if (!this->header_found_)
continue;
// Find a new telegram header, i.e. forward slash.
if (c == '/') {
ESP_LOGV(TAG, "Header of telegram found");
this->reset_telegram_();
this->header_found_ = true;
}
if (!this->header_found_)
continue;
// Check for buffer overflow.
if (this->bytes_read_ >= this->max_telegram_len_) {
this->reset_telegram_();
ESP_LOGE(TAG, "Error: telegram larger than buffer (%d bytes)", this->max_telegram_len_);
return;
}
// Check for buffer overflow.
if (this->bytes_read_ >= this->max_telegram_len_) {
this->reset_telegram_();
ESP_LOGE(TAG, "Error: telegram larger than buffer (%d bytes)", this->max_telegram_len_);
return;
}
// Some v2.2 or v3 meters will send a new value which starts with '('
// in a new line, while the value belongs to the previous ObisId. For
// proper parsing, remove these new line characters.
if (c == '(') {
while (true) {
auto previous_char = this->telegram_[this->bytes_read_ - 1];
if (previous_char == '\n' || previous_char == '\r') {
this->bytes_read_--;
} else {
break;
}
}
}
// Store the byte in the buffer.
this->telegram_[this->bytes_read_] = c;
this->bytes_read_++;
// Check for a footer, i.e. exclamation mark, followed by a hex checksum.
if (c == '!') {
ESP_LOGV(TAG, "Footer of telegram found");
this->footer_found_ = true;
continue;
}
// Check for the end of the hex checksum, i.e. a newline.
if (this->footer_found_ && c == '\n') {
// Parse the telegram and publish sensor values.
this->parse_telegram();
this->reset_telegram_();
return;
// Some v2.2 or v3 meters will send a new value which starts with '('
// in a new line, while the value belongs to the previous ObisId. For
// proper parsing, remove these new line characters.
if (c == '(') {
while (true) {
auto previous_char = this->telegram_[this->bytes_read_ - 1];
if (previous_char == '\n' || previous_char == '\r') {
this->bytes_read_--;
} else {
break;
}
}
}
// Store the byte in the buffer.
this->telegram_[this->bytes_read_] = c;
this->bytes_read_++;
// Check for a footer, i.e. exclamation mark, followed by a hex checksum.
if (c == '!') {
ESP_LOGV(TAG, "Footer of telegram found");
this->footer_found_ = true;
continue;
}
// Check for the end of the hex checksum, i.e. a newline.
if (this->footer_found_ && c == '\n') {
// Parse the telegram and publish sensor values.
this->parse_telegram();
this->reset_telegram_();
return;
}
}
}
void Dsmr::receive_encrypted_telegram_() {
while (this->available_within_timeout_()) {
// Read all available bytes in batches to reduce UART call overhead.
uint8_t buf[64];
size_t avail = this->available();
while (avail > 0) {
size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read))
return;
avail -= to_read;
const char c = this->read();
for (size_t i = 0; i < to_read; i++) {
const char c = static_cast<char>(buf[i]);
// Find a new telegram start byte.
if (!this->header_found_) {
if ((uint8_t) c != 0xDB) {
continue;
}
ESP_LOGV(TAG, "Start byte 0xDB of encrypted telegram found");
this->reset_telegram_();
this->header_found_ = true;
}
// Check for buffer overflow.
if (this->crypt_bytes_read_ >= this->max_telegram_len_) {
this->reset_telegram_();
ESP_LOGE(TAG, "Error: encrypted telegram larger than buffer (%d bytes)", this->max_telegram_len_);
return;
}
// Store the byte in the buffer.
this->crypt_telegram_[this->crypt_bytes_read_] = c;
this->crypt_bytes_read_++;
// Read the length of the incoming encrypted telegram.
if (this->crypt_telegram_len_ == 0 && this->crypt_bytes_read_ > 20) {
// Complete header + data bytes
this->crypt_telegram_len_ = 13 + (this->crypt_telegram_[11] << 8 | this->crypt_telegram_[12]);
ESP_LOGV(TAG, "Encrypted telegram length: %d bytes", this->crypt_telegram_len_);
}
// Check for the end of the encrypted telegram.
if (this->crypt_telegram_len_ == 0 || this->crypt_bytes_read_ != this->crypt_telegram_len_) {
continue;
}
ESP_LOGV(TAG, "End of encrypted telegram found");
// Decrypt the encrypted telegram.
GCM<AES128> *gcmaes128{new GCM<AES128>()};
gcmaes128->setKey(this->decryption_key_.data(), gcmaes128->keySize());
// the iv is 8 bytes of the system title + 4 bytes frame counter
// system title is at byte 2 and frame counter at byte 15
for (int i = 10; i < 14; i++)
this->crypt_telegram_[i] = this->crypt_telegram_[i + 4];
constexpr uint16_t iv_size{12};
gcmaes128->setIV(&this->crypt_telegram_[2], iv_size);
gcmaes128->decrypt(reinterpret_cast<uint8_t *>(this->telegram_),
// the ciphertext start at byte 18
&this->crypt_telegram_[18],
// cipher size
this->crypt_bytes_read_ - 17);
delete gcmaes128; // NOLINT(cppcoreguidelines-owning-memory)
this->bytes_read_ = strnlen(this->telegram_, this->max_telegram_len_);
ESP_LOGV(TAG, "Decrypted telegram size: %d bytes", this->bytes_read_);
ESP_LOGVV(TAG, "Decrypted telegram: %s", this->telegram_);
// Parse the decrypted telegram and publish sensor values.
this->parse_telegram();
this->reset_telegram_();
return;
// Find a new telegram start byte.
if (!this->header_found_) {
if ((uint8_t) c != 0xDB) {
continue;
}
ESP_LOGV(TAG, "Start byte 0xDB of encrypted telegram found");
this->reset_telegram_();
this->header_found_ = true;
}
// Check for buffer overflow.
if (this->crypt_bytes_read_ >= this->max_telegram_len_) {
this->reset_telegram_();
ESP_LOGE(TAG, "Error: encrypted telegram larger than buffer (%d bytes)", this->max_telegram_len_);
return;
}
// Store the byte in the buffer.
this->crypt_telegram_[this->crypt_bytes_read_] = c;
this->crypt_bytes_read_++;
// Read the length of the incoming encrypted telegram.
if (this->crypt_telegram_len_ == 0 && this->crypt_bytes_read_ > 20) {
// Complete header + data bytes
this->crypt_telegram_len_ = 13 + (this->crypt_telegram_[11] << 8 | this->crypt_telegram_[12]);
ESP_LOGV(TAG, "Encrypted telegram length: %d bytes", this->crypt_telegram_len_);
}
// Check for the end of the encrypted telegram.
if (this->crypt_telegram_len_ == 0 || this->crypt_bytes_read_ != this->crypt_telegram_len_) {
continue;
}
ESP_LOGV(TAG, "End of encrypted telegram found");
// Decrypt the encrypted telegram.
GCM<AES128> *gcmaes128{new GCM<AES128>()};
gcmaes128->setKey(this->decryption_key_.data(), gcmaes128->keySize());
// the iv is 8 bytes of the system title + 4 bytes frame counter
// system title is at byte 2 and frame counter at byte 15
for (int i = 10; i < 14; i++)
this->crypt_telegram_[i] = this->crypt_telegram_[i + 4];
constexpr uint16_t iv_size{12};
gcmaes128->setIV(&this->crypt_telegram_[2], iv_size);
gcmaes128->decrypt(reinterpret_cast<uint8_t *>(this->telegram_),
// the ciphertext start at byte 18
&this->crypt_telegram_[18],
// cipher size
this->crypt_bytes_read_ - 17);
delete gcmaes128; // NOLINT(cppcoreguidelines-owning-memory)
this->bytes_read_ = strnlen(this->telegram_, this->max_telegram_len_);
ESP_LOGV(TAG, "Decrypted telegram size: %d bytes", this->bytes_read_);
ESP_LOGVV(TAG, "Decrypted telegram: %s", this->telegram_);
// Parse the decrypted telegram and publish sensor values.
this->parse_telegram();
this->reset_telegram_();
return;
}
}

View File

@@ -85,7 +85,6 @@ class Dsmr : public Component, public uart::UARTDevice {
void receive_telegram_();
void receive_encrypted_telegram_();
void reset_telegram_();
void drain_rx_buffer_();
/// Wait for UART data to become available within the read timeout.
///

View File

@@ -55,6 +55,7 @@ void E131Component::setup() {
}
void E131Component::loop() {
std::vector<uint8_t> payload;
E131Packet packet;
int universe = 0;
uint8_t buf[1460];
@@ -63,9 +64,11 @@ void E131Component::loop() {
if (len == -1) {
return;
}
payload.resize(len);
memmove(&payload[0], buf, len);
if (!this->packet_(buf, (size_t) len, universe, packet)) {
ESP_LOGV(TAG, "Invalid packet received of size %zd.", len);
if (!this->packet_(payload, universe, packet)) {
ESP_LOGV(TAG, "Invalid packet received of size %zu.", payload.size());
return;
}

View File

@@ -38,7 +38,7 @@ class E131Component : public esphome::Component {
void set_method(E131ListenMethod listen_method) { this->listen_method_ = listen_method; }
protected:
bool packet_(const uint8_t *data, size_t len, int &universe, E131Packet &packet);
bool packet_(const std::vector<uint8_t> &data, int &universe, E131Packet &packet);
bool process_(int universe, const E131Packet &packet);
bool join_igmp_groups_();
void join_(int universe);

View File

@@ -116,11 +116,11 @@ void E131Component::leave_(int universe) {
ESP_LOGD(TAG, "Left %d universe for E1.31.", universe);
}
bool E131Component::packet_(const uint8_t *data, size_t len, int &universe, E131Packet &packet) {
if (len < E131_MIN_PACKET_SIZE)
bool E131Component::packet_(const std::vector<uint8_t> &data, int &universe, E131Packet &packet) {
if (data.size() < E131_MIN_PACKET_SIZE)
return false;
auto *sbuff = reinterpret_cast<const E131RawPacket *>(data);
auto *sbuff = reinterpret_cast<const E131RawPacket *>(&data[0]);
if (memcmp(sbuff->acn_id, ACN_ID, sizeof(sbuff->acn_id)) != 0)
return false;

View File

@@ -612,9 +612,13 @@ def _format_framework_arduino_version(ver: cv.Version) -> str:
return f"{ARDUINO_FRAMEWORK_PKG}@https://github.com/espressif/arduino-esp32/releases/download/{ver}/{filename}"
def _format_framework_espidf_version(ver: cv.Version, release: str) -> str:
def _format_framework_espidf_version(
ver: cv.Version, release: str | None = None
) -> str:
# format the given espidf (https://github.com/pioarduino/esp-idf/releases) version to
# a PIO platformio/framework-espidf value
if release is None:
release = ESP_IDF_RELEASE_LOOKUP.get(ver)
if ver == cv.Version(5, 4, 3) or ver >= cv.Version(5, 5, 1):
ext = "tar.xz"
else:
@@ -689,6 +693,9 @@ ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
"latest": cv.Version(5, 5, 2),
"dev": cv.Version(5, 5, 2),
}
ESP_IDF_RELEASE_LOOKUP: dict[cv.Version, str] = {
cv.Version(5, 5, 2): "260206",
}
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
cv.Version(5, 5, 2): cv.Version(55, 3, 36),
cv.Version(5, 5, 1): cv.Version(55, 3, 31, "2"),
@@ -750,9 +757,10 @@ def _check_versions(config):
raise cv.Invalid("Only ESP-IDF 5.0+ is supported.")
recommended_version = ESP_IDF_FRAMEWORK_VERSION_LOOKUP["recommended"]
platform_lookup = ESP_IDF_PLATFORM_VERSION_LOOKUP.get(version)
release = value.get(CONF_RELEASE, ESP_IDF_RELEASE_LOOKUP.get(version))
value[CONF_SOURCE] = value.get(
CONF_SOURCE,
_format_framework_espidf_version(version, value.get(CONF_RELEASE, None)),
_format_framework_espidf_version(version, release),
)
if _is_framework_url(value[CONF_SOURCE]):
value[CONF_SOURCE] = f"pioarduino/framework-espidf@{value[CONF_SOURCE]}"
@@ -1435,10 +1443,6 @@ async def to_code(config):
CORE.relative_internal_path(".espressif")
)
# Set the uv cache inside the data dir so "Clean All" clears it.
# Avoids persistent corrupted cache from mid-stream download failures.
os.environ["UV_CACHE_DIR"] = str(CORE.relative_internal_path(".uv_cache"))
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
cg.add_build_flag("-DUSE_ESP_IDF")
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF")
@@ -1468,7 +1472,7 @@ async def to_code(config):
if (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is not None:
cg.add_platformio_option(
"platform_packages",
[_format_framework_espidf_version(idf_ver, None)],
[_format_framework_espidf_version(idf_ver)],
)
# Use stub package to skip downloading precompiled libs
stubs_dir = CORE.relative_build_path("arduino_libs_stub")
@@ -1512,6 +1516,14 @@ async def to_code(config):
f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True
)
# ESP32-P4: The ESP-IDF 5.5.2.260206 release changed the default of
# ESP32P4_SELECTS_REV_LESS_V3 from y to n. PlatformIO always uses
# sections.ld.in (for rev <3) rather than sections.rev3.ld.in (for rev >=3),
# causing a linker script mismatch with the generated memory.ld.
# Restore the previous default until PlatformIO handles this properly.
if variant == VARIANT_ESP32P4:
add_idf_sdkconfig_option("CONFIG_ESP32P4_SELECTS_REV_LESS_V3", True)
# Set minimum chip revision for ESP32 variant
# Setting this to 3.0 or higher reduces flash size by excluding workaround code,
# and for PSRAM users saves significant IRAM by keeping C library functions in ROM.

View File

@@ -85,6 +85,7 @@ void ESP32InternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpi
break;
}
gpio_set_intr_type(this->get_pin_num(), idf_type);
gpio_intr_enable(this->get_pin_num());
if (!isr_service_installed) {
auto res = gpio_install_isr_service(ESP_INTR_FLAG_LEVEL3);
if (res != ESP_OK) {
@@ -94,7 +95,6 @@ void ESP32InternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpi
isr_service_installed = true;
}
gpio_isr_handler_add(this->get_pin_num(), func, arg);
gpio_intr_enable(this->get_pin_num());
}
size_t ESP32InternalGPIOPin::dump_summary(char *buffer, size_t len) const {

View File

@@ -19,7 +19,16 @@ 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.)
std::unique_ptr<uint8_t[]> data;
size_t len;
void set_data(const uint8_t *src, size_t size) {
if (!this->data || this->len != size) {
this->data = std::make_unique<uint8_t[]>(size);
this->len = size;
}
memcpy(this->data.get(), src, size);
}
};
static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@@ -32,14 +41,14 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend {
// try find in pending saves and update that
for (auto &obj : s_pending_save) {
if (obj.key == this->key) {
obj.data.set(data, len);
obj.set_data(data, len);
return true;
}
}
NVSData save{};
save.key = this->key;
save.data.set(data, len);
s_pending_save.push_back(std::move(save));
save.set_data(data, len);
s_pending_save.emplace_back(std::move(save));
ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len);
return true;
}
@@ -47,11 +56,11 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend {
// try find in pending saves and load from that
for (auto &obj : s_pending_save) {
if (obj.key == this->key) {
if (obj.data.size() != len) {
if (obj.len != len) {
// size mismatch
return false;
}
memcpy(data, obj.data.data(), len);
memcpy(data, obj.data.get(), len);
return true;
}
}
@@ -127,10 +136,10 @@ class ESP32Preferences : public ESPPreferences {
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());
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size());
esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.get(), save.len);
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.len);
if (err != 0) {
ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.data.size(), esp_err_to_name(err));
ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.len, esp_err_to_name(err));
failed++;
last_err = err;
last_key = save.key;
@@ -138,7 +147,7 @@ class ESP32Preferences : public ESPPreferences {
}
written++;
} else {
ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.data.size());
ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.len);
cached++;
}
s_pending_save.erase(s_pending_save.begin() + i);
@@ -169,7 +178,7 @@ class ESP32Preferences : public ESPPreferences {
return true;
}
// Check size first before allocating memory
if (actual_len != to_save.data.size()) {
if (actual_len != to_save.len) {
return true;
}
// Most preferences are small, use stack buffer with heap fallback for large ones
@@ -179,7 +188,7 @@ class ESP32Preferences : public ESPPreferences {
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err));
return true;
}
return memcmp(to_save.data.data(), stored_data.get(), to_save.data.size()) != 0;
return memcmp(to_save.data.get(), stored_data.get(), to_save.len) != 0;
}
bool reset() override {

View File

@@ -403,18 +403,21 @@ def final_validation(config):
add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID", True)
add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_BLUEDROID_HCI_VHCI", True)
# Check if BLE Server is needed
has_ble_server = "esp32_ble_server" in full_config
# Check if BLE Client is needed (via esp32_ble_tracker or esp32_ble_client)
has_ble_client = (
"esp32_ble_tracker" in full_config or "esp32_ble_client" in full_config
)
# ESP-IDF BLE stack requires GATT Server to be enabled when GATT Client is enabled
# This is an internal dependency in the Bluedroid stack (tested ESP-IDF 5.4.2-5.5.1)
# Always enable GATTS: ESP-IDF 5.5.2.260206 has a bug in gatt_main.c where a
# GATT_TRACE_DEBUG references 'msg_len' outside the GATTS_INCLUDED/GATTC_INCLUDED
# guard, causing a compile error when both are disabled.
# Additionally, when GATT Client is enabled, GATT Server must also be enabled
# as an internal dependency in the Bluedroid stack.
# See: https://github.com/espressif/esp-idf/issues/17724
add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server or has_ble_client)
# TODO: Revert to conditional once the gatt_main.c bug is fixed upstream:
# has_ble_server = "esp32_ble_server" in full_config
# add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server or has_ble_client)
add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", True)
add_idf_sdkconfig_option("CONFIG_BT_GATTC_ENABLE", has_ble_client)
# Handle max_connections: check for deprecated location in esp32_ble_tracker

View File

@@ -98,10 +98,6 @@ void ESP32BLE::advertising_set_service_data(const std::vector<uint8_t> &data) {
}
void ESP32BLE::advertising_set_manufacturer_data(const std::vector<uint8_t> &data) {
this->advertising_set_manufacturer_data(std::span<const uint8_t>(data));
}
void ESP32BLE::advertising_set_manufacturer_data(std::span<const uint8_t> data) {
this->advertising_init_();
this->advertising_->set_manufacturer_data(data);
this->advertising_start();
@@ -373,9 +369,42 @@ bool ESP32BLE::ble_dismantle_() {
}
void ESP32BLE::loop() {
if (this->state_ != BLE_COMPONENT_STATE_ACTIVE) {
this->loop_handle_state_transition_not_active_();
return;
switch (this->state_) {
case BLE_COMPONENT_STATE_OFF:
case BLE_COMPONENT_STATE_DISABLED:
return;
case BLE_COMPONENT_STATE_DISABLE: {
ESP_LOGD(TAG, "Disabling");
#ifdef ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT
for (auto *ble_event_handler : this->ble_status_event_handlers_) {
ble_event_handler->ble_before_disabled_event_handler();
}
#endif
if (!ble_dismantle_()) {
ESP_LOGE(TAG, "Could not be dismantled");
this->mark_failed();
return;
}
this->state_ = BLE_COMPONENT_STATE_DISABLED;
return;
}
case BLE_COMPONENT_STATE_ENABLE: {
ESP_LOGD(TAG, "Enabling");
this->state_ = BLE_COMPONENT_STATE_OFF;
if (!ble_setup_()) {
ESP_LOGE(TAG, "Could not be set up");
this->mark_failed();
return;
}
this->state_ = BLE_COMPONENT_STATE_ACTIVE;
return;
}
case BLE_COMPONENT_STATE_ACTIVE:
break;
}
BLEEvent *ble_event = this->ble_events_.pop();
@@ -491,37 +520,6 @@ void ESP32BLE::loop() {
}
}
void ESP32BLE::loop_handle_state_transition_not_active_() {
// Caller ensures state_ != ACTIVE
if (this->state_ == BLE_COMPONENT_STATE_DISABLE) {
ESP_LOGD(TAG, "Disabling");
#ifdef ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT
for (auto *ble_event_handler : this->ble_status_event_handlers_) {
ble_event_handler->ble_before_disabled_event_handler();
}
#endif
if (!ble_dismantle_()) {
ESP_LOGE(TAG, "Could not be dismantled");
this->mark_failed();
return;
}
this->state_ = BLE_COMPONENT_STATE_DISABLED;
} else if (this->state_ == BLE_COMPONENT_STATE_ENABLE) {
ESP_LOGD(TAG, "Enabling");
this->state_ = BLE_COMPONENT_STATE_OFF;
if (!ble_setup_()) {
ESP_LOGE(TAG, "Could not be set up");
this->mark_failed();
return;
}
this->state_ = BLE_COMPONENT_STATE_ACTIVE;
}
}
// Helper function to load new event data based on type
void load_ble_event(BLEEvent *event, esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
event->load_gap_event(e, p);

View File

@@ -118,7 +118,6 @@ class ESP32BLE : public Component {
void advertising_start();
void advertising_set_service_data(const std::vector<uint8_t> &data);
void advertising_set_manufacturer_data(const std::vector<uint8_t> &data);
void advertising_set_manufacturer_data(std::span<const uint8_t> data);
void advertising_set_appearance(uint16_t appearance) { this->appearance_ = appearance; }
void advertising_set_service_data_and_name(std::span<const uint8_t> data, bool include_name);
void advertising_add_service_uuid(ESPBTUUID uuid);
@@ -156,10 +155,6 @@ class ESP32BLE : public Component {
#endif
static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param);
// Handle non-ACTIVE state transitions (DISABLE, ENABLE, OFF, DISABLED).
// Extracted from loop() to keep the hot event-processing path small.
void __attribute__((noinline)) loop_handle_state_transition_not_active_();
bool ble_setup_();
bool ble_dismantle_();
bool ble_pre_setup_();

View File

@@ -59,10 +59,6 @@ void BLEAdvertising::set_service_data(const std::vector<uint8_t> &data) {
}
void BLEAdvertising::set_manufacturer_data(const std::vector<uint8_t> &data) {
this->set_manufacturer_data(std::span<const uint8_t>(data));
}
void BLEAdvertising::set_manufacturer_data(std::span<const uint8_t> data) {
delete[] this->advertising_data_.p_manufacturer_data;
this->advertising_data_.p_manufacturer_data = nullptr;
this->advertising_data_.manufacturer_len = data.size();

View File

@@ -28,7 +28,6 @@ class BLEAdvertising {
void set_scan_response(bool scan_response) { this->scan_response_ = scan_response; }
void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; }
void set_manufacturer_data(const std::vector<uint8_t> &data);
void set_manufacturer_data(std::span<const uint8_t> data);
void set_appearance(uint16_t appearance) { this->advertising_data_.appearance = appearance; }
void set_service_data(const std::vector<uint8_t> &data);
void set_service_data(std::span<const uint8_t> data);

View File

@@ -48,7 +48,7 @@ class ESPBTUUID {
// Remove before 2026.8.0
ESPDEPRECATED("Use to_str() instead. Removed in 2026.8.0", "2026.2.0")
std::string to_string() const; // NOLINT
std::string to_string() const;
const char *to_str(std::span<char, UUID_STR_LEN> output) const;
protected:

View File

@@ -1,6 +1,5 @@
#include "esp32_ble_beacon.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#ifdef USE_ESP32

View File

@@ -15,10 +15,7 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_characteristic_on_w
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
new Trigger<std::vector<uint8_t>, uint16_t>();
characteristic->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
// Convert span to vector for trigger - copy is necessary because:
// 1. Trigger stores the data for use in automation actions that execute later
// 2. The span is only valid during this callback (points to temporary BLE stack data)
// 3. User lambdas in automations need persistent data they can access asynchronously
// Convert span to vector for trigger
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
});
return on_write_trigger;
@@ -30,10 +27,7 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_descriptor_on_write
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
new Trigger<std::vector<uint8_t>, uint16_t>();
descriptor->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
// Convert span to vector for trigger - copy is necessary because:
// 1. Trigger stores the data for use in automation actions that execute later
// 2. The span is only valid during this callback (points to temporary BLE stack data)
// 3. User lambdas in automations need persistent data they can access asynchronously
// Convert span to vector for trigger
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
});
return on_write_trigger;

View File

@@ -27,11 +27,6 @@ static const char *const TAG = "esp32_hosted.update";
// Older coprocessor firmware versions have a 1500-byte limit per RPC call
constexpr size_t CHUNK_SIZE = 1500;
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
// Interval/timeout IDs (uint32_t to avoid string comparison)
constexpr uint32_t INITIAL_CHECK_INTERVAL_ID = 0;
#endif
// Compile-time version string from esp_hosted_host_fw_ver.h macros
#define STRINGIFY_(x) #x
#define STRINGIFY(x) STRINGIFY_(x)
@@ -132,18 +127,15 @@ void Esp32HostedUpdate::setup() {
this->status_clear_error();
this->publish_state();
#else
// HTTP mode: check every 10s until network is ready (max 6 attempts)
// HTTP mode: retry initial check every 10s until network is ready (max 6 attempts)
// Only if update interval is > 1 minute to avoid redundant checks
if (this->get_update_interval() > 60000) {
this->initial_check_remaining_ = 6;
this->set_interval(INITIAL_CHECK_INTERVAL_ID, 10000, [this]() {
bool connected = network::is_connected();
if (--this->initial_check_remaining_ == 0 || connected) {
this->cancel_interval(INITIAL_CHECK_INTERVAL_ID);
if (connected) {
this->check();
}
this->set_retry("initial_check", 10000, 6, [this](uint8_t) {
if (!network::is_connected()) {
return RetryResult::RETRY;
}
this->check();
return RetryResult::DONE;
});
}
#endif

View File

@@ -44,7 +44,6 @@ class Esp32HostedUpdate : public update::UpdateEntity, public PollingComponent {
// HTTP mode helpers
bool fetch_manifest_();
bool stream_firmware_to_coprocessor_();
uint8_t initial_check_remaining_{0};
#else
// Embedded mode members
const uint8_t *firmware_data_{nullptr};

View File

@@ -811,8 +811,8 @@ void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) {
ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed");
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
char hex_buf[format_hex_pretty_size(PHY_REG_SIZE)];
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE));
#endif
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE));
/*
* Bit 7 is `RMII Reference Clock Select`. Default is `0`.
@@ -829,10 +829,8 @@ void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) {
ESPHL_ERROR_CHECK(err, "Write PHY Control 2 failed");
err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2));
ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed");
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s",
format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE));
#endif
}
}
#endif // USE_ETHERNET_KSZ8081

View File

@@ -68,7 +68,7 @@ void FanCall::validate_() {
auto traits = this->parent_.get_traits();
if (this->speed_.has_value()) {
this->speed_ = clamp(*this->speed_, 1, static_cast<int>(traits.supported_speed_count()));
this->speed_ = clamp(*this->speed_, 1, traits.supported_speed_count());
// https://developers.home-assistant.io/docs/core/entity/fan/#preset-modes
// "Manually setting a speed must disable any set preset mode"

View File

@@ -11,7 +11,7 @@ namespace fan {
class FanTraits {
public:
FanTraits() = default;
FanTraits(bool oscillation, bool speed, bool direction, uint8_t speed_count)
FanTraits(bool oscillation, bool speed, bool direction, int speed_count)
: oscillation_(oscillation), speed_(speed), direction_(direction), speed_count_(speed_count) {}
/// Return if this fan supports oscillation.
@@ -23,9 +23,9 @@ class FanTraits {
/// Set whether this fan supports speed levels.
void set_speed(bool speed) { this->speed_ = speed; }
/// Return how many speed levels the fan has
uint8_t supported_speed_count() const { return this->speed_count_; }
int supported_speed_count() const { return this->speed_count_; }
/// Set how many speed levels this fan has.
void set_supported_speed_count(uint8_t speed_count) { this->speed_count_ = speed_count; }
void set_supported_speed_count(int speed_count) { this->speed_count_ = speed_count; }
/// Return if this fan supports changing direction
bool supports_direction() const { return this->direction_; }
/// Set whether this fan supports changing direction
@@ -64,7 +64,7 @@ class FanTraits {
bool oscillation_{false};
bool speed_{false};
bool direction_{false};
uint8_t speed_count_{};
int speed_count_{};
std::vector<const char *> preset_modes_{};
};

View File

@@ -39,7 +39,7 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_DECAY_MODE, default="SLOW"): cv.enum(
DECAY_MODE_OPTIONS, upper=True
),
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1, max=255),
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1),
cv.Optional(CONF_ENABLE_PIN): cv.use_id(output.FloatOutput),
cv.Optional(CONF_PRESET_MODES): validate_preset_modes,
}

View File

@@ -15,7 +15,7 @@ enum DecayMode {
class HBridgeFan : public Component, public fan::Fan {
public:
HBridgeFan(uint8_t speed_count, DecayMode decay_mode) : speed_count_(speed_count), decay_mode_(decay_mode) {}
HBridgeFan(int speed_count, DecayMode decay_mode) : speed_count_(speed_count), decay_mode_(decay_mode) {}
void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; }
void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; }
@@ -33,7 +33,7 @@ class HBridgeFan : public Component, public fan::Fan {
output::FloatOutput *pin_b_;
output::FloatOutput *enable_{nullptr};
output::BinaryOutput *oscillating_{nullptr};
uint8_t speed_count_{};
int speed_count_{};
DecayMode decay_mode_{DECAY_MODE_SLOW};
fan::FanTraits traits_;
std::vector<const char *> preset_modes_{};

View File

@@ -1,16 +1,20 @@
#include "hlk_fm22x.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#include <array>
#include <cinttypes>
namespace esphome::hlk_fm22x {
static const char *const TAG = "hlk_fm22x";
// Maximum response size is 36 bytes (VERIFY reply: face_id + 32-byte name)
static constexpr size_t HLK_FM22X_MAX_RESPONSE_SIZE = 36;
void HlkFm22xComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up HLK-FM22X...");
this->set_enrolling_(false);
while (this->available() > 0) {
while (this->available()) {
this->read();
}
this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_STATUS); });
@@ -31,7 +35,7 @@ void HlkFm22xComponent::update() {
}
void HlkFm22xComponent::enroll_face(const std::string &name, HlkFm22xFaceDirection direction) {
if (name.length() > HLK_FM22X_NAME_SIZE - 1) {
if (name.length() > 31) {
ESP_LOGE(TAG, "enroll_face(): name too long '%s'", name.c_str());
return;
}
@@ -84,7 +88,7 @@ void HlkFm22xComponent::send_command_(HlkFm22xCommand command, const uint8_t *da
}
this->wait_cycles_ = 0;
this->active_command_ = command;
while (this->available() > 0)
while (this->available())
this->read();
this->write((uint8_t) (START_CODE >> 8));
this->write((uint8_t) (START_CODE & 0xFF));
@@ -133,24 +137,17 @@ void HlkFm22xComponent::recv_command_() {
checksum ^= byte;
length |= byte;
if (length > HLK_FM22X_MAX_RESPONSE_SIZE) {
ESP_LOGE(TAG, "Response too large: %u bytes", length);
// Discard exactly the remaining payload and checksum for this frame
for (uint16_t i = 0; i < length + 1 && this->available() > 0; ++i)
this->read();
return;
}
std::vector<uint8_t> data;
data.reserve(length);
for (uint16_t idx = 0; idx < length; ++idx) {
byte = this->read();
checksum ^= byte;
this->recv_buf_[idx] = byte;
data.push_back(byte);
}
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char hex_buf[format_hex_pretty_size(HLK_FM22X_MAX_RESPONSE_SIZE)];
ESP_LOGV(TAG, "Recv type: 0x%.2X, data: %s", response_type,
format_hex_pretty_to(hex_buf, this->recv_buf_.data(), length));
ESP_LOGV(TAG, "Recv type: 0x%.2X, data: %s", response_type, format_hex_pretty_to(hex_buf, data.data(), data.size()));
#endif
byte = this->read();
@@ -160,10 +157,10 @@ void HlkFm22xComponent::recv_command_() {
}
switch (response_type) {
case HlkFm22xResponseType::NOTE:
this->handle_note_(this->recv_buf_.data(), length);
this->handle_note_(data);
break;
case HlkFm22xResponseType::REPLY:
this->handle_reply_(this->recv_buf_.data(), length);
this->handle_reply_(data);
break;
default:
ESP_LOGW(TAG, "Unexpected response type: 0x%.2X", response_type);
@@ -171,15 +168,11 @@ void HlkFm22xComponent::recv_command_() {
}
}
void HlkFm22xComponent::handle_note_(const uint8_t *data, size_t length) {
if (length < 1) {
ESP_LOGE(TAG, "Empty note data");
return;
}
void HlkFm22xComponent::handle_note_(const std::vector<uint8_t> &data) {
switch (data[0]) {
case HlkFm22xNoteType::FACE_STATE:
if (length < 17) {
ESP_LOGE(TAG, "Invalid face note data size: %zu", length);
if (data.size() < 17) {
ESP_LOGE(TAG, "Invalid face note data size: %u", data.size());
break;
}
{
@@ -216,13 +209,9 @@ void HlkFm22xComponent::handle_note_(const uint8_t *data, size_t length) {
}
}
void HlkFm22xComponent::handle_reply_(const uint8_t *data, size_t length) {
void HlkFm22xComponent::handle_reply_(const std::vector<uint8_t> &data) {
auto expected = this->active_command_;
this->active_command_ = HlkFm22xCommand::NONE;
if (length < 2) {
ESP_LOGE(TAG, "Reply too short: %zu bytes", length);
return;
}
if (data[0] != (uint8_t) expected) {
ESP_LOGE(TAG, "Unexpected response command. Expected: 0x%.2X, Received: 0x%.2X", expected, data[0]);
return;
@@ -249,20 +238,16 @@ void HlkFm22xComponent::handle_reply_(const uint8_t *data, size_t length) {
}
switch (expected) {
case HlkFm22xCommand::VERIFY: {
if (length < 4 + HLK_FM22X_NAME_SIZE) {
ESP_LOGE(TAG, "VERIFY response too short: %zu bytes", length);
break;
}
int16_t face_id = ((int16_t) data[2] << 8) | data[3];
const char *name_ptr = reinterpret_cast<const char *>(data + 4);
ESP_LOGD(TAG, "Face verified. ID: %d, name: %.*s", face_id, (int) HLK_FM22X_NAME_SIZE, name_ptr);
std::string name(data.begin() + 4, data.begin() + 36);
ESP_LOGD(TAG, "Face verified. ID: %d, name: %s", face_id, name.c_str());
if (this->last_face_id_sensor_ != nullptr) {
this->last_face_id_sensor_->publish_state(face_id);
}
if (this->last_face_name_text_sensor_ != nullptr) {
this->last_face_name_text_sensor_->publish_state(name_ptr, HLK_FM22X_NAME_SIZE);
this->last_face_name_text_sensor_->publish_state(name);
}
this->face_scan_matched_callback_.call(face_id, std::string(name_ptr, HLK_FM22X_NAME_SIZE));
this->face_scan_matched_callback_.call(face_id, name);
break;
}
case HlkFm22xCommand::ENROLL: {
@@ -281,8 +266,9 @@ void HlkFm22xComponent::handle_reply_(const uint8_t *data, size_t length) {
this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_VERSION); });
break;
case HlkFm22xCommand::GET_VERSION:
if (this->version_text_sensor_ != nullptr && length > 2) {
this->version_text_sensor_->publish_state(reinterpret_cast<const char *>(data + 2), length - 2);
if (this->version_text_sensor_ != nullptr) {
std::string version(data.begin() + 2, data.end());
this->version_text_sensor_->publish_state(version);
}
this->defer([this]() { this->get_face_count_(); });
break;

View File

@@ -7,15 +7,12 @@
#include "esphome/components/text_sensor/text_sensor.h"
#include "esphome/components/uart/uart.h"
#include <array>
#include <utility>
#include <vector>
namespace esphome::hlk_fm22x {
static const uint16_t START_CODE = 0xEFAA;
static constexpr size_t HLK_FM22X_NAME_SIZE = 32;
// Maximum response payload: command(1) + result(1) + face_id(2) + name(32) = 36
static constexpr size_t HLK_FM22X_MAX_RESPONSE_SIZE = 36;
enum HlkFm22xCommand {
NONE = 0x00,
RESET = 0x10,
@@ -121,11 +118,10 @@ class HlkFm22xComponent : public PollingComponent, public uart::UARTDevice {
void get_face_count_();
void send_command_(HlkFm22xCommand command, const uint8_t *data = nullptr, size_t size = 0);
void recv_command_();
void handle_note_(const uint8_t *data, size_t length);
void handle_reply_(const uint8_t *data, size_t length);
void handle_note_(const std::vector<uint8_t> &data);
void handle_reply_(const std::vector<uint8_t> &data);
void set_enrolling_(bool enrolling);
std::array<uint8_t, HLK_FM22X_MAX_RESPONSE_SIZE> recv_buf_;
HlkFm22xCommand active_command_ = HlkFm22xCommand::NONE;
uint16_t wait_cycles_ = 0;
sensor::Sensor *face_count_sensor_{nullptr};

View File

@@ -90,14 +90,16 @@ void HttpRequestUpdate::update_task(void *params) {
UPDATE_RETURN;
}
size_t read_index = container->get_bytes_read();
size_t content_length = container->content_length;
container->end();
container.reset(); // Release ownership of the container's shared_ptr
bool valid = false;
{ // Scope to ensure JsonDocument is destroyed before deallocating buffer
valid = json::parse_json(data, read_index, [this_update](JsonObject root) -> bool {
{ // Ensures the response string falls out of scope and deallocates before the task ends
std::string response((char *) data, read_index);
allocator.deallocate(data, container->content_length);
container->end();
container.reset(); // Release ownership of the container's shared_ptr
valid = json::parse_json(response, [this_update](JsonObject root) -> bool {
if (!root[ESPHOME_F("name")].is<const char *>() || !root[ESPHOME_F("version")].is<const char *>() ||
!root[ESPHOME_F("builds")].is<JsonArray>()) {
ESP_LOGE(TAG, "Manifest does not contain required fields");
@@ -135,7 +137,6 @@ void HttpRequestUpdate::update_task(void *params) {
return false;
});
}
allocator.deallocate(data, content_length);
if (!valid) {
ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str());
@@ -156,12 +157,17 @@ void HttpRequestUpdate::update_task(void *params) {
}
}
{ // Ensures the current version string falls out of scope and deallocates before the task ends
std::string current_version;
#ifdef ESPHOME_PROJECT_VERSION
this_update->update_info_.current_version = ESPHOME_PROJECT_VERSION;
current_version = ESPHOME_PROJECT_VERSION;
#else
this_update->update_info_.current_version = ESPHOME_VERSION;
current_version = ESPHOME_VERSION;
#endif
this_update->update_info_.current_version = current_version;
}
bool trigger_update_available = false;
if (this_update->update_info_.latest_version.empty() ||

View File

@@ -134,23 +134,25 @@ ErrorCode ArduinoI2CBus::write_readv(uint8_t address, const uint8_t *write_buffe
for (size_t j = 0; j != read_count; j++)
read_buffer[j] = wire_->read();
}
// Avoid switch to prevent compiler-generated lookup table in RAM on ESP8266
if (status == 0)
return ERROR_OK;
if (status == 1) {
ESP_LOGVV(TAG, "TX failed: buffer not large enough");
return ERROR_UNKNOWN;
switch (status) {
case 0:
return ERROR_OK;
case 1:
// transmit buffer not large enough
ESP_LOGVV(TAG, "TX failed: buffer not large enough");
return ERROR_UNKNOWN;
case 2:
case 3:
ESP_LOGVV(TAG, "TX failed: not acknowledged: %d", status);
return ERROR_NOT_ACKNOWLEDGED;
case 5:
ESP_LOGVV(TAG, "TX failed: timeout");
return ERROR_UNKNOWN;
case 4:
default:
ESP_LOGVV(TAG, "TX failed: unknown error %u", status);
return ERROR_UNKNOWN;
}
if (status == 2 || status == 3) {
ESP_LOGVV(TAG, "TX failed: not acknowledged: %u", status);
return ERROR_NOT_ACKNOWLEDGED;
}
if (status == 5) {
ESP_LOGVV(TAG, "TX failed: timeout");
return ERROR_UNKNOWN;
}
ESP_LOGVV(TAG, "TX failed: unknown error %u", status);
return ERROR_UNKNOWN;
}
/// Perform I2C bus recovery, see:

View File

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

View File

@@ -267,26 +267,16 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
for (auto &scan : results) {
if (scan.get_is_hidden())
continue;
const char *ssid_cstr = scan.get_ssid().c_str();
// Check if we've already sent this SSID
bool duplicate = false;
for (const auto &seen : networks) {
if (strcmp(seen.c_str(), ssid_cstr) == 0) {
duplicate = true;
break;
}
}
if (duplicate)
const std::string &ssid = scan.get_ssid();
if (std::find(networks.begin(), networks.end(), ssid) != networks.end())
continue;
// Only allocate std::string after confirming it's not a duplicate
std::string ssid(ssid_cstr);
// Send each ssid separately to avoid overflowing the buffer
char rssi_buf[5]; // int8_t: -128 to 127, max 4 chars + null
*int8_to_str(rssi_buf, scan.get_rssi()) = '\0';
std::vector<uint8_t> data =
improv::build_rpc_response(improv::GET_WIFI_NETWORKS, {ssid, rssi_buf, YESNO(scan.get_with_auth())}, false);
this->send_response_(data);
networks.push_back(std::move(ssid));
networks.push_back(ssid);
}
// Send empty response to signify the end of the list.
std::vector<uint8_t> data =

View File

@@ -15,7 +15,7 @@ static const char *const TAG = "json";
static SpiRamAllocator global_json_allocator;
#endif
SerializationBuffer<> build_json(const json_build_t &f) {
std::string build_json(const json_build_t &f) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
JsonBuilder builder;
JsonObject root = builder.root();
@@ -25,13 +25,8 @@ SerializationBuffer<> build_json(const json_build_t &f) {
}
bool parse_json(const std::string &data, const json_parse_t &f) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
return parse_json(reinterpret_cast<const uint8_t *>(data.c_str()), data.size(), f);
}
bool parse_json(const uint8_t *data, size_t len, const json_parse_t &f) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
JsonDocument doc = parse_json(data, len);
JsonDocument doc = parse_json(reinterpret_cast<const uint8_t *>(data.c_str()), data.size());
if (doc.overflowed() || doc.isNull())
return false;
return f(doc.as<JsonObject>());
@@ -66,62 +61,14 @@ JsonDocument parse_json(const uint8_t *data, size_t len) {
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
}
SerializationBuffer<> JsonBuilder::serialize() {
// ===========================================================================================
// CRITICAL: NRVO (Named Return Value Optimization) - DO NOT REFACTOR WITHOUT UNDERSTANDING
// ===========================================================================================
//
// This function is carefully structured to enable NRVO. The compiler constructs `result`
// directly in the caller's stack frame, eliminating the move constructor call entirely.
//
// WITHOUT NRVO: Each return would trigger SerializationBuffer's move constructor, which
// must memcpy up to 768 bytes of stack buffer content. This happens on EVERY JSON
// serialization (sensor updates, web server responses, MQTT publishes, etc.).
//
// WITH NRVO: Zero memcpy, zero move constructor overhead. The buffer lives directly
// where the caller needs it.
//
// Requirements for NRVO to work:
// 1. Single named variable (`result`) returned from ALL paths
// 2. All paths must return the SAME variable (not different variables)
// 3. No std::move() on the return statement
//
// If you must modify this function:
// - Keep a single `result` variable declared at the top
// - All code paths must return `result` (not a different variable)
// - Verify NRVO still works by checking the disassembly for move constructor calls
// - Test: objdump -d -C firmware.elf | grep "SerializationBuffer.*SerializationBuffer"
// Should show only destructor, NOT move constructor
//
// Why we avoid measureJson(): It instantiates DummyWriter templates adding ~1KB flash.
// Instead, try stack buffer first. 768 bytes covers 99.9% of JSON payloads (sensors ~200B,
// lights ~170B, climate ~700B). Only entities with 40+ options exceed this.
//
// ===========================================================================================
constexpr size_t buf_size = SerializationBuffer<>::BUFFER_SIZE;
SerializationBuffer<> result(buf_size - 1); // Max content size (reserve 1 for null)
std::string JsonBuilder::serialize() {
if (doc_.overflowed()) {
ESP_LOGE(TAG, "JSON document overflow");
auto *buf = result.data_writable_();
buf[0] = '{';
buf[1] = '}';
buf[2] = '\0';
result.set_size_(2);
return result;
return "{}";
}
size_t size = serializeJson(doc_, result.data_writable_(), buf_size);
if (size < buf_size) {
// Fits in stack buffer - update size to actual length
result.set_size_(size);
return result;
}
// Needs heap allocation - reallocate and serialize again with exact size
result.reallocate_heap_(size);
serializeJson(doc_, result.data_writable_(), size + 1);
return result;
std::string output;
serializeJson(doc_, output);
return output;
}
} // namespace json

View File

@@ -1,7 +1,5 @@
#pragma once
#include <cstring>
#include <string>
#include <vector>
#include "esphome/core/defines.h"
@@ -16,108 +14,6 @@
namespace esphome {
namespace json {
/// Buffer for JSON serialization that uses stack allocation for small payloads.
/// Template parameter STACK_SIZE specifies the stack buffer size (default 768 bytes).
/// Supports move semantics for efficient return-by-value.
template<size_t STACK_SIZE = 768> class SerializationBuffer {
public:
static constexpr size_t BUFFER_SIZE = STACK_SIZE; ///< Stack buffer size for this instantiation
/// Construct with known size (typically from measureJson)
explicit SerializationBuffer(size_t size) : size_(size) {
if (size + 1 <= STACK_SIZE) {
buffer_ = stack_buffer_;
} else {
heap_buffer_ = new char[size + 1];
buffer_ = heap_buffer_;
}
buffer_[0] = '\0';
}
~SerializationBuffer() { delete[] heap_buffer_; }
// Move constructor - works with same template instantiation
SerializationBuffer(SerializationBuffer &&other) noexcept : heap_buffer_(other.heap_buffer_), size_(other.size_) {
if (other.buffer_ == other.stack_buffer_) {
// Stack buffer - must copy content
std::memcpy(stack_buffer_, other.stack_buffer_, size_ + 1);
buffer_ = stack_buffer_;
} else {
// Heap buffer - steal ownership
buffer_ = heap_buffer_;
other.heap_buffer_ = nullptr;
}
// Leave moved-from object in valid empty state
other.stack_buffer_[0] = '\0';
other.buffer_ = other.stack_buffer_;
other.size_ = 0;
}
// Move assignment
SerializationBuffer &operator=(SerializationBuffer &&other) noexcept {
if (this != &other) {
delete[] heap_buffer_;
heap_buffer_ = other.heap_buffer_;
size_ = other.size_;
if (other.buffer_ == other.stack_buffer_) {
std::memcpy(stack_buffer_, other.stack_buffer_, size_ + 1);
buffer_ = stack_buffer_;
} else {
buffer_ = heap_buffer_;
other.heap_buffer_ = nullptr;
}
// Leave moved-from object in valid empty state
other.stack_buffer_[0] = '\0';
other.buffer_ = other.stack_buffer_;
other.size_ = 0;
}
return *this;
}
// Delete copy operations
SerializationBuffer(const SerializationBuffer &) = delete;
SerializationBuffer &operator=(const SerializationBuffer &) = delete;
/// Get null-terminated C string
const char *c_str() const { return buffer_; }
/// Get data pointer
const char *data() const { return buffer_; }
/// Get string length (excluding null terminator)
size_t size() const { return size_; }
/// Implicit conversion to std::string for backward compatibility
/// WARNING: This allocates a new std::string on the heap. Prefer using
/// c_str() or data()/size() directly when possible to avoid allocation.
operator std::string() const { return std::string(buffer_, size_); } // NOLINT(google-explicit-constructor)
private:
friend class JsonBuilder; ///< Allows JsonBuilder::serialize() to call private methods
/// Get writable buffer (for serialization)
char *data_writable_() { return buffer_; }
/// Set actual size after serialization (must not exceed allocated size)
/// Also ensures null termination for c_str() safety
void set_size_(size_t size) {
size_ = size;
buffer_[size] = '\0';
}
/// Reallocate to heap buffer with new size (for when stack buffer is too small)
/// This invalidates any previous buffer content. Used by JsonBuilder::serialize().
void reallocate_heap_(size_t size) {
delete[] heap_buffer_;
heap_buffer_ = new char[size + 1];
buffer_ = heap_buffer_;
size_ = size;
buffer_[0] = '\0';
}
char stack_buffer_[STACK_SIZE];
char *heap_buffer_{nullptr};
char *buffer_;
size_t size_;
};
#ifdef USE_PSRAM
// Build an allocator for the JSON Library using the RAMAllocator class
// This is only compiled when PSRAM is enabled
@@ -150,13 +46,10 @@ using json_parse_t = std::function<bool(JsonObject)>;
using json_build_t = std::function<void(JsonObject)>;
/// Build a JSON string with the provided json build function.
/// Returns SerializationBuffer for stack-first allocation; implicitly converts to std::string.
SerializationBuffer<> build_json(const json_build_t &f);
std::string build_json(const json_build_t &f);
/// Parse a JSON string and run the provided json parse function if it's valid.
bool parse_json(const std::string &data, const json_parse_t &f);
/// Parse JSON from raw bytes and run the provided json parse function if it's valid.
bool parse_json(const uint8_t *data, size_t len, const json_parse_t &f);
/// Parse a JSON string and return the root JsonDocument (or an unbound object on error)
JsonDocument parse_json(const uint8_t *data, size_t len);
@@ -176,9 +69,7 @@ class JsonBuilder {
return root_;
}
/// Serialize the JSON document to a SerializationBuffer (stack-first allocation)
/// Uses 768-byte stack buffer by default, falls back to heap for larger JSON
SerializationBuffer<> serialize();
std::string serialize();
private:
#ifdef USE_PSRAM

View File

@@ -11,7 +11,7 @@ static const char *const TAG = "kuntze";
static const uint8_t CMD_READ_REG = 0x03;
static const uint16_t REGISTER[] = {4136, 4160, 4680, 6000, 4688, 4728, 5832};
// Maximum bytes to log for Modbus responses (2 registers = 4 bytes, plus byte count = 5 bytes)
// Maximum bytes to log for Modbus responses (2 registers = 4, plus count = 5)
static constexpr size_t KUNTZE_MAX_LOG_BYTES = 8;
void Kuntze::on_modbus_data(const std::vector<uint8_t> &data) {

View File

@@ -275,19 +275,8 @@ void LD2410Component::restart_and_read_all_info() {
}
void LD2410Component::loop() {
// Read all available bytes in batches to reduce UART call overhead.
size_t avail = this->available();
uint8_t buf[MAX_LINE_LENGTH];
while (avail > 0) {
size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}
avail -= to_read;
for (size_t i = 0; i < to_read; i++) {
this->readline_(buf[i]);
}
while (this->available()) {
this->readline_(this->read());
}
}

View File

@@ -310,19 +310,8 @@ void LD2412Component::restart_and_read_all_info() {
}
void LD2412Component::loop() {
// Read all available bytes in batches to reduce UART call overhead.
size_t avail = this->available();
uint8_t buf[MAX_LINE_LENGTH];
while (avail > 0) {
size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}
avail -= to_read;
for (size_t i = 0; i < to_read; i++) {
this->readline_(buf[i]);
}
while (this->available()) {
this->readline_(this->read());
}
}

View File

@@ -335,10 +335,9 @@ void LD2420Component::revert_config_action() {
void LD2420Component::loop() {
// If there is a active send command do not process it here, the send command call will handle it.
if (this->cmd_active_) {
return;
while (!this->cmd_active_ && this->available()) {
this->readline_(this->read(), this->buffer_data_, MAX_LINE_LENGTH);
}
this->read_batch_(this->buffer_data_);
}
void LD2420Component::update_radar_data(uint16_t const *gate_energy, uint8_t sample_number) {
@@ -540,23 +539,6 @@ void LD2420Component::handle_simple_mode_(const uint8_t *inbuf, int len) {
}
}
void LD2420Component::read_batch_(std::span<uint8_t, MAX_LINE_LENGTH> buffer) {
// Read all available bytes in batches to reduce UART call overhead.
size_t avail = this->available();
uint8_t buf[MAX_LINE_LENGTH];
while (avail > 0) {
size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}
avail -= to_read;
for (size_t i = 0; i < to_read; i++) {
this->readline_(buf[i], buffer.data(), buffer.size());
}
}
}
void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) {
this->cmd_reply_.command = buffer[CMD_FRAME_COMMAND];
this->cmd_reply_.length = buffer[CMD_FRAME_DATA_LENGTH];

View File

@@ -4,7 +4,6 @@
#include "esphome/components/uart/uart.h"
#include "esphome/core/automation.h"
#include "esphome/core/helpers.h"
#include <span>
#ifdef USE_TEXT_SENSOR
#include "esphome/components/text_sensor/text_sensor.h"
#endif
@@ -166,7 +165,6 @@ class LD2420Component : public Component, public uart::UARTDevice {
void handle_energy_mode_(uint8_t *buffer, int len);
void handle_ack_data_(uint8_t *buffer, int len);
void readline_(int rx_data, uint8_t *buffer, int len);
void read_batch_(std::span<uint8_t, MAX_LINE_LENGTH> buffer);
void set_calibration_(bool state) { this->calibration_ = state; };
bool get_calibration_() { return this->calibration_; };

View File

@@ -276,19 +276,8 @@ void LD2450Component::dump_config() {
}
void LD2450Component::loop() {
// Read all available bytes in batches to reduce UART call overhead.
size_t avail = this->available();
uint8_t buf[MAX_LINE_LENGTH];
while (avail > 0) {
size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}
avail -= to_read;
for (size_t i = 0; i < to_read; i++) {
this->readline_(buf[i]);
}
while (this->available()) {
this->readline_(this->read());
}
}

View File

@@ -193,14 +193,14 @@ def _notify_old_style(config):
# The dev and latest branches will be at *least* this version, which is what matters.
# Use GitHub releases directly to avoid PlatformIO moderation delays.
ARDUINO_VERSIONS = {
"dev": (cv.Version(1, 12, 1), "https://github.com/libretiny-eu/libretiny.git"),
"dev": (cv.Version(1, 11, 0), "https://github.com/libretiny-eu/libretiny.git"),
"latest": (
cv.Version(1, 12, 1),
"https://github.com/libretiny-eu/libretiny.git#v1.12.1",
cv.Version(1, 11, 0),
"https://github.com/libretiny-eu/libretiny.git#v1.11.0",
),
"recommended": (
cv.Version(1, 12, 1),
"https://github.com/libretiny-eu/libretiny.git#v1.12.1",
cv.Version(1, 11, 0),
"https://github.com/libretiny-eu/libretiny.git#v1.11.0",
),
}

View File

@@ -18,7 +18,16 @@ 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.)
std::unique_ptr<uint8_t[]> data;
size_t len;
void set_data(const uint8_t *src, size_t size) {
if (!this->data || this->len != size) {
this->data = std::make_unique<uint8_t[]>(size);
this->len = size;
}
memcpy(this->data.get(), src, size);
}
};
static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@@ -33,14 +42,14 @@ class LibreTinyPreferenceBackend : public ESPPreferenceBackend {
// try find in pending saves and update that
for (auto &obj : s_pending_save) {
if (obj.key == this->key) {
obj.data.set(data, len);
obj.set_data(data, len);
return true;
}
}
NVSData save{};
save.key = this->key;
save.data.set(data, len);
s_pending_save.push_back(std::move(save));
save.set_data(data, len);
s_pending_save.emplace_back(std::move(save));
ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len);
return true;
}
@@ -49,11 +58,11 @@ class LibreTinyPreferenceBackend : public ESPPreferenceBackend {
// try find in pending saves and load from that
for (auto &obj : s_pending_save) {
if (obj.key == this->key) {
if (obj.data.size() != len) {
if (obj.len != len) {
// size mismatch
return false;
}
memcpy(data, obj.data.data(), len);
memcpy(data, obj.data.get(), len);
return true;
}
}
@@ -117,11 +126,11 @@ class LibreTinyPreferences : public ESPPreferences {
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());
fdb_blob_make(&this->blob, save.data.data(), save.data.size());
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.len);
fdb_blob_make(&this->blob, save.data.get(), save.len);
fdb_err_t err = fdb_kv_set_blob(&this->db, key_str, &this->blob);
if (err != FDB_NO_ERR) {
ESP_LOGV(TAG, "fdb_kv_set_blob('%s', len=%zu) failed: %d", key_str, save.data.size(), err);
ESP_LOGV(TAG, "fdb_kv_set_blob('%s', len=%zu) failed: %d", key_str, save.len, err);
failed++;
last_err = err;
last_key = save.key;
@@ -129,7 +138,7 @@ class LibreTinyPreferences : public ESPPreferences {
}
written++;
} else {
ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.data.size());
ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.len);
cached++;
}
s_pending_save.erase(s_pending_save.begin() + i);
@@ -153,7 +162,7 @@ class LibreTinyPreferences : public ESPPreferences {
}
// Check size first - if different, data has changed
if (kv.value_len != to_save.data.size()) {
if (kv.value_len != to_save.len) {
return true;
}
@@ -167,7 +176,7 @@ class LibreTinyPreferences : public ESPPreferences {
}
// Compare the actual data
return memcmp(to_save.data.data(), stored_data.get(), kv.value_len) != 0;
return memcmp(to_save.data.get(), stored_data.get(), kv.value_len) != 0;
}
bool reset() override {

View File

@@ -154,26 +154,28 @@ LN882X_BOARD_PINS = {
"A7": 21,
},
"wb02a": {
"WIRE0_SCL_0": 1,
"WIRE0_SCL_1": 2,
"WIRE0_SCL_0": 7,
"WIRE0_SCL_1": 5,
"WIRE0_SCL_2": 3,
"WIRE0_SCL_3": 4,
"WIRE0_SCL_4": 5,
"WIRE0_SCL_5": 7,
"WIRE0_SCL_6": 9,
"WIRE0_SCL_7": 10,
"WIRE0_SCL_8": 24,
"WIRE0_SCL_9": 25,
"WIRE0_SDA_0": 1,
"WIRE0_SDA_1": 2,
"WIRE0_SCL_3": 10,
"WIRE0_SCL_4": 2,
"WIRE0_SCL_5": 1,
"WIRE0_SCL_6": 4,
"WIRE0_SCL_7": 5,
"WIRE0_SCL_8": 9,
"WIRE0_SCL_9": 24,
"WIRE0_SCL_10": 25,
"WIRE0_SDA_0": 7,
"WIRE0_SDA_1": 5,
"WIRE0_SDA_2": 3,
"WIRE0_SDA_3": 4,
"WIRE0_SDA_4": 5,
"WIRE0_SDA_5": 7,
"WIRE0_SDA_6": 9,
"WIRE0_SDA_7": 10,
"WIRE0_SDA_8": 24,
"WIRE0_SDA_9": 25,
"WIRE0_SDA_3": 10,
"WIRE0_SDA_4": 2,
"WIRE0_SDA_5": 1,
"WIRE0_SDA_6": 4,
"WIRE0_SDA_7": 5,
"WIRE0_SDA_8": 9,
"WIRE0_SDA_9": 24,
"WIRE0_SDA_10": 25,
"SERIAL0_RX": 3,
"SERIAL0_TX": 2,
"SERIAL1_RX": 24,
@@ -219,32 +221,32 @@ LN882X_BOARD_PINS = {
"A1": 4,
},
"wl2s": {
"WIRE0_SCL_0": 0,
"WIRE0_SCL_1": 1,
"WIRE0_SCL_2": 2,
"WIRE0_SCL_3": 3,
"WIRE0_SCL_4": 5,
"WIRE0_SCL_5": 7,
"WIRE0_SCL_6": 9,
"WIRE0_SCL_7": 10,
"WIRE0_SCL_8": 11,
"WIRE0_SCL_9": 12,
"WIRE0_SCL_10": 19,
"WIRE0_SCL_11": 24,
"WIRE0_SCL_12": 25,
"WIRE0_SDA_0": 0,
"WIRE0_SDA_1": 1,
"WIRE0_SDA_2": 2,
"WIRE0_SDA_3": 3,
"WIRE0_SDA_4": 5,
"WIRE0_SDA_5": 7,
"WIRE0_SDA_6": 9,
"WIRE0_SDA_7": 10,
"WIRE0_SDA_8": 11,
"WIRE0_SDA_9": 12,
"WIRE0_SDA_10": 19,
"WIRE0_SDA_11": 24,
"WIRE0_SDA_12": 25,
"WIRE0_SCL_0": 7,
"WIRE0_SCL_1": 12,
"WIRE0_SCL_2": 3,
"WIRE0_SCL_3": 10,
"WIRE0_SCL_4": 2,
"WIRE0_SCL_5": 0,
"WIRE0_SCL_6": 19,
"WIRE0_SCL_7": 11,
"WIRE0_SCL_8": 9,
"WIRE0_SCL_9": 24,
"WIRE0_SCL_10": 25,
"WIRE0_SCL_11": 5,
"WIRE0_SCL_12": 1,
"WIRE0_SDA_0": 7,
"WIRE0_SDA_1": 12,
"WIRE0_SDA_2": 3,
"WIRE0_SDA_3": 10,
"WIRE0_SDA_4": 2,
"WIRE0_SDA_5": 0,
"WIRE0_SDA_6": 19,
"WIRE0_SDA_7": 11,
"WIRE0_SDA_8": 9,
"WIRE0_SDA_9": 24,
"WIRE0_SDA_10": 25,
"WIRE0_SDA_11": 5,
"WIRE0_SDA_12": 1,
"SERIAL0_RX": 3,
"SERIAL0_TX": 2,
"SERIAL1_RX": 24,
@@ -299,24 +301,24 @@ LN882X_BOARD_PINS = {
"A2": 1,
},
"ln-02": {
"WIRE0_SCL_0": 0,
"WIRE0_SCL_1": 1,
"WIRE0_SCL_2": 2,
"WIRE0_SCL_3": 3,
"WIRE0_SCL_4": 9,
"WIRE0_SCL_5": 11,
"WIRE0_SCL_6": 19,
"WIRE0_SCL_7": 24,
"WIRE0_SCL_8": 25,
"WIRE0_SDA_0": 0,
"WIRE0_SDA_1": 1,
"WIRE0_SDA_2": 2,
"WIRE0_SDA_3": 3,
"WIRE0_SDA_4": 9,
"WIRE0_SDA_5": 11,
"WIRE0_SDA_6": 19,
"WIRE0_SDA_7": 24,
"WIRE0_SDA_8": 25,
"WIRE0_SCL_0": 11,
"WIRE0_SCL_1": 19,
"WIRE0_SCL_2": 3,
"WIRE0_SCL_3": 24,
"WIRE0_SCL_4": 2,
"WIRE0_SCL_5": 25,
"WIRE0_SCL_6": 1,
"WIRE0_SCL_7": 0,
"WIRE0_SCL_8": 9,
"WIRE0_SDA_0": 11,
"WIRE0_SDA_1": 19,
"WIRE0_SDA_2": 3,
"WIRE0_SDA_3": 24,
"WIRE0_SDA_4": 2,
"WIRE0_SDA_5": 25,
"WIRE0_SDA_6": 1,
"WIRE0_SDA_7": 0,
"WIRE0_SDA_8": 9,
"SERIAL0_RX": 3,
"SERIAL0_TX": 2,
"SERIAL1_RX": 24,

View File

@@ -36,9 +36,8 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
#endif
// Fast path: main thread, no recursion (99.9% of all logs)
// Pass nullptr for thread_name since we already know this is the main task
if (is_main_task && !this->main_task_recursion_guard_) [[likely]] {
this->log_message_to_buffer_and_send_(this->main_task_recursion_guard_, level, tag, line, format, args, nullptr);
this->log_message_to_buffer_and_send_(this->main_task_recursion_guard_, level, tag, line, format, args);
return;
}
@@ -48,23 +47,21 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
}
// Non-main thread handling (~0.1% of logs)
// Resolve thread name once and pass it through the logging chain.
// ESP32/LibreTiny: use TaskHandle_t overload to avoid redundant xTaskGetCurrentTaskHandle()
// (we already have the handle from the main task check above).
// Host: pass a stack buffer for pthread_getname_np to write into.
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
const char *thread_name = get_thread_name_(current_task);
this->log_vprintf_non_main_thread_(level, tag, line, format, args, current_task);
#else // USE_HOST
char thread_name_buf[THREAD_NAME_BUF_SIZE];
const char *thread_name = this->get_thread_name_(thread_name_buf);
this->log_vprintf_non_main_thread_(level, tag, line, format, args);
#endif
this->log_vprintf_non_main_thread_(level, tag, line, format, args, thread_name);
}
// Handles non-main thread logging only
// Kept separate from hot path to improve instruction cache performance
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args,
const char *thread_name) {
TaskHandle_t current_task) {
#else // USE_HOST
void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args) {
#endif
// Check if already in recursion for this non-main thread/task
if (this->is_non_main_task_recursive_()) {
return;
@@ -76,8 +73,12 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li
bool message_sent = false;
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
// For non-main threads/tasks, queue the message for callbacks
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
message_sent =
this->log_buffer_->send_message_thread_safe(level, tag, static_cast<uint16_t>(line), thread_name, format, args);
this->log_buffer_->send_message_thread_safe(level, tag, static_cast<uint16_t>(line), current_task, format, args);
#else // USE_HOST
message_sent = this->log_buffer_->send_message_thread_safe(level, tag, static_cast<uint16_t>(line), format, args);
#endif
if (message_sent) {
// Enable logger loop to process the buffered message
// This is safe to call from any context including ISRs
@@ -100,27 +101,19 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li
#endif
char console_buffer[MAX_CONSOLE_LOG_MSG_SIZE]; // MUST be stack allocated for thread safety
LogBuffer buf{console_buffer, MAX_CONSOLE_LOG_MSG_SIZE};
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf, thread_name);
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf);
this->write_to_console_(buf);
}
// RAII guard automatically resets on return
}
#else
// Implementation for single-task platforms (ESP8266, RP2040, Zephyr)
// TODO: Zephyr may have multiple threads (work queues, etc.) but uses this single-task path.
// Logging calls are NOT thread-safe: global_recursion_guard_ is a plain bool and tx_buffer_ has no locking.
// Not a problem in practice yet since Zephyr has no API support (logs are console-only).
// Implementation for all other platforms (single-task, no threading)
void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const char *format, va_list args) { // NOLINT
if (level > this->level_for(tag) || global_recursion_guard_)
return;
#ifdef USE_ZEPHYR
char tmp[MAX_POINTER_REPRESENTATION];
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args,
this->get_thread_name_(tmp));
#else // Other single-task platforms don't have thread names, so pass nullptr
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args, nullptr);
#endif
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args);
}
#endif // USE_ESP32 / USE_HOST / USE_LIBRETINY
@@ -136,7 +129,7 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas
if (level > this->level_for(tag) || global_recursion_guard_)
return;
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args, nullptr);
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args);
}
#endif // USE_STORE_LOG_STR_IN_FLASH

View File

@@ -2,7 +2,6 @@
#include <cstdarg>
#include <map>
#include <span>
#include <type_traits>
#if defined(USE_ESP32) || defined(USE_HOST)
#include <pthread.h>
@@ -125,10 +124,6 @@ static constexpr uint16_t MAX_HEADER_SIZE = 128;
// "0x" + 2 hex digits per byte + '\0'
static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1;
// Stack buffer size for retrieving thread/task names from the OS
// macOS allows up to 64 bytes, Linux up to 16
static constexpr size_t THREAD_NAME_BUF_SIZE = 64;
// Buffer wrapper for log formatting functions
struct LogBuffer {
char *data;
@@ -413,24 +408,34 @@ class Logger : public Component {
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY)
// Handles non-main thread logging only (~0.1% of calls)
// thread_name is resolved by the caller from the task handle, avoiding redundant lookups
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
// ESP32/LibreTiny: Pass task handle to avoid calling xTaskGetCurrentTaskHandle() twice
void log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args,
const char *thread_name);
TaskHandle_t current_task);
#else // USE_HOST
// Host: No task handle parameter needed (not used in send_message_thread_safe)
void log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args);
#endif
#endif
void process_messages_();
void write_msg_(const char *msg, uint16_t len);
// Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator
// thread_name: name of the calling thread/task, or nullptr for main task (callers already know which task they're on)
inline void HOT format_log_to_buffer_with_terminator_(uint8_t level, const char *tag, int line, const char *format,
va_list args, LogBuffer &buf, const char *thread_name) {
buf.write_header(level, tag, line, thread_name);
va_list args, LogBuffer &buf) {
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_HOST)
buf.write_header(level, tag, line, this->get_thread_name_());
#elif defined(USE_ZEPHYR)
char tmp[MAX_POINTER_REPRESENTATION];
buf.write_header(level, tag, line, this->get_thread_name_(tmp));
#else
buf.write_header(level, tag, line, nullptr);
#endif
buf.format_body(format, args);
}
#ifdef USE_STORE_LOG_STR_IN_FLASH
// Format a log message with flash string format and write it to a buffer with header, footer, and null terminator
// ESP8266-only (single-task), thread_name is always nullptr
inline void HOT format_log_to_buffer_with_terminator_P_(uint8_t level, const char *tag, int line,
const __FlashStringHelper *format, va_list args,
LogBuffer &buf) {
@@ -461,10 +466,9 @@ class Logger : public Component {
// Helper to format and send a log message to both console and listeners
// Template handles both const char* (RAM) and __FlashStringHelper* (flash) format strings
// thread_name: name of the calling thread/task, or nullptr for main task
template<typename FormatType>
inline void HOT log_message_to_buffer_and_send_(bool &recursion_guard, uint8_t level, const char *tag, int line,
FormatType format, va_list args, const char *thread_name) {
FormatType format, va_list args) {
RecursionGuard guard(recursion_guard);
LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_};
#ifdef USE_STORE_LOG_STR_IN_FLASH
@@ -473,7 +477,7 @@ class Logger : public Component {
} else
#endif
{
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf, thread_name);
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf);
}
this->notify_listeners_(level, tag, buf);
this->write_log_buffer_to_console_(buf);
@@ -561,57 +565,37 @@ class Logger : public Component {
bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms
#endif
// --- get_thread_name_ overloads (per-platform) ---
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
// Primary overload - takes a task handle directly to avoid redundant xTaskGetCurrentTaskHandle() calls
// when the caller already has the handle (e.g. from the main task check in log_vprintf_)
const char *get_thread_name_(TaskHandle_t task) {
if (task == this->main_task_) {
return nullptr; // Main task
}
#if defined(USE_ESP32)
return pcTaskGetName(task);
#elif defined(USE_LIBRETINY)
return pcTaskGetTaskName(task);
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
const char *HOT get_thread_name_(
#ifdef USE_ZEPHYR
char *buff
#endif
}
// Convenience overload - gets the current task handle and delegates
const char *HOT get_thread_name_() { return this->get_thread_name_(xTaskGetCurrentTaskHandle()); }
#elif defined(USE_HOST)
// Takes a caller-provided buffer for the thread name (stack-allocated for thread safety)
const char *HOT get_thread_name_(std::span<char> buff) {
pthread_t current_thread = pthread_self();
if (pthread_equal(current_thread, main_thread_)) {
return nullptr; // Main thread
}
// For non-main threads, get the thread name into the caller-provided buffer
if (pthread_getname_np(current_thread, buff.data(), buff.size()) == 0) {
return buff.data();
}
return nullptr;
}
#elif defined(USE_ZEPHYR)
const char *HOT get_thread_name_(std::span<char> buff) {
) {
#ifdef USE_ZEPHYR
k_tid_t current_task = k_current_get();
#else
TaskHandle_t current_task = xTaskGetCurrentTaskHandle();
#endif
if (current_task == main_task_) {
return nullptr; // Main task
} else {
#if defined(USE_ESP32)
return pcTaskGetName(current_task);
#elif defined(USE_LIBRETINY)
return pcTaskGetTaskName(current_task);
#elif defined(USE_ZEPHYR)
const char *name = k_thread_name_get(current_task);
if (name) {
// zephyr print task names only if debug component is present
return name;
}
std::snprintf(buff, MAX_POINTER_REPRESENTATION, "%p", current_task);
return buff;
#endif
}
const char *name = k_thread_name_get(current_task);
if (name) {
// zephyr print task names only if debug component is present
return name;
}
std::snprintf(buff.data(), buff.size(), "%p", current_task);
return buff.data();
}
#endif
// --- Non-main task recursion guards (per-platform) ---
#if defined(USE_ESP32) || defined(USE_HOST)
// RAII guard for non-main task recursion using pthread TLS
class NonMainTaskRecursionGuard {
@@ -651,6 +635,22 @@ class Logger : public Component {
inline RecursionGuard make_non_main_task_guard_() { return RecursionGuard(non_main_task_recursion_guard_); }
#endif
#ifdef USE_HOST
const char *HOT get_thread_name_() {
pthread_t current_thread = pthread_self();
if (pthread_equal(current_thread, main_thread_)) {
return nullptr; // Main thread
}
// For non-main threads, return the thread name
// We store it in thread-local storage to avoid allocation
static thread_local char thread_name_buf[32];
if (pthread_getname_np(current_thread, thread_name_buf, sizeof(thread_name_buf)) == 0) {
return thread_name_buf;
}
return nullptr;
}
#endif
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
// Disable loop when task buffer is empty (with USB CDC check on ESP32)
inline void disable_loop_when_buffer_empty_() {

View File

@@ -1,51 +0,0 @@
#ifdef USE_ESP8266
#include "logger.h"
#include "esphome/core/log.h"
namespace esphome::logger {
static const char *const TAG = "logger";
void Logger::pre_setup() {
if (this->baud_rate_ > 0) {
switch (this->uart_) {
case UART_SELECTION_UART0:
case UART_SELECTION_UART0_SWAP:
this->hw_serial_ = &Serial;
Serial.begin(this->baud_rate_);
if (this->uart_ == UART_SELECTION_UART0_SWAP) {
Serial.swap();
}
Serial.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE);
break;
case UART_SELECTION_UART1:
this->hw_serial_ = &Serial1;
Serial1.begin(this->baud_rate_);
Serial1.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE);
break;
}
} else {
uart_set_debug(UART_NO);
}
global_logger = this;
ESP_LOGI(TAG, "Log initialized");
}
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
const LogString *Logger::get_uart_selection_() {
switch (this->uart_) {
case UART_SELECTION_UART0:
return LOG_STR("UART0");
case UART_SELECTION_UART1:
return LOG_STR("UART1");
case UART_SELECTION_UART0_SWAP:
default:
return LOG_STR("UART0_SWAP");
}
}
} // namespace esphome::logger
#endif

View File

@@ -1,22 +0,0 @@
#if defined(USE_HOST)
#include "logger.h"
namespace esphome::logger {
void HOT Logger::write_msg_(const char *msg) {
time_t rawtime;
struct tm *timeinfo;
char buffer[80];
time(&rawtime);
timeinfo = localtime(&rawtime);
strftime(buffer, sizeof buffer, "[%H:%M:%S]", timeinfo);
fputs(buffer, stdout);
puts(msg);
}
void Logger::pre_setup() { global_logger = this; }
} // namespace esphome::logger
#endif

View File

@@ -1,70 +0,0 @@
#ifdef USE_LIBRETINY
#include "logger.h"
namespace esphome::logger {
static const char *const TAG = "logger";
void Logger::pre_setup() {
if (this->baud_rate_ > 0) {
switch (this->uart_) {
#if LT_HW_UART0
case UART_SELECTION_UART0:
this->hw_serial_ = &Serial0;
Serial0.begin(this->baud_rate_);
break;
#endif
#if LT_HW_UART1
case UART_SELECTION_UART1:
this->hw_serial_ = &Serial1;
Serial1.begin(this->baud_rate_);
break;
#endif
#if LT_HW_UART2
case UART_SELECTION_UART2:
this->hw_serial_ = &Serial2;
Serial2.begin(this->baud_rate_);
break;
#endif
default:
this->hw_serial_ = &Serial;
Serial.begin(this->baud_rate_);
if (this->uart_ != UART_SELECTION_DEFAULT) {
ESP_LOGW(TAG, " The chosen logger UART port is not available on this board."
"The default port was used instead.");
}
break;
}
// change lt_log() port to match default Serial
if (this->uart_ == UART_SELECTION_DEFAULT) {
this->uart_ = (UARTSelection) (LT_UART_DEFAULT_SERIAL + 1);
lt_log_set_port(LT_UART_DEFAULT_SERIAL);
} else {
lt_log_set_port(this->uart_ - 1);
}
}
global_logger = this;
ESP_LOGI(TAG, "Log initialized");
}
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
const LogString *Logger::get_uart_selection_() {
switch (this->uart_) {
case UART_SELECTION_DEFAULT:
return LOG_STR("DEFAULT");
case UART_SELECTION_UART0:
return LOG_STR("UART0");
case UART_SELECTION_UART1:
return LOG_STR("UART1");
case UART_SELECTION_UART2:
default:
return LOG_STR("UART2");
}
}
} // namespace esphome::logger
#endif // USE_LIBRETINY

View File

@@ -1,48 +0,0 @@
#ifdef USE_RP2040
#include "logger.h"
#include "esphome/core/log.h"
namespace esphome::logger {
static const char *const TAG = "logger";
void Logger::pre_setup() {
if (this->baud_rate_ > 0) {
switch (this->uart_) {
case UART_SELECTION_UART0:
this->hw_serial_ = &Serial1;
Serial1.begin(this->baud_rate_);
break;
case UART_SELECTION_UART1:
this->hw_serial_ = &Serial2;
Serial2.begin(this->baud_rate_);
break;
case UART_SELECTION_USB_CDC:
this->hw_serial_ = &Serial;
Serial.begin(this->baud_rate_);
break;
}
}
global_logger = this;
ESP_LOGI(TAG, "Log initialized");
}
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
const LogString *Logger::get_uart_selection_() {
switch (this->uart_) {
case UART_SELECTION_UART0:
return LOG_STR("UART0");
case UART_SELECTION_UART1:
return LOG_STR("UART1");
#ifdef USE_LOGGER_USB_CDC
case UART_SELECTION_USB_CDC:
return LOG_STR("USB_CDC");
#endif
default:
return LOG_STR("UNKNOWN");
}
}
} // namespace esphome::logger
#endif // USE_RP2040

View File

@@ -1,96 +0,0 @@
#ifdef USE_ZEPHYR
#include "esphome/core/application.h"
#include "esphome/core/log.h"
#include "logger.h"
#include <zephyr/device.h>
#include <zephyr/drivers/uart.h>
#include <zephyr/usb/usb_device.h>
namespace esphome::logger {
static const char *const TAG = "logger";
#ifdef USE_LOGGER_USB_CDC
void Logger::loop() {
if (this->uart_ != UART_SELECTION_USB_CDC || nullptr == this->uart_dev_) {
return;
}
static bool opened = false;
uint32_t dtr = 0;
uart_line_ctrl_get(this->uart_dev_, UART_LINE_CTRL_DTR, &dtr);
/* Poll if the DTR flag was set, optional */
if (opened == dtr) {
return;
}
if (!opened) {
App.schedule_dump_config();
}
opened = !opened;
}
#endif
void Logger::pre_setup() {
if (this->baud_rate_ > 0) {
static const struct device *uart_dev = nullptr;
switch (this->uart_) {
case UART_SELECTION_UART0:
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(uart0));
break;
case UART_SELECTION_UART1:
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(uart1));
break;
#ifdef USE_LOGGER_USB_CDC
case UART_SELECTION_USB_CDC:
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(cdc_acm_uart0));
if (device_is_ready(uart_dev)) {
usb_enable(nullptr);
}
break;
#endif
}
if (!device_is_ready(uart_dev)) {
ESP_LOGE(TAG, "%s is not ready.", LOG_STR_ARG(get_uart_selection_()));
} else {
this->uart_dev_ = uart_dev;
}
}
global_logger = this;
ESP_LOGI(TAG, "Log initialized");
}
void HOT Logger::write_msg_(const char *msg) {
#ifdef CONFIG_PRINTK
printk("%s\n", msg);
#endif
if (nullptr == this->uart_dev_) {
return;
}
while (*msg) {
uart_poll_out(this->uart_dev_, *msg);
++msg;
}
uart_poll_out(this->uart_dev_, '\n');
}
const LogString *Logger::get_uart_selection_() {
switch (this->uart_) {
case UART_SELECTION_UART0:
return LOG_STR("UART0");
case UART_SELECTION_UART1:
return LOG_STR("UART1");
#ifdef USE_LOGGER_USB_CDC
case UART_SELECTION_USB_CDC:
return LOG_STR("USB_CDC");
#endif
default:
return LOG_STR("UNKNOWN");
}
}
} // namespace esphome::logger
#endif

View File

@@ -59,7 +59,7 @@ void TaskLogBuffer::release_message_main_loop(void *token) {
last_processed_counter_ = message_counter_.load(std::memory_order_relaxed);
}
bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, TaskHandle_t task_handle,
const char *format, va_list args) {
// First, calculate the exact length needed using a null buffer (no actual writing)
va_list args_copy;
@@ -95,6 +95,7 @@ bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uin
// Store the thread name now instead of waiting until main loop processing
// This avoids crashes if the task completes or is deleted between when this message
// is enqueued and when it's processed by the main loop
const char *thread_name = pcTaskGetName(task_handle);
if (thread_name != nullptr) {
strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1);
msg->thread_name[sizeof(msg->thread_name) - 1] = '\0'; // Ensure null termination

View File

@@ -58,7 +58,7 @@ class TaskLogBuffer {
void release_message_main_loop(void *token);
// Thread-safe - send a message to the ring buffer from any thread
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, TaskHandle_t task_handle,
const char *format, va_list args);
// Check if there are messages ready to be processed using an atomic counter for performance

View File

@@ -70,8 +70,8 @@ void TaskLogBufferHost::commit_write_slot_(int slot_index) {
}
}
bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
const char *format, va_list args) {
bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *format,
va_list args) {
// Acquire a slot
int slot_index = this->acquire_write_slot_();
if (slot_index < 0) {
@@ -85,9 +85,11 @@ bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag,
msg.tag = tag;
msg.line = line;
// Store the thread name now to avoid crashes if thread exits before processing
if (thread_name != nullptr) {
strncpy(msg.thread_name, thread_name, sizeof(msg.thread_name) - 1);
// Get thread name using pthread
char thread_name_buf[LogMessage::MAX_THREAD_NAME_SIZE];
// pthread_getname_np works the same on Linux and macOS
if (pthread_getname_np(pthread_self(), thread_name_buf, sizeof(thread_name_buf)) == 0) {
strncpy(msg.thread_name, thread_name_buf, sizeof(msg.thread_name) - 1);
msg.thread_name[sizeof(msg.thread_name) - 1] = '\0';
} else {
msg.thread_name[0] = '\0';

View File

@@ -86,8 +86,7 @@ class TaskLogBufferHost {
// Thread-safe - send a message to the buffer from any thread
// Returns true if message was queued, false if buffer is full
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
const char *format, va_list args);
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *format, va_list args);
// Check if there are messages ready to be processed
inline bool HOT has_messages() const {

View File

@@ -101,7 +101,7 @@ void TaskLogBufferLibreTiny::release_message_main_loop() {
}
bool TaskLogBufferLibreTiny::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line,
const char *thread_name, const char *format, va_list args) {
TaskHandle_t task_handle, const char *format, va_list args) {
// First, calculate the exact length needed using a null buffer (no actual writing)
va_list args_copy;
va_copy(args_copy, args);
@@ -162,6 +162,7 @@ bool TaskLogBufferLibreTiny::send_message_thread_safe(uint8_t level, const char
msg->line = line;
// Store the thread name now to avoid crashes if task is deleted before processing
const char *thread_name = pcTaskGetTaskName(task_handle);
if (thread_name != nullptr) {
strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1);
msg->thread_name[sizeof(msg->thread_name) - 1] = '\0';

View File

@@ -70,7 +70,7 @@ class TaskLogBufferLibreTiny {
void release_message_main_loop();
// Thread-safe - send a message to the buffer from any thread
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, TaskHandle_t task_handle,
const char *format, va_list args);
// Fast check using volatile counter - no lock needed

View File

@@ -56,7 +56,7 @@ void MCP23016::pin_mode(uint8_t pin, gpio::Flags flags) {
this->update_reg_(pin, false, iodir);
}
}
float MCP23016::get_setup_priority() const { return setup_priority::IO; }
float MCP23016::get_setup_priority() const { return setup_priority::HARDWARE; }
bool MCP23016::read_reg_(uint8_t reg, uint8_t *value) {
if (this->is_failed())
return false;

View File

@@ -120,101 +120,3 @@ DriverChip(
(0xB2, 0x10),
],
)
DriverChip(
"WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-3.4C",
height=800,
width=800,
hsync_back_porch=20,
hsync_pulse_width=20,
hsync_front_porch=40,
vsync_back_porch=12,
vsync_pulse_width=4,
vsync_front_porch=24,
pclk_frequency="80MHz",
lane_bit_rate="1.5Gbps",
swap_xy=cv.UNDEFINED,
color_order="RGB",
initsequence=[
(0xE0, 0x00), # select userpage
(0xE1, 0x93), (0xE2, 0x65), (0xE3, 0xF8),
(0x80, 0x01), # Select number of lanes (2)
(0xE0, 0x01), # select page 1
(0x00, 0x00), (0x01, 0x41), (0x03, 0x10), (0x04, 0x44), (0x17, 0x00), (0x18, 0xD0), (0x19, 0x00), (0x1A, 0x00),
(0x1B, 0xD0), (0x1C, 0x00), (0x24, 0xFE), (0x35, 0x26), (0x37, 0x09), (0x38, 0x04), (0x39, 0x08), (0x3A, 0x0A),
(0x3C, 0x78), (0x3D, 0xFF), (0x3E, 0xFF), (0x3F, 0xFF), (0x40, 0x00), (0x41, 0x64), (0x42, 0xC7), (0x43, 0x18),
(0x44, 0x0B), (0x45, 0x14), (0x55, 0x02), (0x57, 0x49), (0x59, 0x0A), (0x5A, 0x1B), (0x5B, 0x19), (0x5D, 0x7F),
(0x5E, 0x56), (0x5F, 0x43), (0x60, 0x37), (0x61, 0x33), (0x62, 0x25), (0x63, 0x2A), (0x64, 0x16), (0x65, 0x30),
(0x66, 0x2F), (0x67, 0x32), (0x68, 0x53), (0x69, 0x43), (0x6A, 0x4C), (0x6B, 0x40), (0x6C, 0x3D), (0x6D, 0x31),
(0x6E, 0x20), (0x6F, 0x0F), (0x70, 0x7F), (0x71, 0x56), (0x72, 0x43), (0x73, 0x37), (0x74, 0x33), (0x75, 0x25),
(0x76, 0x2A), (0x77, 0x16), (0x78, 0x30), (0x79, 0x2F), (0x7A, 0x32), (0x7B, 0x53), (0x7C, 0x43), (0x7D, 0x4C),
(0x7E, 0x40), (0x7F, 0x3D), (0x80, 0x31), (0x81, 0x20), (0x82, 0x0F),
(0xE0, 0x02), # select page 2
(0x00, 0x5F), (0x01, 0x5F), (0x02, 0x5E), (0x03, 0x5E), (0x04, 0x50), (0x05, 0x48), (0x06, 0x48), (0x07, 0x4A),
(0x08, 0x4A), (0x09, 0x44), (0x0A, 0x44), (0x0B, 0x46), (0x0C, 0x46), (0x0D, 0x5F), (0x0E, 0x5F), (0x0F, 0x57),
(0x10, 0x57), (0x11, 0x77), (0x12, 0x77), (0x13, 0x40), (0x14, 0x42), (0x15, 0x5F), (0x16, 0x5F), (0x17, 0x5F),
(0x18, 0x5E), (0x19, 0x5E), (0x1A, 0x50), (0x1B, 0x49), (0x1C, 0x49), (0x1D, 0x4B), (0x1E, 0x4B), (0x1F, 0x45),
(0x20, 0x45), (0x21, 0x47), (0x22, 0x47), (0x23, 0x5F), (0x24, 0x5F), (0x25, 0x57), (0x26, 0x57), (0x27, 0x77),
(0x28, 0x77), (0x29, 0x41), (0x2A, 0x43), (0x2B, 0x5F), (0x2C, 0x1E), (0x2D, 0x1E), (0x2E, 0x1F), (0x2F, 0x1F),
(0x30, 0x10), (0x31, 0x07), (0x32, 0x07), (0x33, 0x05), (0x34, 0x05), (0x35, 0x0B), (0x36, 0x0B), (0x37, 0x09),
(0x38, 0x09), (0x39, 0x1F), (0x3A, 0x1F), (0x3B, 0x17), (0x3C, 0x17), (0x3D, 0x17), (0x3E, 0x17), (0x3F, 0x03),
(0x40, 0x01), (0x41, 0x1F), (0x42, 0x1E), (0x43, 0x1E), (0x44, 0x1F), (0x45, 0x1F), (0x46, 0x10), (0x47, 0x06),
(0x48, 0x06), (0x49, 0x04), (0x4A, 0x04), (0x4B, 0x0A), (0x4C, 0x0A), (0x4D, 0x08), (0x4E, 0x08), (0x4F, 0x1F),
(0x50, 0x1F), (0x51, 0x17), (0x52, 0x17), (0x53, 0x17), (0x54, 0x17), (0x55, 0x02), (0x56, 0x00), (0x57, 0x1F),
(0xE0, 0x02), # select page 2
(0x58, 0x40), (0x59, 0x00), (0x5A, 0x00), (0x5B, 0x30), (0x5C, 0x01), (0x5D, 0x30), (0x5E, 0x01), (0x5F, 0x02),
(0x60, 0x30), (0x61, 0x03), (0x62, 0x04), (0x63, 0x04), (0x64, 0xA6), (0x65, 0x43), (0x66, 0x30), (0x67, 0x73),
(0x68, 0x05), (0x69, 0x04), (0x6A, 0x7F), (0x6B, 0x08), (0x6C, 0x00), (0x6D, 0x04), (0x6E, 0x04), (0x6F, 0x88),
(0x75, 0xD9), (0x76, 0x00), (0x77, 0x33), (0x78, 0x43),
(0xE0, 0x00), # select userpage
],
)
DriverChip(
"WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-4C",
height=720,
width=720,
hsync_back_porch=20,
hsync_pulse_width=20,
hsync_front_porch=40,
vsync_back_porch=12,
vsync_pulse_width=4,
vsync_front_porch=24,
pclk_frequency="80MHz",
lane_bit_rate="1.5Gbps",
swap_xy=cv.UNDEFINED,
color_order="RGB",
initsequence=[
(0xE0, 0x00), # select userpage
(0xE1, 0x93), (0xE2, 0x65), (0xE3, 0xF8),
(0x80, 0x01), # Select number of lanes (2)
(0xE0, 0x01), # select page 1
(0x00, 0x00), (0x01, 0x41), (0x03, 0x10), (0x04, 0x44), (0x17, 0x00), (0x18, 0xD0), (0x19, 0x00), (0x1A, 0x00),
(0x1B, 0xD0), (0x1C, 0x00), (0x24, 0xFE), (0x35, 0x26), (0x37, 0x09), (0x38, 0x04), (0x39, 0x08), (0x3A, 0x0A),
(0x3C, 0x78), (0x3D, 0xFF), (0x3E, 0xFF), (0x3F, 0xFF), (0x40, 0x04), (0x41, 0x64), (0x42, 0xC7), (0x43, 0x18),
(0x44, 0x0B), (0x45, 0x14), (0x55, 0x02), (0x57, 0x49), (0x59, 0x0A), (0x5A, 0x1B), (0x5B, 0x19), (0x5D, 0x7F),
(0x5E, 0x56), (0x5F, 0x43), (0x60, 0x37), (0x61, 0x33), (0x62, 0x25), (0x63, 0x2A), (0x64, 0x16), (0x65, 0x30),
(0x66, 0x2F), (0x67, 0x32), (0x68, 0x53), (0x69, 0x43), (0x6A, 0x4C), (0x6B, 0x40), (0x6C, 0x3D), (0x6D, 0x31),
(0x6E, 0x20), (0x6F, 0x0F), (0x70, 0x7F), (0x71, 0x56), (0x72, 0x43), (0x73, 0x37), (0x74, 0x33), (0x75, 0x25),
(0x76, 0x2A), (0x77, 0x16), (0x78, 0x30), (0x79, 0x2F), (0x7A, 0x32), (0x7B, 0x53), (0x7C, 0x43), (0x7D, 0x4C),
(0x7E, 0x40), (0x7F, 0x3D), (0x80, 0x31), (0x81, 0x20), (0x82, 0x0F),
(0xE0, 0x02), # select page 2
(0x00, 0x5F), (0x01, 0x5F), (0x02, 0x5E), (0x03, 0x5E), (0x04, 0x50), (0x05, 0x48), (0x06, 0x48), (0x07, 0x4A),
(0x08, 0x4A), (0x09, 0x44), (0x0A, 0x44), (0x0B, 0x46), (0x0C, 0x46), (0x0D, 0x5F), (0x0E, 0x5F), (0x0F, 0x57),
(0x10, 0x57), (0x11, 0x77), (0x12, 0x77), (0x13, 0x40), (0x14, 0x42), (0x15, 0x5F), (0x16, 0x5F), (0x17, 0x5F),
(0x18, 0x5E), (0x19, 0x5E), (0x1A, 0x50), (0x1B, 0x49), (0x1C, 0x49), (0x1D, 0x4B), (0x1E, 0x4B), (0x1F, 0x45),
(0x20, 0x45), (0x21, 0x47), (0x22, 0x47), (0x23, 0x5F), (0x24, 0x5F), (0x25, 0x57), (0x26, 0x57), (0x27, 0x77),
(0x28, 0x77), (0x29, 0x41), (0x2A, 0x43), (0x2B, 0x5F), (0x2C, 0x1E), (0x2D, 0x1E), (0x2E, 0x1F), (0x2F, 0x1F),
(0x30, 0x10), (0x31, 0x07), (0x32, 0x07), (0x33, 0x05), (0x34, 0x05), (0x35, 0x0B), (0x36, 0x0B), (0x37, 0x09),
(0x38, 0x09), (0x39, 0x1F), (0x3A, 0x1F), (0x3B, 0x17), (0x3C, 0x17), (0x3D, 0x17), (0x3E, 0x17), (0x3F, 0x03),
(0x40, 0x01), (0x41, 0x1F), (0x42, 0x1E), (0x43, 0x1E), (0x44, 0x1F), (0x45, 0x1F), (0x46, 0x10), (0x47, 0x06),
(0x48, 0x06), (0x49, 0x04), (0x4A, 0x04), (0x4B, 0x0A), (0x4C, 0x0A), (0x4D, 0x08), (0x4E, 0x08), (0x4F, 0x1F),
(0x50, 0x1F), (0x51, 0x17), (0x52, 0x17), (0x53, 0x17), (0x54, 0x17), (0x55, 0x02), (0x56, 0x00), (0x57, 0x1F),
(0xE0, 0x02), # select page 2
(0x58, 0x40), (0x59, 0x00), (0x5A, 0x00), (0x5B, 0x30), (0x5C, 0x01), (0x5D, 0x30), (0x5E, 0x01), (0x5F, 0x02),
(0x60, 0x30), (0x61, 0x03), (0x62, 0x04), (0x63, 0x04), (0x64, 0xA6), (0x65, 0x43), (0x66, 0x30), (0x67, 0x73),
(0x68, 0x05), (0x69, 0x04), (0x6A, 0x7F), (0x6B, 0x08), (0x6C, 0x00), (0x6D, 0x04), (0x6E, 0x04), (0x6F, 0x88),
(0x75, 0xD9), (0x76, 0x00), (0x77, 0x33), (0x78, 0x43),
(0xE0, 0x00), # select userpage
]
)

View File

@@ -11,7 +11,7 @@ from esphome.components.const import (
CONF_DRAW_ROUNDING,
)
from esphome.components.display import CONF_SHOW_TEST_CARD
from esphome.components.esp32 import VARIANT_ESP32P4, VARIANT_ESP32S3, only_on_variant
from esphome.components.esp32 import VARIANT_ESP32S3, only_on_variant
from esphome.components.mipi import (
COLOR_ORDERS,
CONF_DE_PIN,
@@ -225,7 +225,7 @@ def _config_schema(config):
return cv.All(
schema,
cv.only_on_esp32,
only_on_variant(supported=[VARIANT_ESP32S3, VARIANT_ESP32P4]),
only_on_variant(supported=[VARIANT_ESP32S3]),
)(config)

View File

@@ -1,4 +1,4 @@
#if defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4)
#ifdef USE_ESP32_VARIANT_ESP32S3
#include "mipi_rgb.h"
#include "esphome/core/gpio.h"
#include "esphome/core/hal.h"
@@ -401,4 +401,4 @@ void MipiRgb::dump_config() {
} // namespace mipi_rgb
} // namespace esphome
#endif // defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4)
#endif // USE_ESP32_VARIANT_ESP32S3

View File

@@ -1,6 +1,6 @@
#pragma once
#if defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4)
#ifdef USE_ESP32_VARIANT_ESP32S3
#include "esphome/core/gpio.h"
#include "esphome/components/display/display.h"
#include "esp_lcd_panel_ops.h"
@@ -28,7 +28,7 @@ class MipiRgb : public display::Display {
void setup() override;
void loop() override;
void update() override;
void fill(Color color) override;
void fill(Color color);
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
void write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset,
@@ -115,7 +115,7 @@ class MipiRgbSpi : public MipiRgb,
void write_command_(uint8_t value);
void write_data_(uint8_t value);
void write_init_sequence_();
void dump_config() override;
void dump_config();
GPIOPin *dc_pin_{nullptr};
std::vector<uint8_t> init_sequence_;

View File

@@ -1,6 +1,6 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components import audio, esp32, socket, speaker
from esphome.components import audio, esp32, speaker
import esphome.config_validation as cv
from esphome.const import (
CONF_BITS_PER_SAMPLE,
@@ -61,7 +61,7 @@ def _set_stream_limits(config):
def _validate_source_speaker(config):
fconf = fv.full_config.get()
# Get ID for the output speaker and add it to the source speakers config to easily inherit properties
# Get ID for the output speaker and add it to the source speakrs config to easily inherit properties
path = fconf.get_path_for_id(config[CONF_ID])[:-3]
path.append(CONF_OUTPUT_SPEAKER)
output_speaker_id = fconf.get_config_for_path(path)
@@ -111,9 +111,6 @@ FINAL_VALIDATE_SCHEMA = cv.All(
async def to_code(config):
# Enable wake_loop_threadsafe for immediate command processing from other tasks
socket.require_wake_loop_threadsafe()
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
@@ -130,9 +127,6 @@ async def to_code(config):
"CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True
)
# Initialize FixedVector with exact count of source speakers
cg.add(var.init_source_speakers(len(config[CONF_SOURCE_SPEAKERS])))
for speaker_config in config[CONF_SOURCE_SPEAKERS]:
source_speaker = cg.new_Pvariable(speaker_config[CONF_ID])

View File

@@ -8,8 +8,8 @@
namespace esphome {
namespace mixer_speaker {
template<typename... Ts> class DuckingApplyAction : public Action<Ts...>, public Parented<SourceSpeaker> {
TEMPLATABLE_VALUE(uint8_t, decibel_reduction);
TEMPLATABLE_VALUE(uint32_t, duration);
TEMPLATABLE_VALUE(uint8_t, decibel_reduction)
TEMPLATABLE_VALUE(uint32_t, duration)
void play(const Ts &...x) override {
this->parent_->apply_ducking(this->decibel_reduction_.value(x...), this->duration_.value(x...));
}

View File

@@ -2,13 +2,11 @@
#ifdef USE_ESP32
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <algorithm>
#include <array>
#include <cstring>
namespace esphome {
@@ -16,7 +14,6 @@ namespace mixer_speaker {
static const UBaseType_t MIXER_TASK_PRIORITY = 10;
static const uint32_t STOPPING_TIMEOUT_MS = 5000;
static const uint32_t TRANSFER_BUFFER_DURATION_MS = 50;
static const uint32_t TASK_DELAY_MS = 25;
@@ -30,53 +27,21 @@ static const char *const TAG = "speaker_mixer";
// Gives the Q15 fixed point scaling factor to reduce by 0 dB, 1dB, ..., 50 dB
// dB to PCM scaling factor formula: floating_point_scale_factor = 2^(-db/6.014)
// float to Q15 fixed point formula: q15_scale_factor = floating_point_scale_factor * 2^(15)
static const std::array<int16_t, 51> DECIBEL_REDUCTION_TABLE = {
static const std::vector<int16_t> DECIBEL_REDUCTION_TABLE = {
32767, 29201, 26022, 23189, 20665, 18415, 16410, 14624, 13032, 11613, 10349, 9222, 8218, 7324, 6527, 5816, 5183,
4619, 4116, 3668, 3269, 2913, 2596, 2313, 2061, 1837, 1637, 1459, 1300, 1158, 1032, 920, 820, 731,
651, 580, 517, 461, 411, 366, 326, 291, 259, 231, 206, 183, 163, 146, 130, 116, 103};
// Event bits for SourceSpeaker command processing
enum SourceSpeakerEventBits : uint32_t {
SOURCE_SPEAKER_COMMAND_START = (1 << 0),
SOURCE_SPEAKER_COMMAND_STOP = (1 << 1),
SOURCE_SPEAKER_COMMAND_FINISH = (1 << 2),
enum MixerEventGroupBits : uint32_t {
COMMAND_STOP = (1 << 0), // stops the mixer task
STATE_STARTING = (1 << 10),
STATE_RUNNING = (1 << 11),
STATE_STOPPING = (1 << 12),
STATE_STOPPED = (1 << 13),
ERR_ESP_NO_MEM = (1 << 19),
ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits
};
// Event bits for mixer task control and state
enum MixerTaskEventBits : uint32_t {
MIXER_TASK_COMMAND_START = (1 << 0),
MIXER_TASK_COMMAND_STOP = (1 << 1),
MIXER_TASK_STATE_STARTING = (1 << 10),
MIXER_TASK_STATE_RUNNING = (1 << 11),
MIXER_TASK_STATE_STOPPING = (1 << 12),
MIXER_TASK_STATE_STOPPED = (1 << 13),
MIXER_TASK_ERR_ESP_NO_MEM = (1 << 19),
MIXER_TASK_ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits
};
static inline uint32_t atomic_subtract_clamped(std::atomic<uint32_t> &var, uint32_t amount) {
uint32_t current = var.load(std::memory_order_acquire);
uint32_t subtracted = 0;
if (current > 0) {
uint32_t new_value;
do {
subtracted = std::min(amount, current);
new_value = current - subtracted;
} while (!var.compare_exchange_weak(current, new_value, std::memory_order_release, std::memory_order_acquire));
}
return subtracted;
}
static bool create_event_group(EventGroupHandle_t &event_group, Component *component) {
event_group = xEventGroupCreate();
if (event_group == nullptr) {
ESP_LOGE(TAG, "Failed to create event group");
component->mark_failed();
return false;
}
return true;
}
void SourceSpeaker::dump_config() {
ESP_LOGCONFIG(TAG,
"Mixer Source Speaker\n"
@@ -90,70 +55,22 @@ void SourceSpeaker::dump_config() {
}
void SourceSpeaker::setup() {
if (!create_event_group(this->event_group_, this)) {
return;
}
// Start with loop disabled since we begin in STATE_STOPPED with no pending commands
this->disable_loop();
this->parent_->get_output_speaker()->add_audio_output_callback([this](uint32_t new_frames, int64_t write_timestamp) {
// First, drain the playback delay (frames in pipeline before this source started contributing)
uint32_t delay_to_drain = atomic_subtract_clamped(this->playback_delay_frames_, new_frames);
uint32_t remaining_frames = new_frames - delay_to_drain;
// The SourceSpeaker may not have included any audio in the mixed output, so verify there were pending frames
uint32_t speakers_playback_frames = std::min(new_frames, this->pending_playback_frames_);
this->pending_playback_frames_ -= speakers_playback_frames;
// Then, count towards this source's pending playback frames
if (remaining_frames > 0) {
uint32_t speakers_playback_frames = atomic_subtract_clamped(this->pending_playback_frames_, remaining_frames);
if (speakers_playback_frames > 0) {
this->audio_output_callback_(speakers_playback_frames, write_timestamp);
}
if (speakers_playback_frames > 0) {
this->audio_output_callback_(speakers_playback_frames, write_timestamp);
}
});
}
void SourceSpeaker::loop() {
uint32_t event_bits = xEventGroupGetBits(this->event_group_);
// Process commands with priority: STOP > FINISH > START
// This ensures stop commands take precedence over conflicting start commands
if (event_bits & SOURCE_SPEAKER_COMMAND_STOP) {
if (this->state_ == speaker::STATE_RUNNING) {
// Clear both STOP and START bits - stop takes precedence
xEventGroupClearBits(this->event_group_, SOURCE_SPEAKER_COMMAND_STOP | SOURCE_SPEAKER_COMMAND_START);
this->enter_stopping_state_();
} else if (this->state_ == speaker::STATE_STOPPED) {
// Already stopped, just clear the command bits
xEventGroupClearBits(this->event_group_, SOURCE_SPEAKER_COMMAND_STOP | SOURCE_SPEAKER_COMMAND_START);
}
// Leave bits set if transitioning states (STARTING/STOPPING) - will be processed once state allows
} else if (event_bits & SOURCE_SPEAKER_COMMAND_FINISH) {
if (this->state_ == speaker::STATE_RUNNING) {
xEventGroupClearBits(this->event_group_, SOURCE_SPEAKER_COMMAND_FINISH);
this->stop_gracefully_ = true;
} else if (this->state_ == speaker::STATE_STOPPED) {
// Already stopped, just clear the command bit
xEventGroupClearBits(this->event_group_, SOURCE_SPEAKER_COMMAND_FINISH);
}
// Leave bit set if transitioning states - will be processed once state allows
} else if (event_bits & SOURCE_SPEAKER_COMMAND_START) {
if (this->state_ == speaker::STATE_STOPPED) {
xEventGroupClearBits(this->event_group_, SOURCE_SPEAKER_COMMAND_START);
this->state_ = speaker::STATE_STARTING;
} else if (this->state_ == speaker::STATE_RUNNING) {
// Already running, just clear the command bit
xEventGroupClearBits(this->event_group_, SOURCE_SPEAKER_COMMAND_START);
}
// Leave bit set if transitioning states - will be processed once state allows
}
// Process state machine
switch (this->state_) {
case speaker::STATE_STARTING: {
esp_err_t err = this->start_();
if (err == ESP_OK) {
this->pending_playback_frames_.store(0, std::memory_order_release); // reset pending playback frames
this->playback_delay_frames_.store(0, std::memory_order_release); // reset playback delay
this->has_contributed_.store(false, std::memory_order_release); // reset contribution tracking
this->state_ = speaker::STATE_RUNNING;
this->stop_gracefully_ = false;
this->last_seen_data_ms_ = millis();
@@ -161,62 +78,41 @@ void SourceSpeaker::loop() {
} else {
switch (err) {
case ESP_ERR_NO_MEM:
this->status_set_error(LOG_STR("Not enough memory"));
this->status_set_error(LOG_STR("Failed to start mixer: not enough memory"));
break;
case ESP_ERR_NOT_SUPPORTED:
this->status_set_error(LOG_STR("Unsupported bit depth"));
this->status_set_error(LOG_STR("Failed to start mixer: unsupported bits per sample"));
break;
case ESP_ERR_INVALID_ARG:
this->status_set_error(LOG_STR("Incompatible audio streams"));
this->status_set_error(
LOG_STR("Failed to start mixer: audio stream isn't compatible with the other audio stream."));
break;
case ESP_ERR_INVALID_STATE:
this->status_set_error(LOG_STR("Task failed"));
this->status_set_error(LOG_STR("Failed to start mixer: mixer task failed to start"));
break;
default:
this->status_set_error(LOG_STR("Failed"));
this->status_set_error(LOG_STR("Failed to start mixer"));
break;
}
this->enter_stopping_state_();
this->state_ = speaker::STATE_STOPPING;
}
break;
}
case speaker::STATE_RUNNING:
if (!this->transfer_buffer_->has_buffered_data() &&
(this->pending_playback_frames_.load(std::memory_order_acquire) == 0)) {
// No audio data in buffer waiting to get mixed and no frames are pending playback
if (!this->transfer_buffer_->has_buffered_data()) {
if ((this->timeout_ms_.has_value() && ((millis() - this->last_seen_data_ms_) > this->timeout_ms_.value())) ||
this->stop_gracefully_) {
// Timeout exceeded or graceful stop requested
this->enter_stopping_state_();
this->state_ = speaker::STATE_STOPPING;
}
}
break;
case speaker::STATE_STOPPING: {
if ((this->parent_->get_output_speaker()->get_pause_state()) ||
((millis() - this->stopping_start_ms_) > STOPPING_TIMEOUT_MS)) {
// If parent speaker is paused or if the stopping timeout is exceeded, force stop the output speaker
this->parent_->get_output_speaker()->stop();
}
if (this->parent_->get_output_speaker()->is_stopped() ||
(this->pending_playback_frames_.load(std::memory_order_acquire) == 0)) {
// Output speaker is stopped OR all pending playback frames have played
this->pending_playback_frames_.store(0, std::memory_order_release);
this->stop_gracefully_ = false;
this->state_ = speaker::STATE_STOPPED;
}
case speaker::STATE_STOPPING:
this->stop_();
this->stop_gracefully_ = false;
this->state_ = speaker::STATE_STOPPED;
break;
}
case speaker::STATE_STOPPED:
// Re-check event bits for any new commands that may have arrived
event_bits = xEventGroupGetBits(this->event_group_);
if (!(event_bits &
(SOURCE_SPEAKER_COMMAND_START | SOURCE_SPEAKER_COMMAND_STOP | SOURCE_SPEAKER_COMMAND_FINISH))) {
// No pending commands, disable loop to save CPU cycles
this->disable_loop();
}
break;
}
}
@@ -226,34 +122,17 @@ size_t SourceSpeaker::play(const uint8_t *data, size_t length, TickType_t ticks_
this->start();
}
size_t bytes_written = 0;
std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
if (temp_ring_buffer.use_count() > 0) {
// Only write to the ring buffer if the reference is valid
if (this->ring_buffer_.use_count() == 1) {
std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
bytes_written = temp_ring_buffer->write_without_replacement(data, length, ticks_to_wait);
if (bytes_written > 0) {
this->last_seen_data_ms_ = millis();
}
} else {
// Delay to avoid repeatedly hammering while waiting for the speaker to start
vTaskDelay(ticks_to_wait);
}
return bytes_written;
}
void SourceSpeaker::send_command_(uint32_t command_bit, bool wake_loop) {
this->enable_loop_soon_any_context();
uint32_t event_bits = xEventGroupGetBits(this->event_group_);
if (!(event_bits & command_bit)) {
xEventGroupSetBits(this->event_group_, command_bit);
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
if (wake_loop) {
App.wake_loop_threadsafe();
}
#endif
}
}
void SourceSpeaker::start() { this->send_command_(SOURCE_SPEAKER_COMMAND_START, true); }
void SourceSpeaker::start() { this->state_ = speaker::STATE_STARTING; }
esp_err_t SourceSpeaker::start_() {
const size_t ring_buffer_size = this->audio_stream_info_.ms_to_bytes(this->buffer_duration_ms_);
@@ -264,26 +143,35 @@ esp_err_t SourceSpeaker::start_() {
if (this->transfer_buffer_ == nullptr) {
return ESP_ERR_NO_MEM;
}
std::shared_ptr<RingBuffer> temp_ring_buffer;
std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
if (!temp_ring_buffer) {
if (!this->ring_buffer_.use_count()) {
temp_ring_buffer = RingBuffer::create(ring_buffer_size);
this->ring_buffer_ = temp_ring_buffer;
}
if (!temp_ring_buffer) {
if (!this->ring_buffer_.use_count()) {
return ESP_ERR_NO_MEM;
} else {
this->transfer_buffer_->set_source(temp_ring_buffer);
}
}
this->pending_playback_frames_ = 0; // reset
return this->parent_->start(this->audio_stream_info_);
}
void SourceSpeaker::stop() { this->send_command_(SOURCE_SPEAKER_COMMAND_STOP); }
void SourceSpeaker::stop() {
if (this->state_ != speaker::STATE_STOPPED) {
this->state_ = speaker::STATE_STOPPING;
}
}
void SourceSpeaker::finish() { this->send_command_(SOURCE_SPEAKER_COMMAND_FINISH); }
void SourceSpeaker::stop_() {
this->transfer_buffer_.reset(); // deallocates the transfer buffer
}
void SourceSpeaker::finish() { this->stop_gracefully_ = true; }
bool SourceSpeaker::has_buffered_data() const {
return ((this->transfer_buffer_.use_count() > 0) && this->transfer_buffer_->has_buffered_data());
@@ -303,16 +191,19 @@ void SourceSpeaker::set_volume(float volume) {
float SourceSpeaker::get_volume() { return this->parent_->get_output_speaker()->get_volume(); }
size_t SourceSpeaker::process_data_from_source(std::shared_ptr<audio::AudioSourceTransferBuffer> &transfer_buffer,
TickType_t ticks_to_wait) {
// Store current offset, as these samples are already ducked
const size_t current_length = transfer_buffer->available();
size_t SourceSpeaker::process_data_from_source(TickType_t ticks_to_wait) {
if (!this->transfer_buffer_.use_count()) {
return 0;
}
size_t bytes_read = transfer_buffer->transfer_data_from_source(ticks_to_wait);
// Store current offset, as these samples are already ducked
const size_t current_length = this->transfer_buffer_->available();
size_t bytes_read = this->transfer_buffer_->transfer_data_from_source(ticks_to_wait);
uint32_t samples_to_duck = this->audio_stream_info_.bytes_to_samples(bytes_read);
if (samples_to_duck > 0) {
int16_t *current_buffer = reinterpret_cast<int16_t *>(transfer_buffer->get_buffer_start() + current_length);
int16_t *current_buffer = reinterpret_cast<int16_t *>(this->transfer_buffer_->get_buffer_start() + current_length);
duck_samples(current_buffer, samples_to_duck, &this->current_ducking_db_reduction_,
&this->ducking_transition_samples_remaining_, this->samples_per_ducking_step_,
@@ -324,13 +215,10 @@ size_t SourceSpeaker::process_data_from_source(std::shared_ptr<audio::AudioSourc
void SourceSpeaker::apply_ducking(uint8_t decibel_reduction, uint32_t duration) {
if (this->target_ducking_db_reduction_ != decibel_reduction) {
// Start transition from the previous target (which becomes the new current level)
this->current_ducking_db_reduction_ = this->target_ducking_db_reduction_;
this->target_ducking_db_reduction_ = decibel_reduction;
// Calculate the number of intermediate dB steps for the transition timing.
// Subtract 1 because the first step is taken immediately after this calculation.
uint8_t total_ducking_steps = 0;
if (this->target_ducking_db_reduction_ > this->current_ducking_db_reduction_) {
// The dB reduction level is increasing (which results in quieter audio)
@@ -346,7 +234,7 @@ void SourceSpeaker::apply_ducking(uint8_t decibel_reduction, uint32_t duration)
this->samples_per_ducking_step_ = this->ducking_transition_samples_remaining_ / total_ducking_steps;
this->ducking_transition_samples_remaining_ =
this->samples_per_ducking_step_ * total_ducking_steps; // adjust for integer division rounding
this->samples_per_ducking_step_ * total_ducking_steps; // Adjust for integer division rounding
this->current_ducking_db_reduction_ += this->db_change_per_ducking_step_;
} else {
@@ -405,12 +293,6 @@ void SourceSpeaker::duck_samples(int16_t *input_buffer, uint32_t input_samples_t
}
}
void SourceSpeaker::enter_stopping_state_() {
this->state_ = speaker::STATE_STOPPING;
this->stopping_start_ms_ = millis();
this->transfer_buffer_.reset();
}
void MixerSpeaker::dump_config() {
ESP_LOGCONFIG(TAG,
"Speaker Mixer:\n"
@@ -419,74 +301,42 @@ void MixerSpeaker::dump_config() {
}
void MixerSpeaker::setup() {
if (!create_event_group(this->event_group_, this)) {
this->event_group_ = xEventGroupCreate();
if (this->event_group_ == nullptr) {
ESP_LOGE(TAG, "Failed to create event group");
this->mark_failed();
return;
}
// Register callback to track frames in the output pipeline
this->output_speaker_->add_audio_output_callback([this](uint32_t new_frames, int64_t write_timestamp) {
atomic_subtract_clamped(this->frames_in_pipeline_, new_frames);
});
// Start with loop disabled since no task is running and no commands are pending
this->disable_loop();
}
void MixerSpeaker::loop() {
uint32_t event_group_bits = xEventGroupGetBits(this->event_group_);
// Handle pending start request
if (event_group_bits & MIXER_TASK_COMMAND_START) {
// Only start the task if it's fully stopped and cleaned up
if (!this->status_has_error() && (this->task_handle_ == nullptr) && (this->task_stack_buffer_ == nullptr)) {
esp_err_t err = this->start_task_();
switch (err) {
case ESP_OK:
xEventGroupClearBits(this->event_group_, MIXER_TASK_COMMAND_START);
break;
case ESP_ERR_NO_MEM:
ESP_LOGE(TAG, "Failed to start; retrying in 1 second");
this->status_momentary_error("memory-failure", 1000);
return;
case ESP_ERR_INVALID_STATE:
ESP_LOGE(TAG, "Failed to start; retrying in 1 second");
this->status_momentary_error("task-failure", 1000);
return;
default:
ESP_LOGE(TAG, "Failed to start; retrying in 1 second");
this->status_momentary_error("failure", 1000);
return;
}
}
if (event_group_bits & MixerEventGroupBits::STATE_STARTING) {
ESP_LOGD(TAG, "Starting speaker mixer");
xEventGroupClearBits(this->event_group_, MixerEventGroupBits::STATE_STARTING);
}
if (event_group_bits & MIXER_TASK_STATE_STARTING) {
ESP_LOGD(TAG, "Starting");
xEventGroupClearBits(this->event_group_, MIXER_TASK_STATE_STARTING);
if (event_group_bits & MixerEventGroupBits::ERR_ESP_NO_MEM) {
this->status_set_error(LOG_STR("Failed to allocate the mixer's internal buffer"));
xEventGroupClearBits(this->event_group_, MixerEventGroupBits::ERR_ESP_NO_MEM);
}
if (event_group_bits & MIXER_TASK_ERR_ESP_NO_MEM) {
this->status_set_error(LOG_STR("Not enough memory"));
xEventGroupClearBits(this->event_group_, MIXER_TASK_ERR_ESP_NO_MEM);
}
if (event_group_bits & MIXER_TASK_STATE_RUNNING) {
ESP_LOGV(TAG, "Started");
if (event_group_bits & MixerEventGroupBits::STATE_RUNNING) {
ESP_LOGD(TAG, "Started speaker mixer");
this->status_clear_error();
xEventGroupClearBits(this->event_group_, MIXER_TASK_STATE_RUNNING);
xEventGroupClearBits(this->event_group_, MixerEventGroupBits::STATE_RUNNING);
}
if (event_group_bits & MIXER_TASK_STATE_STOPPING) {
ESP_LOGV(TAG, "Stopping");
xEventGroupClearBits(this->event_group_, MIXER_TASK_STATE_STOPPING);
if (event_group_bits & MixerEventGroupBits::STATE_STOPPING) {
ESP_LOGD(TAG, "Stopping speaker mixer");
xEventGroupClearBits(this->event_group_, MixerEventGroupBits::STATE_STOPPING);
}
if (event_group_bits & MIXER_TASK_STATE_STOPPED) {
if (event_group_bits & MixerEventGroupBits::STATE_STOPPED) {
if (this->delete_task_() == ESP_OK) {
ESP_LOGD(TAG, "Stopped");
xEventGroupClearBits(this->event_group_, MIXER_TASK_ALL_BITS);
xEventGroupClearBits(this->event_group_, MixerEventGroupBits::ALL_BITS);
}
}
if (this->task_handle_ != nullptr) {
// If the mixer task is running, check if all source speakers are stopped
bool all_stopped = true;
for (auto &speaker : this->source_speakers_) {
@@ -494,15 +344,7 @@ void MixerSpeaker::loop() {
}
if (all_stopped) {
// Send stop command signal to the mixer task since no source speakers are active
xEventGroupSetBits(this->event_group_, MIXER_TASK_COMMAND_STOP);
}
} else if (this->task_stack_buffer_ == nullptr) {
// Task is fully stopped and cleaned up, check if we can disable loop
event_group_bits = xEventGroupGetBits(this->event_group_);
if (event_group_bits == 0) {
// No pending events, disable loop to save CPU cycles
this->disable_loop();
this->stop();
}
}
}
@@ -524,18 +366,7 @@ esp_err_t MixerSpeaker::start(audio::AudioStreamInfo &stream_info) {
}
}
this->enable_loop_soon_any_context(); // ensure loop processes command
uint32_t event_bits = xEventGroupGetBits(this->event_group_);
if (!(event_bits & MIXER_TASK_COMMAND_START)) {
// Set MIXER_TASK_COMMAND_START bit if not already set, and then immediately wake for low latency
xEventGroupSetBits(this->event_group_, MIXER_TASK_COMMAND_START);
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
App.wake_loop_threadsafe();
#endif
}
return ESP_OK;
return this->start_task_();
}
esp_err_t MixerSpeaker::start_task_() {
@@ -566,32 +397,29 @@ esp_err_t MixerSpeaker::start_task_() {
}
esp_err_t MixerSpeaker::delete_task_() {
if (this->task_handle_ != nullptr) {
// Delete the task
vTaskDelete(this->task_handle_);
if (!this->task_created_) {
this->task_handle_ = nullptr;
}
if ((this->task_handle_ == nullptr) && (this->task_stack_buffer_ != nullptr)) {
// Deallocate the task stack buffer
if (this->task_stack_in_psram_) {
RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_EXTERNAL);
stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE);
} else {
RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_INTERNAL);
stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE);
if (this->task_stack_buffer_ != nullptr) {
if (this->task_stack_in_psram_) {
RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_EXTERNAL);
stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE);
} else {
RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_INTERNAL);
stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE);
}
this->task_stack_buffer_ = nullptr;
}
this->task_stack_buffer_ = nullptr;
return ESP_OK;
}
if ((this->task_handle_ != nullptr) || (this->task_stack_buffer_ != nullptr)) {
return ESP_ERR_INVALID_STATE;
}
return ESP_OK;
return ESP_ERR_INVALID_STATE;
}
void MixerSpeaker::stop() { xEventGroupSetBits(this->event_group_, MixerEventGroupBits::COMMAND_STOP); }
void MixerSpeaker::copy_frames(const int16_t *input_buffer, audio::AudioStreamInfo input_stream_info,
int16_t *output_buffer, audio::AudioStreamInfo output_stream_info,
uint32_t frames_to_transfer) {
@@ -644,34 +472,32 @@ void MixerSpeaker::mix_audio_samples(const int16_t *primary_buffer, audio::Audio
}
void MixerSpeaker::audio_mixer_task(void *params) {
MixerSpeaker *this_mixer = static_cast<MixerSpeaker *>(params);
MixerSpeaker *this_mixer = (MixerSpeaker *) params;
xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STARTING);
xEventGroupSetBits(this_mixer->event_group_, MixerEventGroupBits::STATE_STARTING);
this_mixer->task_created_ = true;
std::unique_ptr<audio::AudioSinkTransferBuffer> output_transfer_buffer = audio::AudioSinkTransferBuffer::create(
this_mixer->audio_stream_info_.value().ms_to_bytes(TRANSFER_BUFFER_DURATION_MS));
if (output_transfer_buffer == nullptr) {
xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPED | MIXER_TASK_ERR_ESP_NO_MEM);
xEventGroupSetBits(this_mixer->event_group_,
MixerEventGroupBits::STATE_STOPPED | MixerEventGroupBits::ERR_ESP_NO_MEM);
vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it
this_mixer->task_created_ = false;
vTaskDelete(nullptr);
}
output_transfer_buffer->set_sink(this_mixer->output_speaker_);
xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_RUNNING);
xEventGroupSetBits(this_mixer->event_group_, MixerEventGroupBits::STATE_RUNNING);
bool sent_finished = false;
// Pre-allocate vectors to avoid heap allocation in the loop (max 8 source speakers per schema)
FixedVector<SourceSpeaker *> speakers_with_data;
FixedVector<std::shared_ptr<audio::AudioSourceTransferBuffer>> transfer_buffers_with_data;
speakers_with_data.init(this_mixer->source_speakers_.size());
transfer_buffers_with_data.init(this_mixer->source_speakers_.size());
while (true) {
uint32_t event_group_bits = xEventGroupGetBits(this_mixer->event_group_);
if (event_group_bits & MIXER_TASK_COMMAND_STOP) {
if (event_group_bits & MixerEventGroupBits::COMMAND_STOP) {
break;
}
@@ -681,20 +507,15 @@ void MixerSpeaker::audio_mixer_task(void *params) {
const uint32_t output_frames_free =
this_mixer->audio_stream_info_.value().bytes_to_frames(output_transfer_buffer->free());
speakers_with_data.clear();
transfer_buffers_with_data.clear();
std::vector<SourceSpeaker *> speakers_with_data;
std::vector<std::shared_ptr<audio::AudioSourceTransferBuffer>> transfer_buffers_with_data;
for (auto &speaker : this_mixer->source_speakers_) {
if (speaker->is_running() && !speaker->get_pause_state()) {
// Speaker is running and not paused, so it possibly can provide audio data
if (speaker->get_transfer_buffer().use_count() > 0) {
std::shared_ptr<audio::AudioSourceTransferBuffer> transfer_buffer = speaker->get_transfer_buffer().lock();
if (transfer_buffer.use_count() == 0) {
// No transfer buffer allocated, so skip processing this speaker
continue;
}
speaker->process_data_from_source(transfer_buffer, 0); // Transfers and ducks audio from source ring buffers
speaker->process_data_from_source(0); // Transfers and ducks audio from source ring buffers
if (transfer_buffer->available() > 0) {
if ((transfer_buffer->available() > 0) && !speaker->get_pause_state()) {
// Store the locked transfer buffers in their own vector to avoid releasing ownership until after the loop
transfer_buffers_with_data.push_back(transfer_buffer);
speakers_with_data.push_back(speaker);
@@ -726,21 +547,13 @@ void MixerSpeaker::audio_mixer_task(void *params) {
reinterpret_cast<int16_t *>(output_transfer_buffer->get_buffer_end()),
this_mixer->audio_stream_info_.value(), frames_to_mix);
// Set playback delay for newly contributing source
if (!speakers_with_data[0]->has_contributed_.load(std::memory_order_acquire)) {
speakers_with_data[0]->playback_delay_frames_.store(
this_mixer->frames_in_pipeline_.load(std::memory_order_acquire), std::memory_order_release);
speakers_with_data[0]->has_contributed_.store(true, std::memory_order_release);
}
// Update source speaker pending frames
speakers_with_data[0]->pending_playback_frames_.fetch_add(frames_to_mix, std::memory_order_release);
// Update source speaker buffer length
transfer_buffers_with_data[0]->decrease_buffer_length(active_stream_info.frames_to_bytes(frames_to_mix));
speakers_with_data[0]->pending_playback_frames_ += frames_to_mix;
// Update output transfer buffer length and pipeline frame count
// Update output transfer buffer length
output_transfer_buffer->increase_buffer_length(
this_mixer->audio_stream_info_.value().frames_to_bytes(frames_to_mix));
this_mixer->frames_in_pipeline_.fetch_add(frames_to_mix, std::memory_order_release);
} else {
// Speaker's stream info doesn't match the output speaker's, so it's a new source speaker
if (!this_mixer->output_speaker_->is_stopped()) {
@@ -755,8 +568,6 @@ void MixerSpeaker::audio_mixer_task(void *params) {
active_stream_info.get_sample_rate());
this_mixer->output_speaker_->set_audio_stream_info(this_mixer->audio_stream_info_.value());
this_mixer->output_speaker_->start();
// Reset pipeline frame count since we're starting fresh with a new sample rate
this_mixer->frames_in_pipeline_.store(0, std::memory_order_release);
sent_finished = false;
}
}
@@ -785,39 +596,26 @@ void MixerSpeaker::audio_mixer_task(void *params) {
}
}
// Get current pipeline depth for delay calculation (before incrementing)
uint32_t current_pipeline_frames = this_mixer->frames_in_pipeline_.load(std::memory_order_acquire);
// Update source transfer buffer lengths and add new audio durations to the source speaker pending playbacks
for (size_t i = 0; i < transfer_buffers_with_data.size(); ++i) {
// Set playback delay for newly contributing sources
if (!speakers_with_data[i]->has_contributed_.load(std::memory_order_acquire)) {
speakers_with_data[i]->playback_delay_frames_.store(current_pipeline_frames, std::memory_order_release);
speakers_with_data[i]->has_contributed_.store(true, std::memory_order_release);
}
speakers_with_data[i]->pending_playback_frames_.fetch_add(frames_to_mix, std::memory_order_release);
transfer_buffers_with_data[i]->decrease_buffer_length(
speakers_with_data[i]->get_audio_stream_info().frames_to_bytes(frames_to_mix));
speakers_with_data[i]->pending_playback_frames_ += frames_to_mix;
}
// Update output transfer buffer length and pipeline frame count (once, not per source)
// Update output transfer buffer length
output_transfer_buffer->increase_buffer_length(
this_mixer->audio_stream_info_.value().frames_to_bytes(frames_to_mix));
this_mixer->frames_in_pipeline_.fetch_add(frames_to_mix, std::memory_order_release);
}
}
xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPING);
// Reset pipeline frame count since the task is stopping
this_mixer->frames_in_pipeline_.store(0, std::memory_order_release);
xEventGroupSetBits(this_mixer->event_group_, MixerEventGroupBits::STATE_STOPPING);
output_transfer_buffer.reset();
xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPED);
vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it
xEventGroupSetBits(this_mixer->event_group_, MixerEventGroupBits::STATE_STOPPED);
this_mixer->task_created_ = false;
vTaskDelete(nullptr);
}
} // namespace mixer_speaker

View File

@@ -7,31 +7,26 @@
#include "esphome/components/speaker/speaker.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include <freertos/FreeRTOS.h>
#include <freertos/event_groups.h>
#include <atomic>
#include <freertos/FreeRTOS.h>
namespace esphome {
namespace mixer_speaker {
/* Classes for mixing several source speaker audio streams and writing it to another speaker component.
* - Volume controls are passed through to the output speaker
* - Source speaker commands are signaled via event group bits and processed in its loop function to ensure thread
* safety
* - Directly handles pausing at the SourceSpeaker level; pause state is not passed through to the output speaker.
* - Audio sent to the SourceSpeaker must have 16 bits per sample.
* - Audio sent to the SourceSpeaker's must have 16 bits per sample.
* - Audio sent to the SourceSpeaker can have any number of channels. They are duplicated or ignored as needed to match
* the number of channels required for the output speaker.
* - In queue mode, the audio sent to the SourceSpeakers can have different sample rates.
* - In queue mode, the audio sent to the SoureSpeakers can have different sample rates.
* - In non-queue mode, the audio sent to the SourceSpeakers must have the same sample rates.
* - SourceSpeaker has an internal ring buffer. It also allocates a shared_ptr for an AudioTranserBuffer object.
* - Audio Data Flow:
* - Audio data played on a SourceSpeaker first writes to its internal ring buffer.
* - MixerSpeaker task temporarily takes shared ownership of each SourceSpeaker's AudioTransferBuffer.
* - MixerSpeaker calls SourceSpeaker's `process_data_from_source`, which transfers audio from the SourceSpeaker's
* - MixerSpeaker calls SourceSpeaker's `process_data_from_source`, which tranfers audio from the SourceSpeaker's
* ring buffer to its AudioTransferBuffer. Audio ducking is applied at this step.
* - In queue mode, MixerSpeaker prioritizes the earliest configured SourceSpeaker with audio data. Audio data is
* sent to the output speaker.
@@ -68,15 +63,13 @@ class SourceSpeaker : public speaker::Speaker, public Component {
bool get_pause_state() const override { return this->pause_state_; }
/// @brief Transfers audio from the ring buffer into the transfer buffer. Ducks audio while transferring.
/// @param transfer_buffer Locked shared_ptr to the transfer buffer (must be valid, not null)
/// @param ticks_to_wait FreeRTOS ticks to wait while waiting to read from the ring buffer.
/// @return Number of bytes transferred from the ring buffer.
size_t process_data_from_source(std::shared_ptr<audio::AudioSourceTransferBuffer> &transfer_buffer,
TickType_t ticks_to_wait);
size_t process_data_from_source(TickType_t ticks_to_wait);
/// @brief Sets the ducking level for the source speaker.
/// @param decibel_reduction The dB reduction level. For example, 0 is no change, 10 is a reduction by 10 dB
/// @param duration The number of milliseconds to transition from the current level to the new level
/// @param decibel_reduction (uint8_t) The dB reduction level. For example, 0 is no change, 10 is a reduction by 10 dB
/// @param duration (uint32_t) The number of milliseconds to transition from the current level to the new level
void apply_ducking(uint8_t decibel_reduction, uint32_t duration);
void set_buffer_duration(uint32_t buffer_duration_ms) { this->buffer_duration_ms_ = buffer_duration_ms; }
@@ -88,15 +81,14 @@ class SourceSpeaker : public speaker::Speaker, public Component {
protected:
friend class MixerSpeaker;
esp_err_t start_();
void enter_stopping_state_();
void send_command_(uint32_t command_bit, bool wake_loop = false);
void stop_();
/// @brief Ducks audio samples by a specified amount. When changing the ducking amount, it can transition gradually
/// over a specified amount of samples.
/// @param input_buffer buffer with audio samples to be ducked in place
/// @param input_samples_to_duck number of samples to process in ``input_buffer``
/// @param current_ducking_db_reduction pointer to the current dB reduction
/// @param ducking_transition_samples_remaining pointer to the total number of samples left before the
/// @param ducking_transition_samples_remaining pointer to the total number of samples left before the the
/// transition is finished
/// @param samples_per_ducking_step total number of samples per ducking step for the transition
/// @param db_change_per_ducking_step the change in dB reduction per step
@@ -122,12 +114,7 @@ class SourceSpeaker : public speaker::Speaker, public Component {
uint32_t ducking_transition_samples_remaining_{0};
uint32_t samples_per_ducking_step_{0};
std::atomic<uint32_t> pending_playback_frames_{0};
std::atomic<uint32_t> playback_delay_frames_{0}; // Frames in output pipeline when this source started contributing
std::atomic<bool> has_contributed_{false}; // Tracks if source has contributed during this session
EventGroupHandle_t event_group_{nullptr};
uint32_t stopping_start_ms_{0};
uint32_t pending_playback_frames_{0};
};
class MixerSpeaker : public Component {
@@ -136,11 +123,10 @@ class MixerSpeaker : public Component {
void setup() override;
void loop() override;
void init_source_speakers(size_t count) { this->source_speakers_.init(count); }
void add_source_speaker(SourceSpeaker *source_speaker) { this->source_speakers_.push_back(source_speaker); }
/// @brief Starts the mixer task. Called by a source speaker giving the current audio stream information
/// @param stream_info The calling source speaker's audio stream information
/// @param stream_info The calling source speakers audio stream information
/// @return ESP_ERR_NOT_SUPPORTED if the incoming stream is incompatible due to unsupported bits per sample
/// ESP_ERR_INVALID_ARG if the incoming stream is incompatible to be mixed with the other input audio stream
/// ESP_ERR_NO_MEM if there isn't enough memory for the task's stack
@@ -148,6 +134,8 @@ class MixerSpeaker : public Component {
/// ESP_OK if the incoming stream is compatible and the mixer task starts
esp_err_t start(audio::AudioStreamInfo &stream_info);
void stop();
void set_output_channels(uint8_t output_channels) { this->output_channels_ = output_channels; }
void set_output_speaker(speaker::Speaker *speaker) { this->output_speaker_ = speaker; }
void set_queue_mode(bool queue_mode) { this->queue_mode_ = queue_mode; }
@@ -155,9 +143,6 @@ class MixerSpeaker : public Component {
speaker::Speaker *get_output_speaker() const { return this->output_speaker_; }
/// @brief Returns the current number of frames in the output pipeline (written but not yet played)
uint32_t get_frames_in_pipeline() const { return this->frames_in_pipeline_.load(std::memory_order_acquire); }
protected:
/// @brief Copies audio frames from the input buffer to the output buffer taking into account the number of channels
/// in each stream. If the output stream has more channels, the input samples are duplicated. If the output stream has
@@ -174,11 +159,11 @@ class MixerSpeaker : public Component {
/// and secondary samples are duplicated or dropped as necessary to ensure the output stream has the configured number
/// of channels. Output samples are clamped to the corresponding int16 min or max values if the mixed sample
/// overflows.
/// @param primary_buffer samples buffer for the primary stream
/// @param primary_buffer (int16_t *) samples buffer for the primary stream
/// @param primary_stream_info stream info for the primary stream
/// @param secondary_buffer samples buffer for secondary stream
/// @param secondary_buffer (int16_t *) samples buffer for secondary stream
/// @param secondary_stream_info stream info for the secondary stream
/// @param output_buffer buffer for the mixed samples
/// @param output_buffer (int16_t *) buffer for the mixed samples
/// @param output_stream_info stream info for the output buffer
/// @param frames_to_mix number of frames in the primary and secondary buffers to mix together
static void mix_audio_samples(const int16_t *primary_buffer, audio::AudioStreamInfo primary_stream_info,
@@ -200,20 +185,20 @@ class MixerSpeaker : public Component {
EventGroupHandle_t event_group_{nullptr};
FixedVector<SourceSpeaker *> source_speakers_;
std::vector<SourceSpeaker *> source_speakers_;
speaker::Speaker *output_speaker_{nullptr};
uint8_t output_channels_;
bool queue_mode_;
bool task_stack_in_psram_{false};
bool task_created_{false};
TaskHandle_t task_handle_{nullptr};
StaticTask_t task_stack_;
StackType_t *task_stack_buffer_{nullptr};
optional<audio::AudioStreamInfo> audio_stream_info_;
std::atomic<uint32_t> frames_in_pipeline_{0}; // Frames written to output but not yet played
};
} // namespace mixer_speaker

View File

@@ -19,25 +19,16 @@ void Modbus::setup() {
void Modbus::loop() {
const uint32_t now = App.get_loop_component_start_time();
// Read all available bytes in batches to reduce UART call overhead.
size_t avail = this->available();
uint8_t buf[64];
while (avail > 0) {
size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}
avail -= to_read;
for (size_t i = 0; i < to_read; i++) {
if (this->parse_modbus_byte_(buf[i])) {
this->last_modbus_byte_ = now;
} else {
size_t at = this->rx_buffer_.size();
if (at > 0) {
ESP_LOGV(TAG, "Clearing buffer of %d bytes - parse failed", at);
this->rx_buffer_.clear();
}
while (this->available()) {
uint8_t byte;
this->read_byte(&byte);
if (this->parse_modbus_byte_(byte)) {
this->last_modbus_byte_ = now;
} else {
size_t at = this->rx_buffer_.size();
if (at > 0) {
ESP_LOGV(TAG, "Clearing buffer of %d bytes - parse failed", at);
this->rx_buffer_.clear();
}
}
}
@@ -228,50 +219,39 @@ void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address
return;
}
static constexpr size_t ADDR_SIZE = 1;
static constexpr size_t FC_SIZE = 1;
static constexpr size_t START_ADDR_SIZE = 2;
static constexpr size_t NUM_ENTITIES_SIZE = 2;
static constexpr size_t BYTE_COUNT_SIZE = 1;
static constexpr size_t MAX_PAYLOAD_SIZE = std::numeric_limits<uint8_t>::max();
static constexpr size_t CRC_SIZE = 2;
static constexpr size_t MAX_FRAME_SIZE =
ADDR_SIZE + FC_SIZE + START_ADDR_SIZE + NUM_ENTITIES_SIZE + BYTE_COUNT_SIZE + MAX_PAYLOAD_SIZE + CRC_SIZE;
uint8_t data[MAX_FRAME_SIZE];
size_t pos = 0;
data[pos++] = address;
data[pos++] = function_code;
std::vector<uint8_t> data;
data.push_back(address);
data.push_back(function_code);
if (this->role == ModbusRole::CLIENT) {
data[pos++] = start_address >> 8;
data[pos++] = start_address >> 0;
data.push_back(start_address >> 8);
data.push_back(start_address >> 0);
if (function_code != ModbusFunctionCode::WRITE_SINGLE_COIL &&
function_code != ModbusFunctionCode::WRITE_SINGLE_REGISTER) {
data[pos++] = number_of_entities >> 8;
data[pos++] = number_of_entities >> 0;
data.push_back(number_of_entities >> 8);
data.push_back(number_of_entities >> 0);
}
}
if (payload != nullptr) {
if (this->role == ModbusRole::SERVER || function_code == ModbusFunctionCode::WRITE_MULTIPLE_COILS ||
function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) { // Write multiple
data[pos++] = payload_len; // Byte count is required for write
data.push_back(payload_len); // Byte count is required for write
} else {
payload_len = 2; // Write single register or coil
}
for (int i = 0; i < payload_len; i++) {
data[pos++] = payload[i];
data.push_back(payload[i]);
}
}
auto crc = crc16(data, pos);
data[pos++] = crc >> 0;
data[pos++] = crc >> 8;
auto crc = crc16(data.data(), data.size());
data.push_back(crc >> 0);
data.push_back(crc >> 8);
if (this->flow_control_pin_ != nullptr)
this->flow_control_pin_->digital_write(true);
this->write_array(data, pos);
this->write_array(data);
this->flush();
if (this->flow_control_pin_ != nullptr)
@@ -281,7 +261,7 @@ void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)];
#endif
ESP_LOGV(TAG, "Modbus write: %s", format_hex_pretty_to(hex_buf, data, pos));
ESP_LOGV(TAG, "Modbus write: %s", format_hex_pretty_to(hex_buf, data.data(), data.size()));
}
// Helper function for lambdas

View File

@@ -170,8 +170,10 @@ void MQTTClientComponent::send_device_info_() {
void MQTTClientComponent::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) {
(void) tag;
if (level <= this->log_level_ && this->is_connected()) {
this->publish(this->log_message_.topic.c_str(), message, message_len, this->log_message_.qos,
this->log_message_.retain);
this->publish({.topic = this->log_message_.topic,
.payload = std::string(message, message_len),
.qos = this->log_message_.qos,
.retain = this->log_message_.retain});
}
}
#endif
@@ -540,8 +542,8 @@ bool MQTTClientComponent::publish(const char *topic, const char *payload, size_t
}
bool MQTTClientComponent::publish_json(const char *topic, const json::json_build_t &f, uint8_t qos, bool retain) {
auto message = json::build_json(f);
return this->publish(topic, message.c_str(), message.size(), qos, retain);
std::string message = json::build_json(f);
return this->publish(topic, message.c_str(), message.length(), qos, retain);
}
void MQTTClientComponent::enable() {

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