1
0
mirror of https://github.com/esphome/esphome.git synced 2026-02-11 10:12:38 +00:00

Compare commits

...

62 Commits

Author SHA1 Message Date
J. Nick Koston
fd683a5609 [socket] Implement working ready() for LWIP raw TCP sockets (ESP8266/RP2040)
Previously ready() always returned true on ESP8266/RP2040, causing
every socket to be checked on every loop iteration even when no data
was available.

Override ready() in LWIPRawImpl to check rx_buf_/rx_closed_/pcb_ state,
and in LWIPRawListenImpl to check accepted_socket_count_. This uses
existing fields so no extra memory is needed per socket.

Keep ready() virtual only on the non-select path (ESP8266/RP2040) so
the select()-based path (ESP32) retains the non-virtual optimization
from the previous commit.
2026-02-10 17:37:42 -06:00
J. Nick Koston
4a6eb0b16d [socket] Devirtualize Socket::ready() and get_fd() for hot loop path
Move fd_, closed_, and loop_monitored_ fields from BSD/LWIP socket
implementations to the base Socket class. Since only one socket
implementation is active per build, these can be non-virtual.

Make Socket::ready() and get_fd() non-virtual, eliminating vtable
dispatch on every main loop iteration. Inline is_socket_ready via
friendship for the fast path while keeping the public API with
bounds checking for external callers.

Saves ~316 bytes of flash on ESP32-IDF builds.
2026-02-10 12:30:20 -06:00
J. Nick Koston
2585779f11 [api] Remove duplicate peername storage to save RAM (#13540) 2026-02-11 07:23:16 +13:00
Jonathan Swoboda
b8ec3aab1d [ci] Pin ESP-IDF version for Arduino framework builds (#13909)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 12:16:25 -05:00
Jonathan Swoboda
c4b109eebd [esp32_rmt_led_strip, remote_receiver, pulse_counter] Replace hardcoded clock frequencies with runtime queries (#13908)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 17:09:56 +00:00
Jonathan Swoboda
03b41855f5 [esp32_hosted] Bump esp_wifi_remote and esp_hosted versions (#13911)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 16:03:26 +00:00
Jonathan Swoboda
13a124c86d [pulse_counter] Migrate from legacy PCNT API to new ESP-IDF 5.x API (#13904)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 10:10:27 -05:00
Kevin Ahrendt
298efb5340 [resampler] Refactor for stability and to support Sendspin (#12254)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-02-10 09:56:31 -05:00
J. Nick Koston
d4ccc64dc0 [http_request] Fix IDF chunked response completion detection (#13886) 2026-02-10 08:55:59 -06:00
tronikos
e3141211c3 [water_heater] Add On/Off and Away mode support to template platform (#13839)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-02-10 12:45:18 +00:00
dependabot[bot]
e85a022c77 Bump esphome-dashboard from 20260110.0 to 20260210.0 (#13905)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 11:49:59 +00:00
dependabot[bot]
1c3af30299 Bump aioesphomeapi from 43.14.0 to 44.0.0 (#13906)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-10 11:45:31 +00:00
tronikos
5caed68cd9 [api] Deprecate WATER_HEATER_COMMAND_HAS_STATE (#13892)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-02-10 05:36:56 -06:00
Cody Cutrer
b97a728cf1 [ld2450] add on_data callback (#13601)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-09 22:40:44 -05:00
Jonathan Swoboda
dcbb020479 [uart] Fix available() return type to size_t across components (#13898)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 20:02:41 -05:00
J. Nick Koston
87ac263264 [dsmr] Batch UART reads to reduce per-loop overhead (#13826) 2026-02-10 00:32:52 +00:00
Sean Kelly
097901e9c8 [aqi] Fix AQI calculation for specific pm2.5 or pm10 readings (#13770) 2026-02-09 19:30:37 -05:00
J. Nick Koston
01a90074ba [ld2420] Batch UART reads to reduce loop overhead (#13821)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-02-10 00:25:34 +00:00
J. Nick Koston
57b85a8400 [dlms_meter] Batch UART reads to reduce per-loop overhead (#13828)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-02-10 00:24:20 +00:00
J. Nick Koston
2edfcf278f [hlk_fm22x] Replace per-cycle vector allocation with member buffer (#13859) 2026-02-09 18:21:10 -06:00
J. Nick Koston
bcd4a9fc39 [pylontech] Batch UART reads to reduce loop overhead (#13824) 2026-02-09 18:20:53 -06:00
J. Nick Koston
78df8be31f [logger] Resolve thread name once and pass through logging chain (#13836) 2026-02-09 18:16:27 -06:00
J. Nick Koston
dacc557a16 [uart] Convert parity_to_str to PROGMEM_STRING_TABLE (#13805) 2026-02-09 18:15:48 -06:00
J. Nick Koston
3767c5ec91 [scheduler] Make core timer ID collisions impossible with type-safe internal IDs (#13882)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-02-09 16:48:08 -06:00
George Joseph
7c1327f96a [mipi_dsi] Add WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD 3.4C and 4C (#13840) 2026-02-10 09:44:47 +11:00
Jonathan Swoboda
475db750e0 [uart] Change available() return type from int to size_t (#13893)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 17:41:16 -05:00
dependabot[bot]
8f74b027b4 Bump setuptools from 80.10.2 to 82.0.0 (#13897) 2026-02-09 16:40:32 -06:00
tomaszduda23
b2b9e0cb0a [nrf52,zigee] print reporting status (#13890)
Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
2026-02-09 16:00:08 -05:00
tronikos
dbf202bf0d Add get_away and get_on in WaterHeaterCall and deprecate get_state (#13891) 2026-02-09 20:57:36 +00:00
J. Nick Koston
b6fdd29953 [voice_assistant] Replace timer unordered_map with vector to eliminate per-tick heap allocation (#13857) 2026-02-09 14:42:40 -06:00
Clyde Stubbs
00256e3ca0 [mipi_rgb] Allow use on P4 (#13740) 2026-02-10 06:35:41 +11:00
J. Nick Koston
e0712cc53b [scheduler] Make core timer ID collisions impossible with type-safe internal IDs (#13882)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-02-09 13:16:22 -06:00
J. Nick Koston
6c6da8a3cd [api] Skip class generation for empty SOURCE_CLIENT protobuf messages (#13880) 2026-02-09 18:45:24 +00:00
J. Nick Koston
e4ea016d1e [ci] Block new std::to_string() usage, suggest snprintf alternatives (#13369) 2026-02-09 12:26:19 -06:00
J. Nick Koston
41a9588d81 [i2c] Replace switch with if-else to avoid CSWTCH table in RAM (#13815) 2026-02-09 12:26:06 -06:00
J. Nick Koston
cd55eb927d [modbus] Batch UART reads to reduce loop overhead (#13822) 2026-02-09 12:21:15 -06:00
J. Nick Koston
4a9ff48f02 [nextion] Batch UART reads to reduce loop overhead (#13823)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-09 12:20:50 -06:00
J. Nick Koston
8fffe7453d [seeed_mr24hpc1/mr60fda2/mr60bha2] Batch UART reads to reduce per-loop overhead (#13825) 2026-02-09 12:18:12 -06:00
J. Nick Koston
a5ee451043 [tuya] Batch UART reads to reduce per-loop overhead (#13827) 2026-02-09 12:17:58 -06:00
J. Nick Koston
e176cf50ab [dfplayer] Batch UART reads to reduce per-loop overhead (#13832) 2026-02-09 12:15:28 -06:00
J. Nick Koston
e7a900fbaa [rf_bridge] Batch UART reads to reduce per-loop overhead (#13831) 2026-02-09 12:15:15 -06:00
J. Nick Koston
623f33c9f9 [rd03d] Batch UART reads to reduce per-loop overhead (#13830) 2026-02-09 12:15:04 -06:00
J. Nick Koston
8b24112be5 [pipsolar] Batch UART reads to reduce per-loop overhead (#13829) 2026-02-09 12:14:48 -06:00
J. Nick Koston
d33f23dc43 [ld2410] Batch UART reads to reduce loop overhead (#13820) 2026-02-09 12:07:55 -06:00
J. Nick Koston
c43d3889b0 [modbus] Use stack buffer instead of heap vector in send() (#13853) 2026-02-09 12:07:42 -06:00
J. Nick Koston
50fe8e51f9 [ld2412] Batch UART reads to reduce loop overhead (#13819) 2026-02-09 12:07:28 -06:00
J. Nick Koston
c7883cb5ae [ld2450] Batch UART reads to reduce loop overhead (#13818) 2026-02-09 12:06:38 -06:00
J. Nick Koston
3b0df145b7 [cse7766] Batch UART reads to reduce loop overhead (#13817) 2026-02-09 12:05:59 -06:00
J. Nick Koston
2383b6b8b4 [core] Deprecate set_retry, cancel_retry, and RetryResult (#13845) 2026-02-09 12:05:32 -06:00
J. Nick Koston
c658d7b57f [api] Merge auth check into base read_message, eliminate APIServerConnection (#13873) 2026-02-09 12:02:02 -06:00
Jonathan Swoboda
04a6238c7b [esp32] Set UV_CACHE_DIR inside data dir so Clean All clears it (#13888)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 17:49:58 +00:00
J. Nick Koston
919afa1553 [web_server_base] Fix RP2040 compilation when Crypto-no-arduino is present (#13887) 2026-02-09 12:47:59 -05:00
Kevin Ahrendt
c28c97fbaf [mixer] Refactor for stability and to support Sendspin (#12253)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2026-02-09 10:19:00 -05:00
J. Nick Koston
3cde3daceb [api] Collapse APIServerConnection intermediary layer (#13872) 2026-02-09 08:45:33 -06:00
J. Nick Koston
be4e573cc4 [esp32_hosted] Replace set_retry with set_interval to avoid heap allocation (#13844) 2026-02-09 08:45:18 -06:00
J. Nick Koston
66af998098 [dashboard] Handle malformed Basic Auth headers gracefully (#13866) 2026-02-09 08:45:03 -06:00
J. Nick Koston
938a11595d [speaker] Replace set_retry with set_interval to avoid heap allocation (#13843) 2026-02-09 08:44:50 -06:00
J. Nick Koston
c812ac8b29 [ms8607] Replace set_retry with set_timeout chain to avoid heap allocation (#13842) 2026-02-09 08:44:35 -06:00
J. Nick Koston
248fc06dac [scheduler] Eliminate heap allocation in full_cleanup_removed_items_ (#13837) 2026-02-09 08:44:20 -06:00
J. Nick Koston
8b8acb3b27 [dashboard] Use constant-time comparison for username check (#13865) 2026-02-09 08:31:06 -06:00
J. Nick Koston
1c60efa4b6 [ota] Use secrets module for OTA authentication cnonce (#13863) 2026-02-09 08:30:49 -06:00
J. Nick Koston
4ef238eb7b [analyze-memory] Attribute third-party library symbols via nm scanning (#13878) 2026-02-09 08:26:03 -06:00
154 changed files with 3687 additions and 2181 deletions

View File

@@ -1 +1 @@
37ec8d5a343c8d0a485fd2118cbdabcbccd7b9bca197e4a392be75087974dced 8dc4dae0acfa22f26c7cde87fc24e60b27f29a73300e02189b78f0315e5d0695

View File

@@ -43,6 +43,7 @@ _READELF_SECTION_PATTERN = re.compile(
# Component category prefixes # Component category prefixes
_COMPONENT_PREFIX_ESPHOME = "[esphome]" _COMPONENT_PREFIX_ESPHOME = "[esphome]"
_COMPONENT_PREFIX_EXTERNAL = "[external]" _COMPONENT_PREFIX_EXTERNAL = "[external]"
_COMPONENT_PREFIX_LIB = "[lib]"
_COMPONENT_CORE = f"{_COMPONENT_PREFIX_ESPHOME}core" _COMPONENT_CORE = f"{_COMPONENT_PREFIX_ESPHOME}core"
_COMPONENT_API = f"{_COMPONENT_PREFIX_ESPHOME}api" _COMPONENT_API = f"{_COMPONENT_PREFIX_ESPHOME}api"
@@ -56,6 +57,16 @@ SymbolInfoType = tuple[str, int, str]
# RAM sections - symbols in these sections consume RAM # RAM sections - symbols in these sections consume RAM
RAM_SECTIONS = frozenset([".data", ".bss"]) 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 @dataclass
class MemorySection: class MemorySection:
@@ -179,11 +190,19 @@ class MemoryAnalyzer:
self._sdk_symbols: list[SDKSymbol] = [] self._sdk_symbols: list[SDKSymbol] = []
# CSWTCH symbols: list of (name, size, source_file, component) # CSWTCH symbols: list of (name, size, source_file, component)
self._cswtch_symbols: list[tuple[str, int, str, str]] = [] 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]: def analyze(self) -> dict[str, ComponentMemory]:
"""Analyze the ELF file and return component memory usage.""" """Analyze the ELF file and return component memory usage."""
self._parse_sections() self._parse_sections()
self._parse_symbols() self._parse_symbols()
self._scan_libraries()
self._categorize_symbols() self._categorize_symbols()
self._analyze_cswtch_symbols() self._analyze_cswtch_symbols()
self._analyze_sdk_libraries() self._analyze_sdk_libraries()
@@ -328,15 +347,19 @@ class MemoryAnalyzer:
# If no component match found, it's core # If no component match found, it's core
return _COMPONENT_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 # Check against symbol patterns
for component, patterns in SYMBOL_PATTERNS.items(): for component, patterns in SYMBOL_PATTERNS.items():
if any(pattern in symbol_name for pattern in patterns): if any(pattern in symbol_name for pattern in patterns):
return component return self._heuristic_to_lib.get(component, component)
# Check against demangled patterns # Check against demangled patterns
for component, patterns in DEMANGLED_PATTERNS.items(): for component, patterns in DEMANGLED_PATTERNS.items():
if any(pattern in demangled for pattern in patterns): if any(pattern in demangled for pattern in patterns):
return component return self._heuristic_to_lib.get(component, component)
# Special cases that need more complex logic # Special cases that need more complex logic
@@ -384,6 +407,327 @@ class MemoryAnalyzer:
return "Other Core" 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: def _find_object_files_dir(self) -> Path | None:
"""Find the directory containing object files for this build. """Find the directory containing object files for this build.
@@ -559,9 +903,21 @@ class MemoryAnalyzer:
if "esphome" in parts and "components" not in parts: if "esphome" in parts and "components" not in parts:
return _COMPONENT_CORE return _COMPONENT_CORE
# Framework/library files - return the first path component # Framework/library files - check for PlatformIO library hash dirs
# e.g., lib65b/ESPAsyncTCP/... -> lib65b # e.g., lib65b/ESPAsyncTCP/... -> [lib]espasynctcp
# FrameworkArduino/... -> FrameworkArduino 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
return parts[0] if parts else source_file return parts[0] if parts else source_file
def _analyze_cswtch_symbols(self) -> None: def _analyze_cswtch_symbols(self) -> None:

View File

@@ -14,6 +14,7 @@ from . import (
_COMPONENT_CORE, _COMPONENT_CORE,
_COMPONENT_PREFIX_ESPHOME, _COMPONENT_PREFIX_ESPHOME,
_COMPONENT_PREFIX_EXTERNAL, _COMPONENT_PREFIX_EXTERNAL,
_COMPONENT_PREFIX_LIB,
RAM_SECTIONS, RAM_SECTIONS,
MemoryAnalyzer, MemoryAnalyzer,
) )
@@ -407,6 +408,11 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
for name, mem in components for name, mem in components
if name.startswith(_COMPONENT_PREFIX_EXTERNAL) 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( top_esphome_components = sorted(
esphome_components, key=lambda x: x[1].flash_total, reverse=True esphome_components, key=lambda x: x[1].flash_total, reverse=True
@@ -417,6 +423,11 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
external_components, key=lambda x: x[1].flash_total, reverse=True 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 # Check if API component exists and ensure it's included
api_component = None api_component = None
for name, mem in components: for name, mem in components:
@@ -435,10 +446,11 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
if name in system_components_to_include if name in system_components_to_include
] ]
# Combine all components to analyze: top ESPHome + all external + API if not already included + system components # Combine all components to analyze: top ESPHome + all external + libraries + API if not already included + system components
components_to_analyze = ( components_to_analyze = (
list(top_esphome_components) list(top_esphome_components)
+ list(top_external_components) + list(top_external_components)
+ list(top_library_components)
+ system_components + system_components
) )
if api_component and api_component not in components_to_analyze: if api_component and api_component not in components_to_analyze:

View File

@@ -1155,9 +1155,11 @@ enum WaterHeaterCommandHasField {
WATER_HEATER_COMMAND_HAS_NONE = 0; WATER_HEATER_COMMAND_HAS_NONE = 0;
WATER_HEATER_COMMAND_HAS_MODE = 1; WATER_HEATER_COMMAND_HAS_MODE = 1;
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE = 2; WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE = 2;
WATER_HEATER_COMMAND_HAS_STATE = 4; WATER_HEATER_COMMAND_HAS_STATE = 4 [deprecated=true];
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8; WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8;
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH = 16; WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH = 16;
WATER_HEATER_COMMAND_HAS_ON_STATE = 32;
WATER_HEATER_COMMAND_HAS_AWAY_STATE = 64;
} }
message WaterHeaterCommandRequest { message WaterHeaterCommandRequest {

View File

@@ -133,8 +133,8 @@ void APIConnection::start() {
return; return;
} }
// Initialize client name with peername (IP address) until Hello message provides actual name // Initialize client name with peername (IP address) until Hello message provides actual name
const char *peername = this->helper_->get_client_peername(); char peername[socket::SOCKADDR_STR_LEN];
this->helper_->set_client_name(peername, strlen(peername)); this->helper_->set_client_name(this->helper_->get_peername_to(peername), strlen(peername));
} }
APIConnection::~APIConnection() { APIConnection::~APIConnection() {
@@ -179,8 +179,8 @@ void APIConnection::begin_iterator_(ActiveIterator type) {
void APIConnection::loop() { void APIConnection::loop() {
if (this->flags_.next_close) { if (this->flags_.next_close) {
// requested a disconnect // requested a disconnect - don't close socket here, let APIServer::loop() do it
this->helper_->close(); // so getpeername() still works for the disconnect trigger
this->flags_.remove = true; this->flags_.remove = true;
return; return;
} }
@@ -283,7 +283,7 @@ void APIConnection::loop() {
#endif #endif
} }
bool APIConnection::send_disconnect_response() { bool APIConnection::send_disconnect_response_() {
// remote initiated disconnect_client // remote initiated disconnect_client
// don't close yet, we still need to send the disconnect response // don't close yet, we still need to send the disconnect response
// close will happen on next loop // close will happen on next loop
@@ -293,7 +293,8 @@ bool APIConnection::send_disconnect_response() {
return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE); return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE);
} }
void APIConnection::on_disconnect_response() { void APIConnection::on_disconnect_response() {
this->helper_->close(); // Don't close socket here, let APIServer::loop() do it
// so getpeername() still works for the disconnect trigger
this->flags_.remove = true; this->flags_.remove = true;
} }
@@ -406,7 +407,7 @@ uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *c
msg.device_class = cover->get_device_class_ref(); msg.device_class = cover->get_device_class_ref();
return fill_and_encode_entity_info(cover, msg, ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_info(cover, msg, ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size);
} }
void APIConnection::cover_command(const CoverCommandRequest &msg) { void APIConnection::on_cover_command_request(const CoverCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(cover::Cover, cover, cover) ENTITY_COMMAND_MAKE_CALL(cover::Cover, cover, cover)
if (msg.has_position) if (msg.has_position)
call.set_position(msg.position); call.set_position(msg.position);
@@ -449,7 +450,7 @@ uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *con
msg.supported_preset_modes = &traits.supported_preset_modes(); msg.supported_preset_modes = &traits.supported_preset_modes();
return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size);
} }
void APIConnection::fan_command(const FanCommandRequest &msg) { void APIConnection::on_fan_command_request(const FanCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(fan::Fan, fan, fan) ENTITY_COMMAND_MAKE_CALL(fan::Fan, fan, fan)
if (msg.has_state) if (msg.has_state)
call.set_state(msg.state); call.set_state(msg.state);
@@ -517,7 +518,7 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c
msg.effects = &effects_list; msg.effects = &effects_list;
return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size);
} }
void APIConnection::light_command(const LightCommandRequest &msg) { void APIConnection::on_light_command_request(const LightCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(light::LightState, light, light) ENTITY_COMMAND_MAKE_CALL(light::LightState, light, light)
if (msg.has_state) if (msg.has_state)
call.set_state(msg.state); call.set_state(msg.state);
@@ -594,7 +595,7 @@ uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection *
msg.device_class = a_switch->get_device_class_ref(); msg.device_class = a_switch->get_device_class_ref();
return fill_and_encode_entity_info(a_switch, msg, ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_info(a_switch, msg, ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size);
} }
void APIConnection::switch_command(const SwitchCommandRequest &msg) { void APIConnection::on_switch_command_request(const SwitchCommandRequest &msg) {
ENTITY_COMMAND_GET(switch_::Switch, a_switch, switch) ENTITY_COMMAND_GET(switch_::Switch, a_switch, switch)
if (msg.state) { if (msg.state) {
@@ -692,7 +693,7 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection
msg.supported_swing_modes = &traits.get_supported_swing_modes(); msg.supported_swing_modes = &traits.get_supported_swing_modes();
return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size);
} }
void APIConnection::climate_command(const ClimateCommandRequest &msg) { void APIConnection::on_climate_command_request(const ClimateCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(climate::Climate, climate, climate) ENTITY_COMMAND_MAKE_CALL(climate::Climate, climate, climate)
if (msg.has_mode) if (msg.has_mode)
call.set_mode(static_cast<climate::ClimateMode>(msg.mode)); call.set_mode(static_cast<climate::ClimateMode>(msg.mode));
@@ -742,7 +743,7 @@ uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection *
msg.step = number->traits.get_step(); msg.step = number->traits.get_step();
return fill_and_encode_entity_info(number, msg, ListEntitiesNumberResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_info(number, msg, ListEntitiesNumberResponse::MESSAGE_TYPE, conn, remaining_size);
} }
void APIConnection::number_command(const NumberCommandRequest &msg) { void APIConnection::on_number_command_request(const NumberCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(number::Number, number, number) ENTITY_COMMAND_MAKE_CALL(number::Number, number, number)
call.set_value(msg.state); call.set_value(msg.state);
call.perform(); call.perform();
@@ -767,7 +768,7 @@ uint16_t APIConnection::try_send_date_info(EntityBase *entity, APIConnection *co
ListEntitiesDateResponse msg; ListEntitiesDateResponse msg;
return fill_and_encode_entity_info(date, msg, ListEntitiesDateResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_info(date, msg, ListEntitiesDateResponse::MESSAGE_TYPE, conn, remaining_size);
} }
void APIConnection::date_command(const DateCommandRequest &msg) { void APIConnection::on_date_command_request(const DateCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(datetime::DateEntity, date, date) ENTITY_COMMAND_MAKE_CALL(datetime::DateEntity, date, date)
call.set_date(msg.year, msg.month, msg.day); call.set_date(msg.year, msg.month, msg.day);
call.perform(); call.perform();
@@ -792,7 +793,7 @@ uint16_t APIConnection::try_send_time_info(EntityBase *entity, APIConnection *co
ListEntitiesTimeResponse msg; ListEntitiesTimeResponse msg;
return fill_and_encode_entity_info(time, msg, ListEntitiesTimeResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_info(time, msg, ListEntitiesTimeResponse::MESSAGE_TYPE, conn, remaining_size);
} }
void APIConnection::time_command(const TimeCommandRequest &msg) { void APIConnection::on_time_command_request(const TimeCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(datetime::TimeEntity, time, time) ENTITY_COMMAND_MAKE_CALL(datetime::TimeEntity, time, time)
call.set_time(msg.hour, msg.minute, msg.second); call.set_time(msg.hour, msg.minute, msg.second);
call.perform(); call.perform();
@@ -819,7 +820,7 @@ uint16_t APIConnection::try_send_datetime_info(EntityBase *entity, APIConnection
ListEntitiesDateTimeResponse msg; ListEntitiesDateTimeResponse msg;
return fill_and_encode_entity_info(datetime, msg, ListEntitiesDateTimeResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_info(datetime, msg, ListEntitiesDateTimeResponse::MESSAGE_TYPE, conn, remaining_size);
} }
void APIConnection::datetime_command(const DateTimeCommandRequest &msg) { void APIConnection::on_date_time_command_request(const DateTimeCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(datetime::DateTimeEntity, datetime, datetime) ENTITY_COMMAND_MAKE_CALL(datetime::DateTimeEntity, datetime, datetime)
call.set_datetime(msg.epoch_seconds); call.set_datetime(msg.epoch_seconds);
call.perform(); call.perform();
@@ -848,7 +849,7 @@ uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *co
msg.pattern = text->traits.get_pattern_ref(); msg.pattern = text->traits.get_pattern_ref();
return fill_and_encode_entity_info(text, msg, ListEntitiesTextResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_info(text, msg, ListEntitiesTextResponse::MESSAGE_TYPE, conn, remaining_size);
} }
void APIConnection::text_command(const TextCommandRequest &msg) { void APIConnection::on_text_command_request(const TextCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(text::Text, text, text) ENTITY_COMMAND_MAKE_CALL(text::Text, text, text)
call.set_value(msg.state); call.set_value(msg.state);
call.perform(); call.perform();
@@ -874,7 +875,7 @@ uint16_t APIConnection::try_send_select_info(EntityBase *entity, APIConnection *
msg.options = &select->traits.get_options(); msg.options = &select->traits.get_options();
return fill_and_encode_entity_info(select, msg, ListEntitiesSelectResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_info(select, msg, ListEntitiesSelectResponse::MESSAGE_TYPE, conn, remaining_size);
} }
void APIConnection::select_command(const SelectCommandRequest &msg) { void APIConnection::on_select_command_request(const SelectCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(select::Select, select, select) ENTITY_COMMAND_MAKE_CALL(select::Select, select, select)
call.set_option(msg.state.c_str(), msg.state.size()); call.set_option(msg.state.c_str(), msg.state.size());
call.perform(); call.perform();
@@ -888,7 +889,7 @@ uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection *
msg.device_class = button->get_device_class_ref(); msg.device_class = button->get_device_class_ref();
return fill_and_encode_entity_info(button, msg, ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_info(button, msg, ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size);
} }
void esphome::api::APIConnection::button_command(const ButtonCommandRequest &msg) { void esphome::api::APIConnection::on_button_command_request(const ButtonCommandRequest &msg) {
ENTITY_COMMAND_GET(button::Button, button, button) ENTITY_COMMAND_GET(button::Button, button, button)
button->press(); button->press();
} }
@@ -914,7 +915,7 @@ uint16_t APIConnection::try_send_lock_info(EntityBase *entity, APIConnection *co
msg.requires_code = a_lock->traits.get_requires_code(); msg.requires_code = a_lock->traits.get_requires_code();
return fill_and_encode_entity_info(a_lock, msg, ListEntitiesLockResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_info(a_lock, msg, ListEntitiesLockResponse::MESSAGE_TYPE, conn, remaining_size);
} }
void APIConnection::lock_command(const LockCommandRequest &msg) { void APIConnection::on_lock_command_request(const LockCommandRequest &msg) {
ENTITY_COMMAND_GET(lock::Lock, a_lock, lock) ENTITY_COMMAND_GET(lock::Lock, a_lock, lock)
switch (msg.command) { switch (msg.command) {
@@ -952,7 +953,7 @@ uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *c
msg.supports_stop = traits.get_supports_stop(); msg.supports_stop = traits.get_supports_stop();
return fill_and_encode_entity_info(valve, msg, ListEntitiesValveResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_info(valve, msg, ListEntitiesValveResponse::MESSAGE_TYPE, conn, remaining_size);
} }
void APIConnection::valve_command(const ValveCommandRequest &msg) { void APIConnection::on_valve_command_request(const ValveCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(valve::Valve, valve, valve) ENTITY_COMMAND_MAKE_CALL(valve::Valve, valve, valve)
if (msg.has_position) if (msg.has_position)
call.set_position(msg.position); call.set_position(msg.position);
@@ -996,7 +997,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, return fill_and_encode_entity_info(media_player, msg, ListEntitiesMediaPlayerResponse::MESSAGE_TYPE, conn,
remaining_size); remaining_size);
} }
void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) { void APIConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(media_player::MediaPlayer, media_player, media_player) ENTITY_COMMAND_MAKE_CALL(media_player::MediaPlayer, media_player, media_player)
if (msg.has_command) { if (msg.has_command) {
call.set_command(static_cast<media_player::MediaPlayerCommand>(msg.command)); call.set_command(static_cast<media_player::MediaPlayerCommand>(msg.command));
@@ -1063,7 +1064,7 @@ uint16_t APIConnection::try_send_camera_info(EntityBase *entity, APIConnection *
ListEntitiesCameraResponse msg; ListEntitiesCameraResponse msg;
return fill_and_encode_entity_info(camera, msg, ListEntitiesCameraResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_info(camera, msg, ListEntitiesCameraResponse::MESSAGE_TYPE, conn, remaining_size);
} }
void APIConnection::camera_image(const CameraImageRequest &msg) { void APIConnection::on_camera_image_request(const CameraImageRequest &msg) {
if (camera::Camera::instance() == nullptr) if (camera::Camera::instance() == nullptr)
return; return;
@@ -1092,41 +1093,47 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) {
#endif #endif
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
void APIConnection::subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) { void APIConnection::on_subscribe_bluetooth_le_advertisements_request(
const SubscribeBluetoothLEAdvertisementsRequest &msg) {
bluetooth_proxy::global_bluetooth_proxy->subscribe_api_connection(this, msg.flags); bluetooth_proxy::global_bluetooth_proxy->subscribe_api_connection(this, msg.flags);
} }
void APIConnection::unsubscribe_bluetooth_le_advertisements() { void APIConnection::on_unsubscribe_bluetooth_le_advertisements_request() {
bluetooth_proxy::global_bluetooth_proxy->unsubscribe_api_connection(this); bluetooth_proxy::global_bluetooth_proxy->unsubscribe_api_connection(this);
} }
void APIConnection::bluetooth_device_request(const BluetoothDeviceRequest &msg) { void APIConnection::on_bluetooth_device_request(const BluetoothDeviceRequest &msg) {
bluetooth_proxy::global_bluetooth_proxy->bluetooth_device_request(msg); bluetooth_proxy::global_bluetooth_proxy->bluetooth_device_request(msg);
} }
void APIConnection::bluetooth_gatt_read(const BluetoothGATTReadRequest &msg) { void APIConnection::on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg) {
bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_read(msg); bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_read(msg);
} }
void APIConnection::bluetooth_gatt_write(const BluetoothGATTWriteRequest &msg) { void APIConnection::on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg) {
bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_write(msg); bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_write(msg);
} }
void APIConnection::bluetooth_gatt_read_descriptor(const BluetoothGATTReadDescriptorRequest &msg) { void APIConnection::on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg) {
bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_read_descriptor(msg); bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_read_descriptor(msg);
} }
void APIConnection::bluetooth_gatt_write_descriptor(const BluetoothGATTWriteDescriptorRequest &msg) { void APIConnection::on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg) {
bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_write_descriptor(msg); bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_write_descriptor(msg);
} }
void APIConnection::bluetooth_gatt_get_services(const BluetoothGATTGetServicesRequest &msg) { void APIConnection::on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg) {
bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_send_services(msg); bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_send_services(msg);
} }
void APIConnection::bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) { void APIConnection::on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) {
bluetooth_proxy::global_bluetooth_proxy->bluetooth_gatt_notify(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); bluetooth_proxy::global_bluetooth_proxy->send_connections_free(this);
return true; return true;
} }
void APIConnection::on_subscribe_bluetooth_connections_free_request() {
if (!this->send_subscribe_bluetooth_connections_free_response_()) {
this->on_fatal_error();
}
}
void APIConnection::bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) { void APIConnection::on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) {
bluetooth_proxy::global_bluetooth_proxy->bluetooth_scanner_set_mode( bluetooth_proxy::global_bluetooth_proxy->bluetooth_scanner_set_mode(
msg.mode == enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE); msg.mode == enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE);
} }
@@ -1138,7 +1145,7 @@ bool APIConnection::check_voice_assistant_api_connection_() const {
voice_assistant::global_voice_assistant->get_api_connection() == this; voice_assistant::global_voice_assistant->get_api_connection() == this;
} }
void APIConnection::subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) { void APIConnection::on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) {
if (voice_assistant::global_voice_assistant != nullptr) { if (voice_assistant::global_voice_assistant != nullptr) {
voice_assistant::global_voice_assistant->client_subscription(this, msg.subscribe); voice_assistant::global_voice_assistant->client_subscription(this, msg.subscribe);
} }
@@ -1184,7 +1191,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; VoiceAssistantConfigurationResponse resp;
if (!this->check_voice_assistant_api_connection_()) { if (!this->check_voice_assistant_api_connection_()) {
return this->send_message(resp, VoiceAssistantConfigurationResponse::MESSAGE_TYPE); return this->send_message(resp, VoiceAssistantConfigurationResponse::MESSAGE_TYPE);
@@ -1221,8 +1228,13 @@ bool APIConnection::send_voice_assistant_get_configuration_response(const VoiceA
resp.max_active_wake_words = config.max_active_wake_words; resp.max_active_wake_words = config.max_active_wake_words;
return this->send_message(resp, VoiceAssistantConfigurationResponse::MESSAGE_TYPE); 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::voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) { void APIConnection::on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) {
if (this->check_voice_assistant_api_connection_()) { if (this->check_voice_assistant_api_connection_()) {
voice_assistant::global_voice_assistant->on_set_configuration(msg.active_wake_words); voice_assistant::global_voice_assistant->on_set_configuration(msg.active_wake_words);
} }
@@ -1230,11 +1242,11 @@ void APIConnection::voice_assistant_set_configuration(const VoiceAssistantSetCon
#endif #endif
#ifdef USE_ZWAVE_PROXY #ifdef USE_ZWAVE_PROXY
void APIConnection::zwave_proxy_frame(const ZWaveProxyFrame &msg) { void APIConnection::on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) {
zwave_proxy::global_zwave_proxy->send_frame(msg.data, msg.data_len); zwave_proxy::global_zwave_proxy->send_frame(msg.data, msg.data_len);
} }
void APIConnection::zwave_proxy_request(const ZWaveProxyRequest &msg) { void APIConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) {
zwave_proxy::global_zwave_proxy->zwave_proxy_request(this, msg.type); zwave_proxy::global_zwave_proxy->zwave_proxy_request(this, msg.type);
} }
#endif #endif
@@ -1262,7 +1274,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, return fill_and_encode_entity_info(a_alarm_control_panel, msg, ListEntitiesAlarmControlPanelResponse::MESSAGE_TYPE,
conn, remaining_size); conn, remaining_size);
} }
void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) { void APIConnection::on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(alarm_control_panel::AlarmControlPanel, a_alarm_control_panel, alarm_control_panel) ENTITY_COMMAND_MAKE_CALL(alarm_control_panel::AlarmControlPanel, a_alarm_control_panel, alarm_control_panel)
switch (msg.command) { switch (msg.command) {
case enums::ALARM_CONTROL_PANEL_DISARM: case enums::ALARM_CONTROL_PANEL_DISARM:
@@ -1322,7 +1334,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); return fill_and_encode_entity_info(wh, msg, ListEntitiesWaterHeaterResponse::MESSAGE_TYPE, conn, remaining_size);
} }
void APIConnection::water_heater_command(const WaterHeaterCommandRequest &msg) { void APIConnection::on_water_heater_command_request(const WaterHeaterCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(water_heater::WaterHeater, water_heater, water_heater) ENTITY_COMMAND_MAKE_CALL(water_heater::WaterHeater, water_heater, water_heater)
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_MODE) if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_MODE)
call.set_mode(static_cast<water_heater::WaterHeaterMode>(msg.mode)); call.set_mode(static_cast<water_heater::WaterHeaterMode>(msg.mode));
@@ -1332,8 +1344,12 @@ void APIConnection::water_heater_command(const WaterHeaterCommandRequest &msg) {
call.set_target_temperature_low(msg.target_temperature_low); call.set_target_temperature_low(msg.target_temperature_low);
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH) if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH)
call.set_target_temperature_high(msg.target_temperature_high); call.set_target_temperature_high(msg.target_temperature_high);
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_STATE) { if ((msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_AWAY_STATE) ||
(msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_STATE)) {
call.set_away((msg.state & water_heater::WATER_HEATER_STATE_AWAY) != 0); call.set_away((msg.state & water_heater::WATER_HEATER_STATE_AWAY) != 0);
}
if ((msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_ON_STATE) ||
(msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_STATE)) {
call.set_on((msg.state & water_heater::WATER_HEATER_STATE_ON) != 0); call.set_on((msg.state & water_heater::WATER_HEATER_STATE_ON) != 0);
} }
call.perform(); call.perform();
@@ -1364,7 +1380,7 @@ uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *c
#endif #endif
#ifdef USE_IR_RF #ifdef USE_IR_RF
void APIConnection::infrared_rf_transmit_raw_timings(const InfraredRFTransmitRawTimingsRequest &msg) { void APIConnection::on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) {
// TODO: When RF is implemented, add a field to the message to distinguish IR vs RF // 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. // and dispatch to the appropriate entity type based on that field.
#ifdef USE_INFRARED #ifdef USE_INFRARED
@@ -1418,7 +1434,7 @@ uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection *
msg.device_class = update->get_device_class_ref(); msg.device_class = update->get_device_class_ref();
return fill_and_encode_entity_info(update, msg, ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size); return fill_and_encode_entity_info(update, msg, ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size);
} }
void APIConnection::update_command(const UpdateCommandRequest &msg) { void APIConnection::on_update_command_request(const UpdateCommandRequest &msg) {
ENTITY_COMMAND_GET(update::UpdateEntity, update, update) ENTITY_COMMAND_GET(update::UpdateEntity, update, update)
switch (msg.command) { switch (msg.command) {
@@ -1454,8 +1470,11 @@ void APIConnection::complete_authentication_() {
this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED); this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED);
this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("connected")); this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("connected"));
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER #ifdef USE_API_CLIENT_CONNECTED_TRIGGER
this->parent_->get_client_connected_trigger()->trigger(std::string(this->helper_->get_client_name()), {
std::string(this->helper_->get_client_peername())); 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)));
}
#endif #endif
#ifdef USE_HOMEASSISTANT_TIME #ifdef USE_HOMEASSISTANT_TIME
if (homeassistant::global_homeassistant_time != nullptr) { if (homeassistant::global_homeassistant_time != nullptr) {
@@ -1469,13 +1488,14 @@ void APIConnection::complete_authentication_() {
#endif #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) // 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->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_major_ = msg.api_version_major;
this->client_api_version_minor_ = msg.api_version_minor; 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(), ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->helper_->get_client_name(),
this->helper_->get_client_peername(), this->client_api_version_major_, this->client_api_version_minor_); this->helper_->get_peername_to(peername), this->client_api_version_major_, this->client_api_version_minor_);
HelloResponse resp; HelloResponse resp;
resp.api_version_major = 1; resp.api_version_major = 1;
@@ -1490,12 +1510,12 @@ bool APIConnection::send_hello_response(const HelloRequest &msg) {
return this->send_message(resp, HelloResponse::MESSAGE_TYPE); return this->send_message(resp, HelloResponse::MESSAGE_TYPE);
} }
bool APIConnection::send_ping_response() { bool APIConnection::send_ping_response_() {
PingResponse resp; PingResponse resp;
return this->send_message(resp, PingResponse::MESSAGE_TYPE); return this->send_message(resp, PingResponse::MESSAGE_TYPE);
} }
bool APIConnection::send_device_info_response() { bool APIConnection::send_device_info_response_() {
DeviceInfoResponse resp{}; DeviceInfoResponse resp{};
resp.name = StringRef(App.get_name()); resp.name = StringRef(App.get_name());
resp.friendly_name = StringRef(App.get_friendly_name()); resp.friendly_name = StringRef(App.get_friendly_name());
@@ -1618,6 +1638,26 @@ bool APIConnection::send_device_info_response() {
return this->send_message(resp, DeviceInfoResponse::MESSAGE_TYPE); 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 #ifdef USE_API_HOMEASSISTANT_STATES
void APIConnection::on_home_assistant_state_response(const HomeAssistantStateResponse &msg) { void APIConnection::on_home_assistant_state_response(const HomeAssistantStateResponse &msg) {
@@ -1656,7 +1696,7 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes
} }
#endif #endif
#ifdef USE_API_USER_DEFINED_ACTIONS #ifdef USE_API_USER_DEFINED_ACTIONS
void APIConnection::execute_service(const ExecuteServiceRequest &msg) { void APIConnection::on_execute_service_request(const ExecuteServiceRequest &msg) {
bool found = false; bool found = false;
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
// Register the call and get a unique server-generated action_call_id // Register the call and get a unique server-generated action_call_id
@@ -1722,7 +1762,7 @@ void APIConnection::on_homeassistant_action_response(const HomeassistantActionRe
}; };
#endif #endif
#ifdef USE_API_NOISE #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; NoiseEncryptionSetKeyResponse resp;
resp.success = false; resp.success = false;
@@ -1743,9 +1783,14 @@ bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryption
return this->send_message(resp, NoiseEncryptionSetKeyResponse::MESSAGE_TYPE); 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 #endif
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
void APIConnection::subscribe_home_assistant_states() { state_subs_at_ = 0; } void APIConnection::on_subscribe_home_assistant_states_request() { state_subs_at_ = 0; }
#endif #endif
bool APIConnection::try_to_clear_buffer(bool log_out_of_space) { bool APIConnection::try_to_clear_buffer(bool log_out_of_space) {
if (this->flags_.remove) if (this->flags_.remove)
@@ -1798,7 +1843,8 @@ void APIConnection::on_no_setup_connection() {
this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("no connection setup")); this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("no connection setup"));
} }
void APIConnection::on_fatal_error() { void APIConnection::on_fatal_error() {
this->helper_->close(); // 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->flags_.remove = true; this->flags_.remove = true;
} }
@@ -2155,12 +2201,14 @@ void APIConnection::process_state_subscriptions_() {
#endif // USE_API_HOMEASSISTANT_STATES #endif // USE_API_HOMEASSISTANT_STATES
void APIConnection::log_client_(int level, const LogString *message) { 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(), esp_log_printf_(level, TAG, __LINE__, ESPHOME_LOG_FORMAT("%s (%s): %s"), this->helper_->get_client_name(),
this->helper_->get_client_peername(), LOG_STR_ARG(message)); this->helper_->get_peername_to(peername), LOG_STR_ARG(message));
} }
void APIConnection::log_warning_(const LogString *message, APIError err) { void APIConnection::log_warning_(const LogString *message, APIError err) {
ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->helper_->get_client_name(), this->helper_->get_client_peername(), 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),
LOG_STR_ARG(message), LOG_STR_ARG(api_error_to_logstr(err)), errno); LOG_STR_ARG(message), LOG_STR_ARG(api_error_to_logstr(err)), errno);
} }

View File

@@ -28,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, static_assert(MAX_MESSAGES_PER_BATCH >= MAX_INITIAL_PER_BATCH,
"MAX_MESSAGES_PER_BATCH must be >= MAX_INITIAL_PER_BATCH"); "MAX_MESSAGES_PER_BATCH must be >= MAX_INITIAL_PER_BATCH");
class APIConnection final : public APIServerConnection { class APIConnection final : public APIServerConnectionBase {
public: public:
friend class APIServer; friend class APIServer;
friend class ListEntitiesIterator; friend class ListEntitiesIterator;
@@ -47,72 +47,72 @@ class APIConnection final : public APIServerConnection {
#endif #endif
#ifdef USE_COVER #ifdef USE_COVER
bool send_cover_state(cover::Cover *cover); bool send_cover_state(cover::Cover *cover);
void cover_command(const CoverCommandRequest &msg) override; void on_cover_command_request(const CoverCommandRequest &msg) override;
#endif #endif
#ifdef USE_FAN #ifdef USE_FAN
bool send_fan_state(fan::Fan *fan); bool send_fan_state(fan::Fan *fan);
void fan_command(const FanCommandRequest &msg) override; void on_fan_command_request(const FanCommandRequest &msg) override;
#endif #endif
#ifdef USE_LIGHT #ifdef USE_LIGHT
bool send_light_state(light::LightState *light); bool send_light_state(light::LightState *light);
void light_command(const LightCommandRequest &msg) override; void on_light_command_request(const LightCommandRequest &msg) override;
#endif #endif
#ifdef USE_SENSOR #ifdef USE_SENSOR
bool send_sensor_state(sensor::Sensor *sensor); bool send_sensor_state(sensor::Sensor *sensor);
#endif #endif
#ifdef USE_SWITCH #ifdef USE_SWITCH
bool send_switch_state(switch_::Switch *a_switch); bool send_switch_state(switch_::Switch *a_switch);
void switch_command(const SwitchCommandRequest &msg) override; void on_switch_command_request(const SwitchCommandRequest &msg) override;
#endif #endif
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
bool send_text_sensor_state(text_sensor::TextSensor *text_sensor); bool send_text_sensor_state(text_sensor::TextSensor *text_sensor);
#endif #endif
#ifdef USE_CAMERA #ifdef USE_CAMERA
void set_camera_state(std::shared_ptr<camera::CameraImage> image); void set_camera_state(std::shared_ptr<camera::CameraImage> image);
void camera_image(const CameraImageRequest &msg) override; void on_camera_image_request(const CameraImageRequest &msg) override;
#endif #endif
#ifdef USE_CLIMATE #ifdef USE_CLIMATE
bool send_climate_state(climate::Climate *climate); bool send_climate_state(climate::Climate *climate);
void climate_command(const ClimateCommandRequest &msg) override; void on_climate_command_request(const ClimateCommandRequest &msg) override;
#endif #endif
#ifdef USE_NUMBER #ifdef USE_NUMBER
bool send_number_state(number::Number *number); bool send_number_state(number::Number *number);
void number_command(const NumberCommandRequest &msg) override; void on_number_command_request(const NumberCommandRequest &msg) override;
#endif #endif
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
bool send_date_state(datetime::DateEntity *date); bool send_date_state(datetime::DateEntity *date);
void date_command(const DateCommandRequest &msg) override; void on_date_command_request(const DateCommandRequest &msg) override;
#endif #endif
#ifdef USE_DATETIME_TIME #ifdef USE_DATETIME_TIME
bool send_time_state(datetime::TimeEntity *time); bool send_time_state(datetime::TimeEntity *time);
void time_command(const TimeCommandRequest &msg) override; void on_time_command_request(const TimeCommandRequest &msg) override;
#endif #endif
#ifdef USE_DATETIME_DATETIME #ifdef USE_DATETIME_DATETIME
bool send_datetime_state(datetime::DateTimeEntity *datetime); bool send_datetime_state(datetime::DateTimeEntity *datetime);
void datetime_command(const DateTimeCommandRequest &msg) override; void on_date_time_command_request(const DateTimeCommandRequest &msg) override;
#endif #endif
#ifdef USE_TEXT #ifdef USE_TEXT
bool send_text_state(text::Text *text); bool send_text_state(text::Text *text);
void text_command(const TextCommandRequest &msg) override; void on_text_command_request(const TextCommandRequest &msg) override;
#endif #endif
#ifdef USE_SELECT #ifdef USE_SELECT
bool send_select_state(select::Select *select); bool send_select_state(select::Select *select);
void select_command(const SelectCommandRequest &msg) override; void on_select_command_request(const SelectCommandRequest &msg) override;
#endif #endif
#ifdef USE_BUTTON #ifdef USE_BUTTON
void button_command(const ButtonCommandRequest &msg) override; void on_button_command_request(const ButtonCommandRequest &msg) override;
#endif #endif
#ifdef USE_LOCK #ifdef USE_LOCK
bool send_lock_state(lock::Lock *a_lock); bool send_lock_state(lock::Lock *a_lock);
void lock_command(const LockCommandRequest &msg) override; void on_lock_command_request(const LockCommandRequest &msg) override;
#endif #endif
#ifdef USE_VALVE #ifdef USE_VALVE
bool send_valve_state(valve::Valve *valve); bool send_valve_state(valve::Valve *valve);
void valve_command(const ValveCommandRequest &msg) override; void on_valve_command_request(const ValveCommandRequest &msg) override;
#endif #endif
#ifdef USE_MEDIA_PLAYER #ifdef USE_MEDIA_PLAYER
bool send_media_player_state(media_player::MediaPlayer *media_player); bool send_media_player_state(media_player::MediaPlayer *media_player);
void media_player_command(const MediaPlayerCommandRequest &msg) override; void on_media_player_command_request(const MediaPlayerCommandRequest &msg) override;
#endif #endif
bool try_send_log_message(int level, const char *tag, const char *line, size_t message_len); bool try_send_log_message(int level, const char *tag, const char *line, size_t message_len);
#ifdef USE_API_HOMEASSISTANT_SERVICES #ifdef USE_API_HOMEASSISTANT_SERVICES
@@ -126,18 +126,18 @@ class APIConnection final : public APIServerConnection {
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES #endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
#endif // USE_API_HOMEASSISTANT_SERVICES #endif // USE_API_HOMEASSISTANT_SERVICES
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
void unsubscribe_bluetooth_le_advertisements() override; void on_unsubscribe_bluetooth_le_advertisements_request() override;
void bluetooth_device_request(const BluetoothDeviceRequest &msg) override; void on_bluetooth_device_request(const BluetoothDeviceRequest &msg) override;
void bluetooth_gatt_read(const BluetoothGATTReadRequest &msg) override; void on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg) override;
void bluetooth_gatt_write(const BluetoothGATTWriteRequest &msg) override; void on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg) override;
void bluetooth_gatt_read_descriptor(const BluetoothGATTReadDescriptorRequest &msg) override; void on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg) override;
void bluetooth_gatt_write_descriptor(const BluetoothGATTWriteDescriptorRequest &msg) override; void on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg) override;
void bluetooth_gatt_get_services(const BluetoothGATTGetServicesRequest &msg) override; void on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg) override;
void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) override; void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) override;
bool send_subscribe_bluetooth_connections_free_response() override; void on_subscribe_bluetooth_connections_free_request() override;
void bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) override; void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) override;
#endif #endif
#ifdef USE_HOMEASSISTANT_TIME #ifdef USE_HOMEASSISTANT_TIME
@@ -148,33 +148,33 @@ class APIConnection final : public APIServerConnection {
#endif #endif
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) override; void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) override;
void on_voice_assistant_response(const VoiceAssistantResponse &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_event_response(const VoiceAssistantEventResponse &msg) override;
void on_voice_assistant_audio(const VoiceAssistantAudio &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_timer_event_response(const VoiceAssistantTimerEventResponse &msg) override;
void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg) override; void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg) override;
bool send_voice_assistant_get_configuration_response(const VoiceAssistantConfigurationRequest &msg) override; void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) override;
void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override; void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override;
#endif #endif
#ifdef USE_ZWAVE_PROXY #ifdef USE_ZWAVE_PROXY
void zwave_proxy_frame(const ZWaveProxyFrame &msg) override; void on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) override;
void zwave_proxy_request(const ZWaveProxyRequest &msg) override; void on_z_wave_proxy_request(const ZWaveProxyRequest &msg) override;
#endif #endif
#ifdef USE_ALARM_CONTROL_PANEL #ifdef USE_ALARM_CONTROL_PANEL
bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel); bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override; void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) override;
#endif #endif
#ifdef USE_WATER_HEATER #ifdef USE_WATER_HEATER
bool send_water_heater_state(water_heater::WaterHeater *water_heater); bool send_water_heater_state(water_heater::WaterHeater *water_heater);
void water_heater_command(const WaterHeaterCommandRequest &msg) override; void on_water_heater_command_request(const WaterHeaterCommandRequest &msg) override;
#endif #endif
#ifdef USE_IR_RF #ifdef USE_IR_RF
void infrared_rf_transmit_raw_timings(const InfraredRFTransmitRawTimingsRequest &msg) override; void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) override;
void send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg); void send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg);
#endif #endif
@@ -184,7 +184,7 @@ class APIConnection final : public APIServerConnection {
#ifdef USE_UPDATE #ifdef USE_UPDATE
bool send_update_state(update::UpdateEntity *update); bool send_update_state(update::UpdateEntity *update);
void update_command(const UpdateCommandRequest &msg) override; void on_update_command_request(const UpdateCommandRequest &msg) override;
#endif #endif
void on_disconnect_response() override; void on_disconnect_response() override;
@@ -198,12 +198,12 @@ class APIConnection final : public APIServerConnection {
#ifdef USE_HOMEASSISTANT_TIME #ifdef USE_HOMEASSISTANT_TIME
void on_get_time_response(const GetTimeResponse &value) override; void on_get_time_response(const GetTimeResponse &value) override;
#endif #endif
bool send_hello_response(const HelloRequest &msg) override; void on_hello_request(const HelloRequest &msg) override;
bool send_disconnect_response() override; void on_disconnect_request() override;
bool send_ping_response() override; void on_ping_request() override;
bool send_device_info_response() override; void on_device_info_request() override;
void list_entities() override { this->begin_iterator_(ActiveIterator::LIST_ENTITIES); } void on_list_entities_request() override { this->begin_iterator_(ActiveIterator::LIST_ENTITIES); }
void subscribe_states() override { void on_subscribe_states_request() override {
this->flags_.state_subscription = true; this->flags_.state_subscription = true;
// Start initial state iterator only if no iterator is active // Start initial state iterator only if no iterator is active
// If list_entities is running, we'll start initial_state when it completes // If list_entities is running, we'll start initial_state when it completes
@@ -211,19 +211,19 @@ class APIConnection final : public APIServerConnection {
this->begin_iterator_(ActiveIterator::INITIAL_STATE); this->begin_iterator_(ActiveIterator::INITIAL_STATE);
} }
} }
void subscribe_logs(const SubscribeLogsRequest &msg) override { void on_subscribe_logs_request(const SubscribeLogsRequest &msg) override {
this->flags_.log_subscription = msg.level; this->flags_.log_subscription = msg.level;
if (msg.dump_config) if (msg.dump_config)
App.schedule_dump_config(); App.schedule_dump_config();
} }
#ifdef USE_API_HOMEASSISTANT_SERVICES #ifdef USE_API_HOMEASSISTANT_SERVICES
void subscribe_homeassistant_services() override { this->flags_.service_call_subscription = true; } void on_subscribe_homeassistant_services_request() override { this->flags_.service_call_subscription = true; }
#endif #endif
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
void subscribe_home_assistant_states() override; void on_subscribe_home_assistant_states_request() override;
#endif #endif
#ifdef USE_API_USER_DEFINED_ACTIONS #ifdef USE_API_USER_DEFINED_ACTIONS
void execute_service(const ExecuteServiceRequest &msg) override; void on_execute_service_request(const ExecuteServiceRequest &msg) override;
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
void send_execute_service_response(uint32_t call_id, bool success, StringRef error_message); void send_execute_service_response(uint32_t call_id, bool success, StringRef error_message);
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
@@ -233,7 +233,7 @@ class APIConnection final : public APIServerConnection {
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES #endif // USE_API_USER_DEFINED_ACTION_RESPONSES
#endif #endif
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
bool send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) override; void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) override;
#endif #endif
bool is_authenticated() override { bool is_authenticated() override {
@@ -276,13 +276,30 @@ class APIConnection final : public APIServerConnection {
bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override; bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override;
const char *get_name() const { return this->helper_->get_client_name(); } const char *get_name() const { return this->helper_->get_client_name(); }
/// Get peer name (IP address) - cached at connection init time /// Get peer name (IP address) into caller-provided buffer, returns buf for convenience
const char *get_peername() const { return this->helper_->get_client_peername(); } const char *get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const {
return this->helper_->get_peername_to(buf);
}
protected: protected:
// Helper function to handle authentication completion // Helper function to handle authentication completion
void complete_authentication_(); 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 #ifdef USE_CAMERA
void try_send_camera_image_(); void try_send_camera_image_();
#endif #endif

View File

@@ -16,7 +16,12 @@ static const char *const TAG = "api.frame_helper";
static constexpr size_t API_MAX_LOG_BYTES = 168; static constexpr size_t API_MAX_LOG_BYTES = 168;
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__) #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)
#else #else
#define HELPER_LOG(msg, ...) ((void) 0) #define HELPER_LOG(msg, ...) ((void) 0)
#endif #endif
@@ -240,13 +245,20 @@ APIError APIFrameHelper::try_send_tx_buf_() {
return APIError::OK; // All buffers sent successfully 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_() { APIError APIFrameHelper::init_common_() {
if (state_ != State::INITIALIZE || this->socket_ == nullptr) { if (state_ != State::INITIALIZE || this->socket_ == nullptr) {
HELPER_LOG("Bad state for init %d", (int) state_); HELPER_LOG("Bad state for init %d", (int) state_);
return APIError::BAD_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); int err = this->socket_->setblocking(false);
if (err != 0) { if (err != 0) {
state_ = State::FAILED; state_ = State::FAILED;

View File

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

View File

@@ -29,7 +29,12 @@ static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit")
static constexpr size_t API_MAX_LOG_BYTES = 168; static constexpr size_t API_MAX_LOG_BYTES = 168;
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__) #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)
#else #else
#define HELPER_LOG(msg, ...) ((void) 0) #define HELPER_LOG(msg, ...) ((void) 0)
#endif #endif

View File

@@ -21,7 +21,12 @@ static const char *const TAG = "api.plaintext";
static constexpr size_t API_MAX_LOG_BYTES = 168; static constexpr size_t API_MAX_LOG_BYTES = 168;
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__) #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)
#else #else
#define HELPER_LOG(msg, ...) ((void) 0) #define HELPER_LOG(msg, ...) ((void) 0)
#endif #endif

View File

@@ -147,6 +147,8 @@ enum WaterHeaterCommandHasField : uint32_t {
WATER_HEATER_COMMAND_HAS_STATE = 4, WATER_HEATER_COMMAND_HAS_STATE = 4,
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8, WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8,
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH = 16, WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH = 16,
WATER_HEATER_COMMAND_HAS_ON_STATE = 32,
WATER_HEATER_COMMAND_HAS_AWAY_STATE = 64,
}; };
#ifdef USE_NUMBER #ifdef USE_NUMBER
enum NumberMode : uint32_t { enum NumberMode : uint32_t {
@@ -440,19 +442,6 @@ class PingResponse final : public ProtoMessage {
protected: 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 #ifdef USE_AREAS
class AreaInfo final : public ProtoMessage { class AreaInfo final : public ProtoMessage {
public: public:
@@ -546,19 +535,6 @@ class DeviceInfoResponse final : public ProtoMessage {
protected: 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 { class ListEntitiesDoneResponse final : public ProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 19; static constexpr uint8_t MESSAGE_TYPE = 19;
@@ -572,19 +548,6 @@ class ListEntitiesDoneResponse final : public ProtoMessage {
protected: 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 #ifdef USE_BINARY_SENSOR
class ListEntitiesBinarySensorResponse final : public InfoResponseProtoMessage { class ListEntitiesBinarySensorResponse final : public InfoResponseProtoMessage {
public: public:
@@ -1037,19 +1000,6 @@ class NoiseEncryptionSetKeyResponse final : public ProtoMessage {
}; };
#endif #endif
#ifdef USE_API_HOMEASSISTANT_SERVICES #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 { class HomeassistantServiceMap final : public ProtoMessage {
public: public:
StringRef key{}; StringRef key{};
@@ -1117,19 +1067,6 @@ class HomeassistantActionResponse final : public ProtoDecodableMessage {
}; };
#endif #endif
#ifdef USE_API_HOMEASSISTANT_STATES #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 { class SubscribeHomeAssistantStateResponse final : public ProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 39; static constexpr uint8_t MESSAGE_TYPE = 39;
@@ -2160,19 +2097,6 @@ class BluetoothGATTNotifyDataResponse final : public ProtoMessage {
protected: 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 { class BluetoothConnectionsFreeResponse final : public ProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 81; static constexpr uint8_t MESSAGE_TYPE = 81;
@@ -2279,19 +2203,6 @@ class BluetoothDeviceUnpairingResponse final : public ProtoMessage {
protected: 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 { class BluetoothDeviceClearCacheResponse final : public ProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 88; static constexpr uint8_t MESSAGE_TYPE = 88;

View File

@@ -385,6 +385,10 @@ const char *proto_enum_to_string<enums::WaterHeaterCommandHasField>(enums::Water
return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW"; return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW";
case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH: case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH:
return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH"; return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH";
case enums::WATER_HEATER_COMMAND_HAS_ON_STATE:
return "WATER_HEATER_COMMAND_HAS_ON_STATE";
case enums::WATER_HEATER_COMMAND_HAS_AWAY_STATE:
return "WATER_HEATER_COMMAND_HAS_AWAY_STATE";
default: default:
return "UNKNOWN"; return "UNKNOWN";
} }
@@ -764,10 +768,6 @@ const char *PingResponse::dump_to(DumpBuffer &out) const {
out.append("PingResponse {}"); out.append("PingResponse {}");
return out.c_str(); return out.c_str();
} }
const char *DeviceInfoRequest::dump_to(DumpBuffer &out) const {
out.append("DeviceInfoRequest {}");
return out.c_str();
}
#ifdef USE_AREAS #ifdef USE_AREAS
const char *AreaInfo::dump_to(DumpBuffer &out) const { const char *AreaInfo::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "AreaInfo"); MessageDumpHelper helper(out, "AreaInfo");
@@ -848,18 +848,10 @@ const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const {
#endif #endif
return out.c_str(); 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 { const char *ListEntitiesDoneResponse::dump_to(DumpBuffer &out) const {
out.append("ListEntitiesDoneResponse {}"); out.append("ListEntitiesDoneResponse {}");
return out.c_str(); return out.c_str();
} }
const char *SubscribeStatesRequest::dump_to(DumpBuffer &out) const {
out.append("SubscribeStatesRequest {}");
return out.c_str();
}
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
const char *ListEntitiesBinarySensorResponse::dump_to(DumpBuffer &out) const { const char *ListEntitiesBinarySensorResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "ListEntitiesBinarySensorResponse"); MessageDumpHelper helper(out, "ListEntitiesBinarySensorResponse");
@@ -1191,10 +1183,6 @@ const char *NoiseEncryptionSetKeyResponse::dump_to(DumpBuffer &out) const {
} }
#endif #endif
#ifdef USE_API_HOMEASSISTANT_SERVICES #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 { const char *HomeassistantServiceMap::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "HomeassistantServiceMap"); MessageDumpHelper helper(out, "HomeassistantServiceMap");
dump_field(out, "key", this->key); dump_field(out, "key", this->key);
@@ -1245,10 +1233,6 @@ const char *HomeassistantActionResponse::dump_to(DumpBuffer &out) const {
} }
#endif #endif
#ifdef USE_API_HOMEASSISTANT_STATES #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 { const char *SubscribeHomeAssistantStateResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "SubscribeHomeAssistantStateResponse"); MessageDumpHelper helper(out, "SubscribeHomeAssistantStateResponse");
dump_field(out, "entity_id", this->entity_id); dump_field(out, "entity_id", this->entity_id);
@@ -1924,10 +1908,6 @@ const char *BluetoothGATTNotifyDataResponse::dump_to(DumpBuffer &out) const {
dump_bytes_field(out, "data", this->data_ptr_, this->data_len_); dump_bytes_field(out, "data", this->data_ptr_, this->data_len_);
return out.c_str(); 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 { const char *BluetoothConnectionsFreeResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "BluetoothConnectionsFreeResponse"); MessageDumpHelper helper(out, "BluetoothConnectionsFreeResponse");
dump_field(out, "free", this->free); dump_field(out, "free", this->free);
@@ -1970,10 +1950,6 @@ const char *BluetoothDeviceUnpairingResponse::dump_to(DumpBuffer &out) const {
dump_field(out, "error", this->error); dump_field(out, "error", this->error);
return out.c_str(); 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 { const char *BluetoothDeviceClearCacheResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "BluetoothDeviceClearCacheResponse"); MessageDumpHelper helper(out, "BluetoothDeviceClearCacheResponse");
dump_field(out, "address", this->address); dump_field(out, "address", this->address);

View File

@@ -21,6 +21,23 @@ void APIServerConnectionBase::log_receive_message_(const LogString *name) {
#endif #endif
void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) { 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) { switch (msg_type) {
case HelloRequest::MESSAGE_TYPE: { case HelloRequest::MESSAGE_TYPE: {
HelloRequest msg; HelloRequest msg;
@@ -59,21 +76,21 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
this->on_ping_response(); this->on_ping_response();
break; break;
} }
case DeviceInfoRequest::MESSAGE_TYPE: { case 9 /* DeviceInfoRequest is empty */: {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_device_info_request")); this->log_receive_message_(LOG_STR("on_device_info_request"));
#endif #endif
this->on_device_info_request(); this->on_device_info_request();
break; break;
} }
case ListEntitiesRequest::MESSAGE_TYPE: { case 11 /* ListEntitiesRequest is empty */: {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_list_entities_request")); this->log_receive_message_(LOG_STR("on_list_entities_request"));
#endif #endif
this->on_list_entities_request(); this->on_list_entities_request();
break; break;
} }
case SubscribeStatesRequest::MESSAGE_TYPE: { case 20 /* SubscribeStatesRequest is empty */: {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_subscribe_states_request")); this->log_receive_message_(LOG_STR("on_subscribe_states_request"));
#endif #endif
@@ -134,7 +151,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_API_HOMEASSISTANT_SERVICES #ifdef USE_API_HOMEASSISTANT_SERVICES
case SubscribeHomeassistantServicesRequest::MESSAGE_TYPE: { case 34 /* SubscribeHomeassistantServicesRequest is empty */: {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_subscribe_homeassistant_services_request")); this->log_receive_message_(LOG_STR("on_subscribe_homeassistant_services_request"));
#endif #endif
@@ -152,7 +169,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
break; break;
} }
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
case SubscribeHomeAssistantStatesRequest::MESSAGE_TYPE: { case 38 /* SubscribeHomeAssistantStatesRequest is empty */: {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_subscribe_home_assistant_states_request")); this->log_receive_message_(LOG_STR("on_subscribe_home_assistant_states_request"));
#endif #endif
@@ -359,7 +376,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
case SubscribeBluetoothConnectionsFreeRequest::MESSAGE_TYPE: { case 80 /* SubscribeBluetoothConnectionsFreeRequest is empty */: {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_subscribe_bluetooth_connections_free_request")); this->log_receive_message_(LOG_STR("on_subscribe_bluetooth_connections_free_request"));
#endif #endif
@@ -368,7 +385,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
case UnsubscribeBluetoothLEAdvertisementsRequest::MESSAGE_TYPE: { case 87 /* UnsubscribeBluetoothLEAdvertisementsRequest is empty */: {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_unsubscribe_bluetooth_le_advertisements_request")); this->log_receive_message_(LOG_STR("on_unsubscribe_bluetooth_le_advertisements_request"));
#endif #endif
@@ -623,222 +640,4 @@ 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 } // namespace esphome::api

View File

@@ -228,270 +228,4 @@ class APIServerConnectionBase : public ProtoService {
void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override; 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 } // namespace esphome::api

View File

@@ -192,11 +192,15 @@ void APIServer::loop() {
ESP_LOGV(TAG, "Remove connection %s", client->get_name()); ESP_LOGV(TAG, "Remove connection %s", client->get_name());
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER #ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
// Save client info before removal for the 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_name(client->get_name());
std::string client_peername(client->get_peername()); std::string client_peername(client->get_peername_to(peername_buf));
#endif #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) // Swap with the last element and pop (avoids expensive vector shifts)
if (client_index < this->clients_.size() - 1) { if (client_index < this->clients_.size() - 1) {
std::swap(this->clients_[client_index], this->clients_.back()); std::swap(this->clients_[client_index], this->clients_.back());

View File

@@ -25,7 +25,9 @@ template<typename... X> class TemplatableStringValue : public TemplatableValue<s
private: private:
// Helper to convert value to string - handles the case where value is already a string // 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)); } template<typename T> static std::string value_to_string(T &&val) {
return to_string(std::forward<T>(val)); // NOLINT
}
// Overloads for string types - needed because std::to_string doesn't support them // Overloads for string types - needed because std::to_string doesn't support them
static std::string value_to_string(char *val) { static std::string value_to_string(char *val) {

View File

@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <algorithm>
#include <cmath> #include <cmath>
#include <limits> #include <limits>
#include "abstract_aqi_calculator.h" #include "abstract_aqi_calculator.h"
@@ -14,7 +15,11 @@ class AQICalculator : public AbstractAQICalculator {
float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID); 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 pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID);
return static_cast<uint16_t>(std::round((pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index)); 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));
} }
protected: protected:
@@ -22,13 +27,27 @@ 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 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] = {{0.0f, 9.0f}, {9.1f, 35.4f}, static constexpr float PM2_5_GRID[NUM_LEVELS][2] = {
{35.5f, 55.4f}, {55.5f, 125.4f}, // clang-format off
{125.5f, 225.4f}, {225.5f, std::numeric_limits<float>::max()}}; {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 PM10_0_GRID[NUM_LEVELS][2] = {{0.0f, 54.0f}, {55.0f, 154.0f}, static constexpr float PM10_0_GRID[NUM_LEVELS][2] = {
{155.0f, 254.0f}, {255.0f, 354.0f}, // clang-format off
{355.0f, 424.0f}, {425.0f, std::numeric_limits<float>::max()}}; {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 float calculate_index(float value, const float array[NUM_LEVELS][2]) { static float calculate_index(float value, const float array[NUM_LEVELS][2]) {
int grid_index = get_grid_index(value, array); int grid_index = get_grid_index(value, array);
@@ -45,7 +64,10 @@ class AQICalculator : public AbstractAQICalculator {
static int get_grid_index(float value, const float array[NUM_LEVELS][2]) { static int get_grid_index(float value, const float array[NUM_LEVELS][2]) {
for (int i = 0; i < NUM_LEVELS; i++) { for (int i = 0; i < NUM_LEVELS; i++) {
if (value >= array[i][0] && value <= array[i][1]) { 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) {
return i; return i;
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -28,15 +28,28 @@ void DlmsMeterComponent::dump_config() {
void DlmsMeterComponent::loop() { void DlmsMeterComponent::loop() {
// Read while data is available, netznoe uses two frames so allow 2x max frame length // Read while data is available, netznoe uses two frames so allow 2x max frame length
while (this->available()) { size_t avail = this->available();
if (this->receive_buffer_.size() >= MBUS_MAX_FRAME_LENGTH * 2) { if (avail > 0) {
size_t remaining = MBUS_MAX_FRAME_LENGTH * 2 - this->receive_buffer_.size();
if (remaining == 0) {
ESP_LOGW(TAG, "Receive buffer full, dropping remaining bytes"); ESP_LOGW(TAG, "Receive buffer full, dropping remaining bytes");
break; } 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();
}
} }
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_) { if (!this->receive_buffer_.empty() && millis() - this->last_read_ > this->read_timeout_) {

View File

@@ -40,9 +40,7 @@ bool Dsmr::ready_to_request_data_() {
this->start_requesting_data_(); this->start_requesting_data_();
} }
if (!this->requesting_data_) { if (!this->requesting_data_) {
while (this->available()) { this->drain_rx_buffer_();
this->read();
}
} }
} }
return this->requesting_data_; return this->requesting_data_;
@@ -115,138 +113,169 @@ void Dsmr::stop_requesting_data_() {
} else { } else {
ESP_LOGV(TAG, "Stop reading data from P1 port"); ESP_LOGV(TAG, "Stop reading data from P1 port");
} }
while (this->available()) { this->drain_rx_buffer_();
this->read();
}
this->requesting_data_ = false; 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;
}
}
}
void Dsmr::reset_telegram_() { void Dsmr::reset_telegram_() {
this->header_found_ = false; this->header_found_ = false;
this->footer_found_ = false; this->footer_found_ = false;
this->bytes_read_ = 0; this->bytes_read_ = 0;
this->crypt_bytes_read_ = 0; this->crypt_bytes_read_ = 0;
this->crypt_telegram_len_ = 0; this->crypt_telegram_len_ = 0;
this->last_read_time_ = 0;
} }
void Dsmr::receive_telegram_() { void Dsmr::receive_telegram_() {
while (this->available_within_timeout_()) { while (this->available_within_timeout_()) {
const char c = this->read(); // 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;
// Find a new telegram header, i.e. forward slash. for (size_t i = 0; i < to_read; i++) {
if (c == '/') { const char c = static_cast<char>(buf[i]);
ESP_LOGV(TAG, "Header of telegram found");
this->reset_telegram_();
this->header_found_ = true;
}
if (!this->header_found_)
continue;
// Check for buffer overflow. // Find a new telegram header, i.e. forward slash.
if (this->bytes_read_ >= this->max_telegram_len_) { if (c == '/') {
this->reset_telegram_(); ESP_LOGV(TAG, "Header of telegram found");
ESP_LOGE(TAG, "Error: telegram larger than buffer (%d bytes)", this->max_telegram_len_); this->reset_telegram_();
return; this->header_found_ = true;
} }
if (!this->header_found_)
continue;
// Some v2.2 or v3 meters will send a new value which starts with '(' // Check for buffer overflow.
// in a new line, while the value belongs to the previous ObisId. For if (this->bytes_read_ >= this->max_telegram_len_) {
// proper parsing, remove these new line characters. this->reset_telegram_();
if (c == '(') { ESP_LOGE(TAG, "Error: telegram larger than buffer (%d bytes)", this->max_telegram_len_);
while (true) { return;
auto previous_char = this->telegram_[this->bytes_read_ - 1]; }
if (previous_char == '\n' || previous_char == '\r') {
this->bytes_read_--; // Some v2.2 or v3 meters will send a new value which starts with '('
} else { // in a new line, while the value belongs to the previous ObisId. For
break; // 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;
} }
} }
} }
// 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_() { void Dsmr::receive_encrypted_telegram_() {
while (this->available_within_timeout_()) { while (this->available_within_timeout_()) {
const char c = this->read(); // 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;
// Find a new telegram start byte. for (size_t i = 0; i < to_read; i++) {
if (!this->header_found_) { const char c = static_cast<char>(buf[i]);
if ((uint8_t) c != 0xDB) {
continue; // 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;
} }
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,6 +85,7 @@ class Dsmr : public Component, public uart::UARTDevice {
void receive_telegram_(); void receive_telegram_();
void receive_encrypted_telegram_(); void receive_encrypted_telegram_();
void reset_telegram_(); void reset_telegram_();
void drain_rx_buffer_();
/// Wait for UART data to become available within the read timeout. /// Wait for UART data to become available within the read timeout.
/// ///

View File

@@ -135,6 +135,7 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = (
"esp_driver_dac", # DAC driver - only needed by esp32_dac component "esp_driver_dac", # DAC driver - only needed by esp32_dac component
"esp_driver_i2s", # I2S driver - only needed by i2s_audio component "esp_driver_i2s", # I2S driver - only needed by i2s_audio component
"esp_driver_mcpwm", # MCPWM driver - ESPHome doesn't use motor control PWM "esp_driver_mcpwm", # MCPWM driver - ESPHome doesn't use motor control PWM
"esp_driver_pcnt", # PCNT driver - only needed by pulse_counter, hlw8012 components
"esp_driver_rmt", # RMT driver - only needed by remote_transmitter/receiver, neopixelbus "esp_driver_rmt", # RMT driver - only needed by remote_transmitter/receiver, neopixelbus
"esp_driver_touch_sens", # Touch sensor driver - only needed by esp32_touch "esp_driver_touch_sens", # Touch sensor driver - only needed by esp32_touch
"esp_driver_twai", # TWAI/CAN driver - only needed by esp32_can component "esp_driver_twai", # TWAI/CAN driver - only needed by esp32_can component
@@ -1435,6 +1436,10 @@ async def to_code(config):
CORE.relative_internal_path(".espressif") 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: if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
cg.add_build_flag("-DUSE_ESP_IDF") cg.add_build_flag("-DUSE_ESP_IDF")
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF") cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF")

View File

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

View File

@@ -95,9 +95,9 @@ async def to_code(config):
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
os.environ["ESP_IDF_VERSION"] = f"{framework_ver.major}.{framework_ver.minor}" os.environ["ESP_IDF_VERSION"] = f"{framework_ver.major}.{framework_ver.minor}"
if framework_ver >= cv.Version(5, 5, 0): if framework_ver >= cv.Version(5, 5, 0):
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.2.4") esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.3.2")
esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.4") esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.4")
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.9.3") esp32.add_idf_component(name="espressif/esp_hosted", ref="2.11.5")
else: else:
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0") esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0")
esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0") esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0")

View File

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

View File

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

View File

@@ -7,22 +7,25 @@
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <esp_attr.h> #include <esp_attr.h>
#include <esp_clk_tree.h>
namespace esphome { namespace esphome {
namespace esp32_rmt_led_strip { namespace esp32_rmt_led_strip {
static const char *const TAG = "esp32_rmt_led_strip"; static const char *const TAG = "esp32_rmt_led_strip";
#ifdef USE_ESP32_VARIANT_ESP32H2
static const uint32_t RMT_CLK_FREQ = 32000000;
static const uint8_t RMT_CLK_DIV = 1;
#else
static const uint32_t RMT_CLK_FREQ = 80000000;
static const uint8_t RMT_CLK_DIV = 2;
#endif
static const size_t RMT_SYMBOLS_PER_BYTE = 8; static const size_t RMT_SYMBOLS_PER_BYTE = 8;
// Query the RMT default clock source frequency. This varies by variant:
// APB (80MHz) on ESP32/S2/S3/C3, PLL_F80M (80MHz) on C6/P4, XTAL (32MHz) on H2.
// Worst-case reset time is WS2811 at 300µs = 24000 ticks at 80MHz, well within
// the 15-bit rmt_symbol_word_t duration field max of 32767.
static uint32_t rmt_resolution_hz() {
uint32_t freq;
esp_clk_tree_src_get_freq_hz((soc_module_clk_t) RMT_CLK_SRC_DEFAULT, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &freq);
return freq;
}
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size_t symbols_written, size_t symbols_free, static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size_t symbols_written, size_t symbols_free,
rmt_symbol_word_t *symbols, bool *done, void *arg) { rmt_symbol_word_t *symbols, bool *done, void *arg) {
@@ -92,7 +95,7 @@ void ESP32RMTLEDStripLightOutput::setup() {
rmt_tx_channel_config_t channel; rmt_tx_channel_config_t channel;
memset(&channel, 0, sizeof(channel)); memset(&channel, 0, sizeof(channel));
channel.clk_src = RMT_CLK_SRC_DEFAULT; channel.clk_src = RMT_CLK_SRC_DEFAULT;
channel.resolution_hz = RMT_CLK_FREQ / RMT_CLK_DIV; channel.resolution_hz = rmt_resolution_hz();
channel.gpio_num = gpio_num_t(this->pin_); channel.gpio_num = gpio_num_t(this->pin_);
channel.mem_block_symbols = this->rmt_symbols_; channel.mem_block_symbols = this->rmt_symbols_;
channel.trans_queue_depth = 1; channel.trans_queue_depth = 1;
@@ -137,7 +140,7 @@ void ESP32RMTLEDStripLightOutput::setup() {
void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high, void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high,
uint32_t bit1_low, uint32_t reset_time_high, uint32_t reset_time_low) { uint32_t bit1_low, uint32_t reset_time_high, uint32_t reset_time_low) {
float ratio = (float) RMT_CLK_FREQ / RMT_CLK_DIV / 1e09f; float ratio = (float) rmt_resolution_hz() / 1e09f;
// 0-bit // 0-bit
this->params_.bit0.duration0 = (uint32_t) (ratio * bit0_high); this->params_.bit0.duration0 = (uint32_t) (ratio * bit0_high);

View File

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

View File

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

View File

@@ -94,10 +94,7 @@ CONFIG_SCHEMA = cv.Schema(
async def to_code(config): async def to_code(config):
if CORE.is_esp32: if CORE.is_esp32:
# Re-enable ESP-IDF's legacy driver component (excluded by default to save compile time) include_builtin_idf_component("esp_driver_pcnt")
# HLW8012 uses pulse_counter's PCNT storage which requires driver/pcnt.h
# TODO: Remove this once pulse_counter migrates to new PCNT API (driver/pulse_cnt.h)
include_builtin_idf_component("driver")
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)

View File

@@ -103,6 +103,42 @@ inline bool is_success(int const status) { return status >= HTTP_STATUS_OK && st
* - ESP-IDF: blocking reads, 0 only returned when all content read * - ESP-IDF: blocking reads, 0 only returned when all content read
* - Arduino: non-blocking, 0 means "no data yet" or "all content read" * - Arduino: non-blocking, 0 means "no data yet" or "all content read"
* *
* Chunked responses that complete in a reasonable time work correctly on both
* platforms. The limitation below applies only to *streaming* chunked
* responses where data arrives slowly over a long period.
*
* Streaming chunked responses are NOT supported (all platforms):
* The read helpers (http_read_loop_result, http_read_fully) block the main
* event loop until all response data is received. For streaming responses
* where data trickles in slowly (e.g., TTS streaming via ffmpeg proxy),
* this starves the event loop on both ESP-IDF and Arduino. If data arrives
* just often enough to avoid the caller's timeout, the loop runs
* indefinitely. If data stops entirely, ESP-IDF fails with
* -ESP_ERR_HTTP_EAGAIN (transport timeout) while Arduino spins with
* delay(1) until the caller's timeout fires. Supporting streaming requires
* a non-blocking incremental read pattern that yields back to the event
* loop between chunks. Components that need streaming should use
* esp_http_client directly on a separate FreeRTOS task with
* esp_http_client_is_complete_data_received() for completion detection
* (see audio_reader.cpp for an example).
*
* Chunked transfer encoding - platform differences:
* - ESP-IDF HttpContainer:
* HttpContainerIDF overrides is_read_complete() to call
* esp_http_client_is_complete_data_received(), which is the
* authoritative completion check for both chunked and non-chunked
* transfers. When esp_http_client_read() returns 0 for a completed
* chunked response, read() returns 0 and is_read_complete() returns
* true, so callers get COMPLETE from http_read_loop_result().
*
* - Arduino HttpContainer:
* Chunked responses are decoded internally (see
* HttpContainerArduino::read_chunked_()). When the final chunk arrives,
* is_chunked_ is cleared and content_length is set to bytes_read_.
* Completion is then detected via is_read_complete(), and a subsequent
* read() returns 0 to indicate "all content read" (not
* HTTP_ERROR_CONNECTION_CLOSED).
*
* Use the helper functions below instead of checking return values directly: * Use the helper functions below instead of checking return values directly:
* - http_read_loop_result(): for manual loops with per-chunk processing * - http_read_loop_result(): for manual loops with per-chunk processing
* - http_read_fully(): for simple "read N bytes into buffer" operations * - http_read_fully(): for simple "read N bytes into buffer" operations
@@ -204,9 +240,13 @@ class HttpContainer : public Parented<HttpRequestComponent> {
size_t get_bytes_read() const { return this->bytes_read_; } size_t get_bytes_read() const { return this->bytes_read_; }
/// Check if all expected content has been read /// Check if all expected content has been read.
/// For chunked responses, returns false (completion detected via read() returning error/EOF) /// Base implementation handles non-chunked responses and status-code-based no-body checks.
bool is_read_complete() const { /// Platform implementations may override for chunked completion detection:
/// - ESP-IDF: overrides to call esp_http_client_is_complete_data_received() for chunked.
/// - Arduino: read_chunked_() clears is_chunked_ and sets content_length on the final
/// chunk, after which the base implementation detects completion.
virtual bool is_read_complete() const {
// Per RFC 9112, these responses have no body: // Per RFC 9112, these responses have no body:
// - 1xx (Informational), 204 No Content, 205 Reset Content, 304 Not Modified // - 1xx (Informational), 204 No Content, 205 Reset Content, 304 Not Modified
if ((this->status_code >= 100 && this->status_code < 200) || this->status_code == HTTP_STATUS_NO_CONTENT || if ((this->status_code >= 100 && this->status_code < 200) || this->status_code == HTTP_STATUS_NO_CONTENT ||

View File

@@ -218,32 +218,50 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
return container; return container;
} }
bool HttpContainerIDF::is_read_complete() const {
// Base class handles no-body status codes and non-chunked content_length completion
if (HttpContainer::is_read_complete()) {
return true;
}
// For chunked responses, use the authoritative ESP-IDF completion check
return this->is_chunked_ && esp_http_client_is_complete_data_received(this->client_);
}
// ESP-IDF HTTP read implementation (blocking mode) // ESP-IDF HTTP read implementation (blocking mode)
// //
// WARNING: Return values differ from BSD sockets! See http_request.h for full documentation. // WARNING: Return values differ from BSD sockets! See http_request.h for full documentation.
// //
// esp_http_client_read() in blocking mode returns: // esp_http_client_read() in blocking mode returns:
// > 0: bytes read // > 0: bytes read
// 0: connection closed (end of stream) // 0: all chunked data received (is_chunk_complete true) or connection closed
// -ESP_ERR_HTTP_EAGAIN: transport timeout, no data available yet
// < 0: error // < 0: error
// //
// We normalize to HttpContainer::read() contract: // We normalize to HttpContainer::read() contract:
// > 0: bytes read // > 0: bytes read
// 0: all content read (only returned when content_length is known and fully read) // 0: all content read (for both content_length-based and chunked completion)
// < 0: error/connection closed // < 0: error/connection closed
// //
// Note on chunked transfer encoding: // Note on chunked transfer encoding:
// esp_http_client_fetch_headers() returns 0 for chunked responses (no Content-Length header). // esp_http_client_fetch_headers() returns 0 for chunked responses (no Content-Length header).
// We handle this by skipping the content_length check when content_length is 0, // When esp_http_client_read() returns 0 for a chunked response, is_read_complete() calls
// allowing esp_http_client_read() to handle chunked decoding internally and signal EOF // esp_http_client_is_complete_data_received() to distinguish successful completion from
// by returning 0. // connection errors. Callers use http_read_loop_result() which checks is_read_complete()
// to return COMPLETE for successful chunked EOF.
//
// Streaming chunked responses are not supported (see http_request.h for details).
// When data stops arriving, esp_http_client_read() returns -ESP_ERR_HTTP_EAGAIN
// after its internal transport timeout (configured via timeout_ms) expires.
// This is passed through as a negative return value, which callers treat as an error.
int HttpContainerIDF::read(uint8_t *buf, size_t max_len) { int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
const uint32_t start = millis(); const uint32_t start = millis();
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout()); watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
// Check if we've already read all expected content (non-chunked only) // Check if we've already read all expected content (non-chunked and no-body only).
// For chunked responses (content_length == 0), esp_http_client_read() handles EOF // Use the base class check here, NOT the override: esp_http_client_is_complete_data_received()
if (this->is_read_complete()) { // returns true as soon as all data arrives from the network, but data may still be in
// the client's internal buffer waiting to be consumed by esp_http_client_read().
if (HttpContainer::is_read_complete()) {
return 0; // All content read successfully return 0; // All content read successfully
} }
@@ -258,15 +276,18 @@ int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
return read_len_or_error; return read_len_or_error;
} }
// esp_http_client_read() returns 0 in two cases: // esp_http_client_read() returns 0 when:
// 1. Known content_length: connection closed before all data received (error) // - Known content_length: connection closed before all data received (error)
// 2. Chunked encoding (content_length == 0): end of stream reached (EOF) // - Chunked encoding: all chunks received (is_chunk_complete true, genuine EOF)
// For case 1, returning HTTP_ERROR_CONNECTION_CLOSED is correct. //
// For case 2, 0 indicates that all chunked data has already been delivered // Return 0 in both cases. Callers use http_read_loop_result() which calls
// in previous successful read() calls, so treating this as a closed // is_read_complete() to distinguish these:
// connection does not cause any loss of response data. // - Chunked complete: is_read_complete() returns true (via
// esp_http_client_is_complete_data_received()), caller gets COMPLETE
// - Non-chunked incomplete: is_read_complete() returns false, caller
// eventually gets TIMEOUT (since no more data arrives)
if (read_len_or_error == 0) { if (read_len_or_error == 0) {
return HTTP_ERROR_CONNECTION_CLOSED; return 0;
} }
// Negative value - error, return the actual error code for debugging // Negative value - error, return the actual error code for debugging

View File

@@ -16,6 +16,7 @@ class HttpContainerIDF : public HttpContainer {
HttpContainerIDF(esp_http_client_handle_t client) : client_(client) {} HttpContainerIDF(esp_http_client_handle_t client) : client_(client) {}
int read(uint8_t *buf, size_t max_len) override; int read(uint8_t *buf, size_t max_len) override;
void end() override; void end() override;
bool is_read_complete() const override;
/// @brief Feeds the watchdog timer if the executing task has one attached /// @brief Feeds the watchdog timer if the executing task has one attached
void feed_wdt(); void feed_wdt();

View File

@@ -134,25 +134,23 @@ ErrorCode ArduinoI2CBus::write_readv(uint8_t address, const uint8_t *write_buffe
for (size_t j = 0; j != read_count; j++) for (size_t j = 0; j != read_count; j++)
read_buffer[j] = wire_->read(); read_buffer[j] = wire_->read();
} }
switch (status) { // Avoid switch to prevent compiler-generated lookup table in RAM on ESP8266
case 0: if (status == 0)
return ERROR_OK; return ERROR_OK;
case 1: if (status == 1) {
// transmit buffer not large enough ESP_LOGVV(TAG, "TX failed: buffer not large enough");
ESP_LOGVV(TAG, "TX failed: buffer not large enough"); return ERROR_UNKNOWN;
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: /// Perform I2C bus recovery, see:

View File

@@ -275,8 +275,19 @@ void LD2410Component::restart_and_read_all_info() {
} }
void LD2410Component::loop() { void LD2410Component::loop() {
while (this->available()) { // Read all available bytes in batches to reduce UART call overhead.
this->readline_(this->read()); 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]);
}
} }
} }

View File

@@ -310,8 +310,19 @@ void LD2412Component::restart_and_read_all_info() {
} }
void LD2412Component::loop() { void LD2412Component::loop() {
while (this->available()) { // Read all available bytes in batches to reduce UART call overhead.
this->readline_(this->read()); 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]);
}
} }
} }

View File

@@ -335,9 +335,10 @@ void LD2420Component::revert_config_action() {
void LD2420Component::loop() { void LD2420Component::loop() {
// If there is a active send command do not process it here, the send command call will handle it. // If there is a active send command do not process it here, the send command call will handle it.
while (!this->cmd_active_ && this->available()) { if (this->cmd_active_) {
this->readline_(this->read(), this->buffer_data_, MAX_LINE_LENGTH); return;
} }
this->read_batch_(this->buffer_data_);
} }
void LD2420Component::update_radar_data(uint16_t const *gate_energy, uint8_t sample_number) { void LD2420Component::update_radar_data(uint16_t const *gate_energy, uint8_t sample_number) {
@@ -539,6 +540,23 @@ 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) { void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) {
this->cmd_reply_.command = buffer[CMD_FRAME_COMMAND]; this->cmd_reply_.command = buffer[CMD_FRAME_COMMAND];
this->cmd_reply_.length = buffer[CMD_FRAME_DATA_LENGTH]; this->cmd_reply_.length = buffer[CMD_FRAME_DATA_LENGTH];

View File

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

View File

@@ -1,7 +1,8 @@
from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import uart from esphome.components import uart
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_THROTTLE from esphome.const import CONF_ID, CONF_ON_DATA, CONF_THROTTLE, CONF_TRIGGER_ID
AUTO_LOAD = ["ld24xx"] AUTO_LOAD = ["ld24xx"]
DEPENDENCIES = ["uart"] DEPENDENCIES = ["uart"]
@@ -11,6 +12,8 @@ MULTI_CONF = True
ld2450_ns = cg.esphome_ns.namespace("ld2450") ld2450_ns = cg.esphome_ns.namespace("ld2450")
LD2450Component = ld2450_ns.class_("LD2450Component", cg.Component, uart.UARTDevice) LD2450Component = ld2450_ns.class_("LD2450Component", cg.Component, uart.UARTDevice)
LD2450DataTrigger = ld2450_ns.class_("LD2450DataTrigger", automation.Trigger.template())
CONF_LD2450_ID = "ld2450_id" CONF_LD2450_ID = "ld2450_id"
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
@@ -20,6 +23,11 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_THROTTLE): cv.invalid( cv.Optional(CONF_THROTTLE): cv.invalid(
f"{CONF_THROTTLE} has been removed; use per-sensor filters, instead" f"{CONF_THROTTLE} has been removed; use per-sensor filters, instead"
), ),
cv.Optional(CONF_ON_DATA): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LD2450DataTrigger),
}
),
} }
) )
.extend(uart.UART_DEVICE_SCHEMA) .extend(uart.UART_DEVICE_SCHEMA)
@@ -45,3 +53,6 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
await uart.register_uart_device(var, config) await uart.register_uart_device(var, config)
for conf in config.get(CONF_ON_DATA, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)

View File

@@ -276,8 +276,19 @@ void LD2450Component::dump_config() {
} }
void LD2450Component::loop() { void LD2450Component::loop() {
while (this->available()) { // Read all available bytes in batches to reduce UART call overhead.
this->readline_(this->read()); 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]);
}
} }
} }
@@ -402,6 +413,10 @@ void LD2450Component::restart_and_read_all_info() {
this->set_timeout(1500, [this]() { this->read_all_info(); }); this->set_timeout(1500, [this]() { this->read_all_info(); });
} }
void LD2450Component::add_on_data_callback(std::function<void()> &&callback) {
this->data_callback_.add(std::move(callback));
}
// Send command with values to LD2450 // Send command with values to LD2450
void LD2450Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) { void LD2450Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) {
ESP_LOGV(TAG, "Sending COMMAND %02X", command); ESP_LOGV(TAG, "Sending COMMAND %02X", command);
@@ -602,6 +617,8 @@ void LD2450Component::handle_periodic_data_() {
this->still_presence_millis_ = App.get_loop_component_start_time(); this->still_presence_millis_ = App.get_loop_component_start_time();
} }
#endif #endif
this->data_callback_.call();
} }
bool LD2450Component::handle_ack_data_() { bool LD2450Component::handle_ack_data_() {

View File

@@ -141,6 +141,9 @@ class LD2450Component : public Component, public uart::UARTDevice {
int32_t zone2_x1, int32_t zone2_y1, int32_t zone2_x2, int32_t zone2_y2, int32_t zone3_x1, int32_t zone2_x1, int32_t zone2_y1, int32_t zone2_x2, int32_t zone2_y2, int32_t zone3_x1,
int32_t zone3_y1, int32_t zone3_x2, int32_t zone3_y2); int32_t zone3_y1, int32_t zone3_x2, int32_t zone3_y2);
/// Add a callback that will be called after each successfully processed periodic data frame.
void add_on_data_callback(std::function<void()> &&callback);
protected: protected:
void send_command_(uint8_t command_str, const uint8_t *command_value, uint8_t command_value_len); void send_command_(uint8_t command_str, const uint8_t *command_value, uint8_t command_value_len);
void set_config_mode_(bool enable); void set_config_mode_(bool enable);
@@ -190,6 +193,15 @@ class LD2450Component : public Component, public uart::UARTDevice {
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
std::array<text_sensor::TextSensor *, 3> direction_text_sensors_{}; std::array<text_sensor::TextSensor *, 3> direction_text_sensors_{};
#endif #endif
LazyCallbackManager<void()> data_callback_;
};
class LD2450DataTrigger : public Trigger<> {
public:
explicit LD2450DataTrigger(LD2450Component *parent) {
parent->add_on_data_callback([this]() { this->trigger(); });
}
}; };
} // namespace esphome::ld2450 } // namespace esphome::ld2450

View File

@@ -36,8 +36,9 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
#endif #endif
// Fast path: main thread, no recursion (99.9% of all logs) // 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]] { 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); this->log_message_to_buffer_and_send_(this->main_task_recursion_guard_, level, tag, line, format, args, nullptr);
return; return;
} }
@@ -47,21 +48,23 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
} }
// Non-main thread handling (~0.1% of logs) // 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) #if defined(USE_ESP32) || defined(USE_LIBRETINY)
this->log_vprintf_non_main_thread_(level, tag, line, format, args, current_task); const char *thread_name = get_thread_name_(current_task);
#else // USE_HOST #else // USE_HOST
this->log_vprintf_non_main_thread_(level, tag, line, format, args); char thread_name_buf[THREAD_NAME_BUF_SIZE];
const char *thread_name = this->get_thread_name_(thread_name_buf);
#endif #endif
this->log_vprintf_non_main_thread_(level, tag, line, format, args, thread_name);
} }
// Handles non-main thread logging only // Handles non-main thread logging only
// Kept separate from hot path to improve instruction cache performance // 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, void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args,
TaskHandle_t current_task) { const char *thread_name) {
#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 // Check if already in recursion for this non-main thread/task
if (this->is_non_main_task_recursive_()) { if (this->is_non_main_task_recursive_()) {
return; return;
@@ -73,12 +76,8 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li
bool message_sent = false; bool message_sent = false;
#ifdef USE_ESPHOME_TASK_LOG_BUFFER #ifdef USE_ESPHOME_TASK_LOG_BUFFER
// For non-main threads/tasks, queue the message for callbacks // For non-main threads/tasks, queue the message for callbacks
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
message_sent = message_sent =
this->log_buffer_->send_message_thread_safe(level, tag, static_cast<uint16_t>(line), current_task, format, args); this->log_buffer_->send_message_thread_safe(level, tag, static_cast<uint16_t>(line), thread_name, 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) { if (message_sent) {
// Enable logger loop to process the buffered message // Enable logger loop to process the buffered message
// This is safe to call from any context including ISRs // This is safe to call from any context including ISRs
@@ -101,19 +100,27 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li
#endif #endif
char console_buffer[MAX_CONSOLE_LOG_MSG_SIZE]; // MUST be stack allocated for thread safety char console_buffer[MAX_CONSOLE_LOG_MSG_SIZE]; // MUST be stack allocated for thread safety
LogBuffer buf{console_buffer, MAX_CONSOLE_LOG_MSG_SIZE}; LogBuffer buf{console_buffer, MAX_CONSOLE_LOG_MSG_SIZE};
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf); this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf, thread_name);
this->write_to_console_(buf); this->write_to_console_(buf);
} }
// RAII guard automatically resets on return // RAII guard automatically resets on return
} }
#else #else
// Implementation for all other platforms (single-task, no threading) // 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).
void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const char *format, va_list args) { // NOLINT 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_) if (level > this->level_for(tag) || global_recursion_guard_)
return; return;
#ifdef USE_ZEPHYR
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args); 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
} }
#endif // USE_ESP32 / USE_HOST / USE_LIBRETINY #endif // USE_ESP32 / USE_HOST / USE_LIBRETINY
@@ -129,7 +136,7 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas
if (level > this->level_for(tag) || global_recursion_guard_) if (level > this->level_for(tag) || global_recursion_guard_)
return; return;
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args); this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args, nullptr);
} }
#endif // USE_STORE_LOG_STR_IN_FLASH #endif // USE_STORE_LOG_STR_IN_FLASH

View File

@@ -2,6 +2,7 @@
#include <cstdarg> #include <cstdarg>
#include <map> #include <map>
#include <span>
#include <type_traits> #include <type_traits>
#if defined(USE_ESP32) || defined(USE_HOST) #if defined(USE_ESP32) || defined(USE_HOST)
#include <pthread.h> #include <pthread.h>
@@ -124,6 +125,10 @@ static constexpr uint16_t MAX_HEADER_SIZE = 128;
// "0x" + 2 hex digits per byte + '\0' // "0x" + 2 hex digits per byte + '\0'
static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1; 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 // Buffer wrapper for log formatting functions
struct LogBuffer { struct LogBuffer {
char *data; char *data;
@@ -408,34 +413,24 @@ class Logger : public Component {
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) #if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY)
// Handles non-main thread logging only (~0.1% of calls) // Handles non-main thread logging only (~0.1% of calls)
#if defined(USE_ESP32) || defined(USE_LIBRETINY) // thread_name is resolved by the caller from the task handle, avoiding redundant lookups
// 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, void log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args,
TaskHandle_t current_task); const char *thread_name);
#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 #endif
void process_messages_(); void process_messages_();
void write_msg_(const char *msg, uint16_t len); 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 // 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, 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) { va_list args, LogBuffer &buf, const char *thread_name) {
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_HOST) buf.write_header(level, tag, line, thread_name);
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); buf.format_body(format, args);
} }
#ifdef USE_STORE_LOG_STR_IN_FLASH #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 // 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, inline void HOT format_log_to_buffer_with_terminator_P_(uint8_t level, const char *tag, int line,
const __FlashStringHelper *format, va_list args, const __FlashStringHelper *format, va_list args,
LogBuffer &buf) { LogBuffer &buf) {
@@ -466,9 +461,10 @@ class Logger : public Component {
// Helper to format and send a log message to both console and listeners // Helper to format and send a log message to both console and listeners
// Template handles both const char* (RAM) and __FlashStringHelper* (flash) format strings // 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> template<typename FormatType>
inline void HOT log_message_to_buffer_and_send_(bool &recursion_guard, uint8_t level, const char *tag, int line, 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) { FormatType format, va_list args, const char *thread_name) {
RecursionGuard guard(recursion_guard); RecursionGuard guard(recursion_guard);
LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_}; LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_};
#ifdef USE_STORE_LOG_STR_IN_FLASH #ifdef USE_STORE_LOG_STR_IN_FLASH
@@ -477,7 +473,7 @@ class Logger : public Component {
} else } else
#endif #endif
{ {
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf); this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf, thread_name);
} }
this->notify_listeners_(level, tag, buf); this->notify_listeners_(level, tag, buf);
this->write_log_buffer_to_console_(buf); this->write_log_buffer_to_console_(buf);
@@ -565,37 +561,57 @@ class Logger : public Component {
bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms
#endif #endif
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) // --- get_thread_name_ overloads (per-platform) ---
const char *HOT get_thread_name_(
#ifdef USE_ZEPHYR #if defined(USE_ESP32) || defined(USE_LIBRETINY)
char *buff // 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);
#endif #endif
) { }
#ifdef USE_ZEPHYR
// 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) {
k_tid_t current_task = k_current_get(); k_tid_t current_task = k_current_get();
#else
TaskHandle_t current_task = xTaskGetCurrentTaskHandle();
#endif
if (current_task == main_task_) { if (current_task == main_task_) {
return nullptr; // 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 #endif
// --- Non-main task recursion guards (per-platform) ---
#if defined(USE_ESP32) || defined(USE_HOST) #if defined(USE_ESP32) || defined(USE_HOST)
// RAII guard for non-main task recursion using pthread TLS // RAII guard for non-main task recursion using pthread TLS
class NonMainTaskRecursionGuard { class NonMainTaskRecursionGuard {
@@ -635,22 +651,6 @@ class Logger : public Component {
inline RecursionGuard make_non_main_task_guard_() { return RecursionGuard(non_main_task_recursion_guard_); } inline RecursionGuard make_non_main_task_guard_() { return RecursionGuard(non_main_task_recursion_guard_); }
#endif #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) #if defined(USE_ESP32) || defined(USE_LIBRETINY)
// Disable loop when task buffer is empty (with USB CDC check on ESP32) // Disable loop when task buffer is empty (with USB CDC check on ESP32)
inline void disable_loop_when_buffer_empty_() { inline void disable_loop_when_buffer_empty_() {

View File

@@ -59,7 +59,7 @@ void TaskLogBuffer::release_message_main_loop(void *token) {
last_processed_counter_ = message_counter_.load(std::memory_order_relaxed); 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, TaskHandle_t task_handle, bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
const char *format, va_list args) { const char *format, va_list args) {
// First, calculate the exact length needed using a null buffer (no actual writing) // First, calculate the exact length needed using a null buffer (no actual writing)
va_list args_copy; va_list args_copy;
@@ -95,7 +95,6 @@ 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 // 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 // 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 // is enqueued and when it's processed by the main loop
const char *thread_name = pcTaskGetName(task_handle);
if (thread_name != nullptr) { if (thread_name != nullptr) {
strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1); strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1);
msg->thread_name[sizeof(msg->thread_name) - 1] = '\0'; // Ensure null termination 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); void release_message_main_loop(void *token);
// Thread-safe - send a message to the ring buffer from any thread // 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, TaskHandle_t task_handle, bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
const char *format, va_list args); const char *format, va_list args);
// Check if there are messages ready to be processed using an atomic counter for performance // 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 *format, bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
va_list args) { const char *format, va_list args) {
// Acquire a slot // Acquire a slot
int slot_index = this->acquire_write_slot_(); int slot_index = this->acquire_write_slot_();
if (slot_index < 0) { if (slot_index < 0) {
@@ -85,11 +85,9 @@ bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag,
msg.tag = tag; msg.tag = tag;
msg.line = line; msg.line = line;
// Get thread name using pthread // Store the thread name now to avoid crashes if thread exits before processing
char thread_name_buf[LogMessage::MAX_THREAD_NAME_SIZE]; if (thread_name != nullptr) {
// pthread_getname_np works the same on Linux and macOS strncpy(msg.thread_name, thread_name, sizeof(msg.thread_name) - 1);
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'; msg.thread_name[sizeof(msg.thread_name) - 1] = '\0';
} else { } else {
msg.thread_name[0] = '\0'; msg.thread_name[0] = '\0';

View File

@@ -86,7 +86,8 @@ class TaskLogBufferHost {
// Thread-safe - send a message to the buffer from any thread // Thread-safe - send a message to the buffer from any thread
// Returns true if message was queued, false if buffer is full // 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 *format, va_list args); bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
const char *format, va_list args);
// Check if there are messages ready to be processed // Check if there are messages ready to be processed
inline bool HOT has_messages() const { 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, bool TaskLogBufferLibreTiny::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line,
TaskHandle_t task_handle, const char *format, va_list args) { const char *thread_name, const char *format, va_list args) {
// First, calculate the exact length needed using a null buffer (no actual writing) // First, calculate the exact length needed using a null buffer (no actual writing)
va_list args_copy; va_list args_copy;
va_copy(args_copy, args); va_copy(args_copy, args);
@@ -162,7 +162,6 @@ bool TaskLogBufferLibreTiny::send_message_thread_safe(uint8_t level, const char
msg->line = line; msg->line = line;
// Store the thread name now to avoid crashes if task is deleted before processing // 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) { if (thread_name != nullptr) {
strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1); strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1);
msg->thread_name[sizeof(msg->thread_name) - 1] = '\0'; msg->thread_name[sizeof(msg->thread_name) - 1] = '\0';

View File

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

View File

@@ -120,3 +120,101 @@ DriverChip(
(0xB2, 0x10), (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, CONF_DRAW_ROUNDING,
) )
from esphome.components.display import CONF_SHOW_TEST_CARD from esphome.components.display import CONF_SHOW_TEST_CARD
from esphome.components.esp32 import VARIANT_ESP32S3, only_on_variant from esphome.components.esp32 import VARIANT_ESP32P4, VARIANT_ESP32S3, only_on_variant
from esphome.components.mipi import ( from esphome.components.mipi import (
COLOR_ORDERS, COLOR_ORDERS,
CONF_DE_PIN, CONF_DE_PIN,
@@ -225,7 +225,7 @@ def _config_schema(config):
return cv.All( return cv.All(
schema, schema,
cv.only_on_esp32, cv.only_on_esp32,
only_on_variant(supported=[VARIANT_ESP32S3]), only_on_variant(supported=[VARIANT_ESP32S3, VARIANT_ESP32P4]),
)(config) )(config)

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
from esphome import automation from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import audio, esp32, speaker from esphome.components import audio, esp32, socket, speaker
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_BITS_PER_SAMPLE, CONF_BITS_PER_SAMPLE,
@@ -61,7 +61,7 @@ def _set_stream_limits(config):
def _validate_source_speaker(config): def _validate_source_speaker(config):
fconf = fv.full_config.get() fconf = fv.full_config.get()
# Get ID for the output speaker and add it to the source speakrs config to easily inherit properties # Get ID for the output speaker and add it to the source speakers config to easily inherit properties
path = fconf.get_path_for_id(config[CONF_ID])[:-3] path = fconf.get_path_for_id(config[CONF_ID])[:-3]
path.append(CONF_OUTPUT_SPEAKER) path.append(CONF_OUTPUT_SPEAKER)
output_speaker_id = fconf.get_config_for_path(path) output_speaker_id = fconf.get_config_for_path(path)
@@ -111,6 +111,9 @@ FINAL_VALIDATE_SCHEMA = cv.All(
async def to_code(config): 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]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
@@ -127,6 +130,9 @@ async def to_code(config):
"CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True "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]: for speaker_config in config[CONF_SOURCE_SPEAKERS]:
source_speaker = cg.new_Pvariable(speaker_config[CONF_ID]) source_speaker = cg.new_Pvariable(speaker_config[CONF_ID])

View File

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

View File

@@ -2,11 +2,13 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
#include "esphome/core/application.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <algorithm> #include <algorithm>
#include <array>
#include <cstring> #include <cstring>
namespace esphome { namespace esphome {
@@ -14,6 +16,7 @@ namespace mixer_speaker {
static const UBaseType_t MIXER_TASK_PRIORITY = 10; 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 TRANSFER_BUFFER_DURATION_MS = 50;
static const uint32_t TASK_DELAY_MS = 25; static const uint32_t TASK_DELAY_MS = 25;
@@ -27,21 +30,53 @@ static const char *const TAG = "speaker_mixer";
// Gives the Q15 fixed point scaling factor to reduce by 0 dB, 1dB, ..., 50 dB // 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) // 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) // float to Q15 fixed point formula: q15_scale_factor = floating_point_scale_factor * 2^(15)
static const std::vector<int16_t> DECIBEL_REDUCTION_TABLE = { static const std::array<int16_t, 51> DECIBEL_REDUCTION_TABLE = {
32767, 29201, 26022, 23189, 20665, 18415, 16410, 14624, 13032, 11613, 10349, 9222, 8218, 7324, 6527, 5816, 5183, 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, 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}; 651, 580, 517, 461, 411, 366, 326, 291, 259, 231, 206, 183, 163, 146, 130, 116, 103};
enum MixerEventGroupBits : uint32_t { // Event bits for SourceSpeaker command processing
COMMAND_STOP = (1 << 0), // stops the mixer task enum SourceSpeakerEventBits : uint32_t {
STATE_STARTING = (1 << 10), SOURCE_SPEAKER_COMMAND_START = (1 << 0),
STATE_RUNNING = (1 << 11), SOURCE_SPEAKER_COMMAND_STOP = (1 << 1),
STATE_STOPPING = (1 << 12), SOURCE_SPEAKER_COMMAND_FINISH = (1 << 2),
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() { void SourceSpeaker::dump_config() {
ESP_LOGCONFIG(TAG, ESP_LOGCONFIG(TAG,
"Mixer Source Speaker\n" "Mixer Source Speaker\n"
@@ -55,22 +90,70 @@ void SourceSpeaker::dump_config() {
} }
void SourceSpeaker::setup() { void SourceSpeaker::setup() {
this->parent_->get_output_speaker()->add_audio_output_callback([this](uint32_t new_frames, int64_t write_timestamp) { if (!create_event_group(this->event_group_, this)) {
// The SourceSpeaker may not have included any audio in the mixed output, so verify there were pending frames return;
uint32_t speakers_playback_frames = std::min(new_frames, this->pending_playback_frames_); }
this->pending_playback_frames_ -= speakers_playback_frames;
if (speakers_playback_frames > 0) { // Start with loop disabled since we begin in STATE_STOPPED with no pending commands
this->audio_output_callback_(speakers_playback_frames, write_timestamp); 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;
// 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);
}
} }
}); });
} }
void SourceSpeaker::loop() { 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_) { switch (this->state_) {
case speaker::STATE_STARTING: { case speaker::STATE_STARTING: {
esp_err_t err = this->start_(); esp_err_t err = this->start_();
if (err == ESP_OK) { 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->state_ = speaker::STATE_RUNNING;
this->stop_gracefully_ = false; this->stop_gracefully_ = false;
this->last_seen_data_ms_ = millis(); this->last_seen_data_ms_ = millis();
@@ -78,41 +161,62 @@ void SourceSpeaker::loop() {
} else { } else {
switch (err) { switch (err) {
case ESP_ERR_NO_MEM: case ESP_ERR_NO_MEM:
this->status_set_error(LOG_STR("Failed to start mixer: not enough memory")); this->status_set_error(LOG_STR("Not enough memory"));
break; break;
case ESP_ERR_NOT_SUPPORTED: case ESP_ERR_NOT_SUPPORTED:
this->status_set_error(LOG_STR("Failed to start mixer: unsupported bits per sample")); this->status_set_error(LOG_STR("Unsupported bit depth"));
break; break;
case ESP_ERR_INVALID_ARG: case ESP_ERR_INVALID_ARG:
this->status_set_error( this->status_set_error(LOG_STR("Incompatible audio streams"));
LOG_STR("Failed to start mixer: audio stream isn't compatible with the other audio stream."));
break; break;
case ESP_ERR_INVALID_STATE: case ESP_ERR_INVALID_STATE:
this->status_set_error(LOG_STR("Failed to start mixer: mixer task failed to start")); this->status_set_error(LOG_STR("Task failed"));
break; break;
default: default:
this->status_set_error(LOG_STR("Failed to start mixer")); this->status_set_error(LOG_STR("Failed"));
break; break;
} }
this->state_ = speaker::STATE_STOPPING; this->enter_stopping_state_();
} }
break; break;
} }
case speaker::STATE_RUNNING: case speaker::STATE_RUNNING:
if (!this->transfer_buffer_->has_buffered_data()) { 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->timeout_ms_.has_value() && ((millis() - this->last_seen_data_ms_) > this->timeout_ms_.value())) || if ((this->timeout_ms_.has_value() && ((millis() - this->last_seen_data_ms_) > this->timeout_ms_.value())) ||
this->stop_gracefully_) { this->stop_gracefully_) {
this->state_ = speaker::STATE_STOPPING; // Timeout exceeded or graceful stop requested
this->enter_stopping_state_();
} }
} }
break; break;
case speaker::STATE_STOPPING: case speaker::STATE_STOPPING: {
this->stop_(); if ((this->parent_->get_output_speaker()->get_pause_state()) ||
this->stop_gracefully_ = false; ((millis() - this->stopping_start_ms_) > STOPPING_TIMEOUT_MS)) {
this->state_ = speaker::STATE_STOPPED; // 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;
}
break; break;
}
case speaker::STATE_STOPPED: 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; break;
} }
} }
@@ -122,17 +226,34 @@ size_t SourceSpeaker::play(const uint8_t *data, size_t length, TickType_t ticks_
this->start(); this->start();
} }
size_t bytes_written = 0; size_t bytes_written = 0;
if (this->ring_buffer_.use_count() == 1) { std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
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
bytes_written = temp_ring_buffer->write_without_replacement(data, length, ticks_to_wait); bytes_written = temp_ring_buffer->write_without_replacement(data, length, ticks_to_wait);
if (bytes_written > 0) { if (bytes_written > 0) {
this->last_seen_data_ms_ = millis(); 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; return bytes_written;
} }
void SourceSpeaker::start() { this->state_ = speaker::STATE_STARTING; } 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); }
esp_err_t SourceSpeaker::start_() { esp_err_t SourceSpeaker::start_() {
const size_t ring_buffer_size = this->audio_stream_info_.ms_to_bytes(this->buffer_duration_ms_); const size_t ring_buffer_size = this->audio_stream_info_.ms_to_bytes(this->buffer_duration_ms_);
@@ -143,35 +264,26 @@ esp_err_t SourceSpeaker::start_() {
if (this->transfer_buffer_ == nullptr) { if (this->transfer_buffer_ == nullptr) {
return ESP_ERR_NO_MEM; return ESP_ERR_NO_MEM;
} }
std::shared_ptr<RingBuffer> temp_ring_buffer;
if (!this->ring_buffer_.use_count()) { std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
if (!temp_ring_buffer) {
temp_ring_buffer = RingBuffer::create(ring_buffer_size); temp_ring_buffer = RingBuffer::create(ring_buffer_size);
this->ring_buffer_ = temp_ring_buffer; this->ring_buffer_ = temp_ring_buffer;
} }
if (!this->ring_buffer_.use_count()) { if (!temp_ring_buffer) {
return ESP_ERR_NO_MEM; return ESP_ERR_NO_MEM;
} else { } else {
this->transfer_buffer_->set_source(temp_ring_buffer); this->transfer_buffer_->set_source(temp_ring_buffer);
} }
} }
this->pending_playback_frames_ = 0; // reset
return this->parent_->start(this->audio_stream_info_); return this->parent_->start(this->audio_stream_info_);
} }
void SourceSpeaker::stop() { void SourceSpeaker::stop() { this->send_command_(SOURCE_SPEAKER_COMMAND_STOP); }
if (this->state_ != speaker::STATE_STOPPED) {
this->state_ = speaker::STATE_STOPPING;
}
}
void SourceSpeaker::stop_() { void SourceSpeaker::finish() { this->send_command_(SOURCE_SPEAKER_COMMAND_FINISH); }
this->transfer_buffer_.reset(); // deallocates the transfer buffer
}
void SourceSpeaker::finish() { this->stop_gracefully_ = true; }
bool SourceSpeaker::has_buffered_data() const { bool SourceSpeaker::has_buffered_data() const {
return ((this->transfer_buffer_.use_count() > 0) && this->transfer_buffer_->has_buffered_data()); return ((this->transfer_buffer_.use_count() > 0) && this->transfer_buffer_->has_buffered_data());
@@ -191,19 +303,16 @@ void SourceSpeaker::set_volume(float volume) {
float SourceSpeaker::get_volume() { return this->parent_->get_output_speaker()->get_volume(); } float SourceSpeaker::get_volume() { return this->parent_->get_output_speaker()->get_volume(); }
size_t SourceSpeaker::process_data_from_source(TickType_t ticks_to_wait) { size_t SourceSpeaker::process_data_from_source(std::shared_ptr<audio::AudioSourceTransferBuffer> &transfer_buffer,
if (!this->transfer_buffer_.use_count()) { TickType_t ticks_to_wait) {
return 0;
}
// Store current offset, as these samples are already ducked // Store current offset, as these samples are already ducked
const size_t current_length = this->transfer_buffer_->available(); const size_t current_length = transfer_buffer->available();
size_t bytes_read = this->transfer_buffer_->transfer_data_from_source(ticks_to_wait); size_t bytes_read = transfer_buffer->transfer_data_from_source(ticks_to_wait);
uint32_t samples_to_duck = this->audio_stream_info_.bytes_to_samples(bytes_read); uint32_t samples_to_duck = this->audio_stream_info_.bytes_to_samples(bytes_read);
if (samples_to_duck > 0) { if (samples_to_duck > 0) {
int16_t *current_buffer = reinterpret_cast<int16_t *>(this->transfer_buffer_->get_buffer_start() + current_length); int16_t *current_buffer = reinterpret_cast<int16_t *>(transfer_buffer->get_buffer_start() + current_length);
duck_samples(current_buffer, samples_to_duck, &this->current_ducking_db_reduction_, duck_samples(current_buffer, samples_to_duck, &this->current_ducking_db_reduction_,
&this->ducking_transition_samples_remaining_, this->samples_per_ducking_step_, &this->ducking_transition_samples_remaining_, this->samples_per_ducking_step_,
@@ -215,10 +324,13 @@ size_t SourceSpeaker::process_data_from_source(TickType_t ticks_to_wait) {
void SourceSpeaker::apply_ducking(uint8_t decibel_reduction, uint32_t duration) { void SourceSpeaker::apply_ducking(uint8_t decibel_reduction, uint32_t duration) {
if (this->target_ducking_db_reduction_ != decibel_reduction) { 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->current_ducking_db_reduction_ = this->target_ducking_db_reduction_;
this->target_ducking_db_reduction_ = decibel_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; uint8_t total_ducking_steps = 0;
if (this->target_ducking_db_reduction_ > this->current_ducking_db_reduction_) { if (this->target_ducking_db_reduction_ > this->current_ducking_db_reduction_) {
// The dB reduction level is increasing (which results in quieter audio) // The dB reduction level is increasing (which results in quieter audio)
@@ -234,7 +346,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->samples_per_ducking_step_ = this->ducking_transition_samples_remaining_ / total_ducking_steps;
this->ducking_transition_samples_remaining_ = 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_; this->current_ducking_db_reduction_ += this->db_change_per_ducking_step_;
} else { } else {
@@ -293,6 +405,12 @@ 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() { void MixerSpeaker::dump_config() {
ESP_LOGCONFIG(TAG, ESP_LOGCONFIG(TAG,
"Speaker Mixer:\n" "Speaker Mixer:\n"
@@ -301,42 +419,74 @@ void MixerSpeaker::dump_config() {
} }
void MixerSpeaker::setup() { void MixerSpeaker::setup() {
this->event_group_ = xEventGroupCreate(); if (!create_event_group(this->event_group_, this)) {
if (this->event_group_ == nullptr) {
ESP_LOGE(TAG, "Failed to create event group");
this->mark_failed();
return; 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() { void MixerSpeaker::loop() {
uint32_t event_group_bits = xEventGroupGetBits(this->event_group_); uint32_t event_group_bits = xEventGroupGetBits(this->event_group_);
if (event_group_bits & MixerEventGroupBits::STATE_STARTING) { // Handle pending start request
ESP_LOGD(TAG, "Starting speaker mixer"); if (event_group_bits & MIXER_TASK_COMMAND_START) {
xEventGroupClearBits(this->event_group_, MixerEventGroupBits::STATE_STARTING); // 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::ERR_ESP_NO_MEM) {
this->status_set_error(LOG_STR("Failed to allocate the mixer's internal buffer")); if (event_group_bits & MIXER_TASK_STATE_STARTING) {
xEventGroupClearBits(this->event_group_, MixerEventGroupBits::ERR_ESP_NO_MEM); ESP_LOGD(TAG, "Starting");
xEventGroupClearBits(this->event_group_, MIXER_TASK_STATE_STARTING);
} }
if (event_group_bits & MixerEventGroupBits::STATE_RUNNING) { if (event_group_bits & MIXER_TASK_ERR_ESP_NO_MEM) {
ESP_LOGD(TAG, "Started speaker mixer"); 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");
this->status_clear_error(); this->status_clear_error();
xEventGroupClearBits(this->event_group_, MixerEventGroupBits::STATE_RUNNING); xEventGroupClearBits(this->event_group_, MIXER_TASK_STATE_RUNNING);
} }
if (event_group_bits & MixerEventGroupBits::STATE_STOPPING) { if (event_group_bits & MIXER_TASK_STATE_STOPPING) {
ESP_LOGD(TAG, "Stopping speaker mixer"); ESP_LOGV(TAG, "Stopping");
xEventGroupClearBits(this->event_group_, MixerEventGroupBits::STATE_STOPPING); xEventGroupClearBits(this->event_group_, MIXER_TASK_STATE_STOPPING);
} }
if (event_group_bits & MixerEventGroupBits::STATE_STOPPED) { if (event_group_bits & MIXER_TASK_STATE_STOPPED) {
if (this->delete_task_() == ESP_OK) { if (this->delete_task_() == ESP_OK) {
xEventGroupClearBits(this->event_group_, MixerEventGroupBits::ALL_BITS); ESP_LOGD(TAG, "Stopped");
xEventGroupClearBits(this->event_group_, MIXER_TASK_ALL_BITS);
} }
} }
if (this->task_handle_ != nullptr) { if (this->task_handle_ != nullptr) {
// If the mixer task is running, check if all source speakers are stopped
bool all_stopped = true; bool all_stopped = true;
for (auto &speaker : this->source_speakers_) { for (auto &speaker : this->source_speakers_) {
@@ -344,7 +494,15 @@ void MixerSpeaker::loop() {
} }
if (all_stopped) { if (all_stopped) {
this->stop(); // 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();
} }
} }
} }
@@ -366,7 +524,18 @@ esp_err_t MixerSpeaker::start(audio::AudioStreamInfo &stream_info) {
} }
} }
return this->start_task_(); 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;
} }
esp_err_t MixerSpeaker::start_task_() { esp_err_t MixerSpeaker::start_task_() {
@@ -397,28 +566,31 @@ esp_err_t MixerSpeaker::start_task_() {
} }
esp_err_t MixerSpeaker::delete_task_() { esp_err_t MixerSpeaker::delete_task_() {
if (!this->task_created_) { if (this->task_handle_ != nullptr) {
// Delete the task
vTaskDelete(this->task_handle_);
this->task_handle_ = nullptr; this->task_handle_ = nullptr;
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;
}
return ESP_OK;
} }
return ESP_ERR_INVALID_STATE; 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);
}
void MixerSpeaker::stop() { xEventGroupSetBits(this->event_group_, MixerEventGroupBits::COMMAND_STOP); } this->task_stack_buffer_ = nullptr;
}
if ((this->task_handle_ != nullptr) || (this->task_stack_buffer_ != nullptr)) {
return ESP_ERR_INVALID_STATE;
}
return ESP_OK;
}
void MixerSpeaker::copy_frames(const int16_t *input_buffer, audio::AudioStreamInfo input_stream_info, void MixerSpeaker::copy_frames(const int16_t *input_buffer, audio::AudioStreamInfo input_stream_info,
int16_t *output_buffer, audio::AudioStreamInfo output_stream_info, int16_t *output_buffer, audio::AudioStreamInfo output_stream_info,
@@ -472,32 +644,34 @@ void MixerSpeaker::mix_audio_samples(const int16_t *primary_buffer, audio::Audio
} }
void MixerSpeaker::audio_mixer_task(void *params) { void MixerSpeaker::audio_mixer_task(void *params) {
MixerSpeaker *this_mixer = (MixerSpeaker *) params; MixerSpeaker *this_mixer = static_cast<MixerSpeaker *>(params);
xEventGroupSetBits(this_mixer->event_group_, MixerEventGroupBits::STATE_STARTING); xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STARTING);
this_mixer->task_created_ = true;
std::unique_ptr<audio::AudioSinkTransferBuffer> output_transfer_buffer = audio::AudioSinkTransferBuffer::create( std::unique_ptr<audio::AudioSinkTransferBuffer> output_transfer_buffer = audio::AudioSinkTransferBuffer::create(
this_mixer->audio_stream_info_.value().ms_to_bytes(TRANSFER_BUFFER_DURATION_MS)); this_mixer->audio_stream_info_.value().ms_to_bytes(TRANSFER_BUFFER_DURATION_MS));
if (output_transfer_buffer == nullptr) { if (output_transfer_buffer == nullptr) {
xEventGroupSetBits(this_mixer->event_group_, xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPED | MIXER_TASK_ERR_ESP_NO_MEM);
MixerEventGroupBits::STATE_STOPPED | MixerEventGroupBits::ERR_ESP_NO_MEM);
this_mixer->task_created_ = false; vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it
vTaskDelete(nullptr);
} }
output_transfer_buffer->set_sink(this_mixer->output_speaker_); output_transfer_buffer->set_sink(this_mixer->output_speaker_);
xEventGroupSetBits(this_mixer->event_group_, MixerEventGroupBits::STATE_RUNNING); xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_RUNNING);
bool sent_finished = false; 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) { while (true) {
uint32_t event_group_bits = xEventGroupGetBits(this_mixer->event_group_); uint32_t event_group_bits = xEventGroupGetBits(this_mixer->event_group_);
if (event_group_bits & MixerEventGroupBits::COMMAND_STOP) { if (event_group_bits & MIXER_TASK_COMMAND_STOP) {
break; break;
} }
@@ -507,15 +681,20 @@ void MixerSpeaker::audio_mixer_task(void *params) {
const uint32_t output_frames_free = const uint32_t output_frames_free =
this_mixer->audio_stream_info_.value().bytes_to_frames(output_transfer_buffer->free()); this_mixer->audio_stream_info_.value().bytes_to_frames(output_transfer_buffer->free());
std::vector<SourceSpeaker *> speakers_with_data; speakers_with_data.clear();
std::vector<std::shared_ptr<audio::AudioSourceTransferBuffer>> transfer_buffers_with_data; transfer_buffers_with_data.clear();
for (auto &speaker : this_mixer->source_speakers_) { for (auto &speaker : this_mixer->source_speakers_) {
if (speaker->get_transfer_buffer().use_count() > 0) { if (speaker->is_running() && !speaker->get_pause_state()) {
// Speaker is running and not paused, so it possibly can provide audio data
std::shared_ptr<audio::AudioSourceTransferBuffer> transfer_buffer = speaker->get_transfer_buffer().lock(); std::shared_ptr<audio::AudioSourceTransferBuffer> transfer_buffer = speaker->get_transfer_buffer().lock();
speaker->process_data_from_source(0); // Transfers and ducks audio from source ring buffers 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
if ((transfer_buffer->available() > 0) && !speaker->get_pause_state()) { if (transfer_buffer->available() > 0) {
// Store the locked transfer buffers in their own vector to avoid releasing ownership until after the loop // 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); transfer_buffers_with_data.push_back(transfer_buffer);
speakers_with_data.push_back(speaker); speakers_with_data.push_back(speaker);
@@ -547,13 +726,21 @@ void MixerSpeaker::audio_mixer_task(void *params) {
reinterpret_cast<int16_t *>(output_transfer_buffer->get_buffer_end()), reinterpret_cast<int16_t *>(output_transfer_buffer->get_buffer_end()),
this_mixer->audio_stream_info_.value(), frames_to_mix); this_mixer->audio_stream_info_.value(), frames_to_mix);
// Update source speaker buffer length // Set playback delay for newly contributing source
transfer_buffers_with_data[0]->decrease_buffer_length(active_stream_info.frames_to_bytes(frames_to_mix)); if (!speakers_with_data[0]->has_contributed_.load(std::memory_order_acquire)) {
speakers_with_data[0]->pending_playback_frames_ += frames_to_mix; 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 output transfer buffer length // Update source speaker pending frames
speakers_with_data[0]->pending_playback_frames_.fetch_add(frames_to_mix, std::memory_order_release);
transfer_buffers_with_data[0]->decrease_buffer_length(active_stream_info.frames_to_bytes(frames_to_mix));
// Update output transfer buffer length and pipeline frame count
output_transfer_buffer->increase_buffer_length( output_transfer_buffer->increase_buffer_length(
this_mixer->audio_stream_info_.value().frames_to_bytes(frames_to_mix)); 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 { } else {
// Speaker's stream info doesn't match the output speaker's, so it's a new source speaker // 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()) { if (!this_mixer->output_speaker_->is_stopped()) {
@@ -568,6 +755,8 @@ void MixerSpeaker::audio_mixer_task(void *params) {
active_stream_info.get_sample_rate()); active_stream_info.get_sample_rate());
this_mixer->output_speaker_->set_audio_stream_info(this_mixer->audio_stream_info_.value()); this_mixer->output_speaker_->set_audio_stream_info(this_mixer->audio_stream_info_.value());
this_mixer->output_speaker_->start(); 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; sent_finished = false;
} }
} }
@@ -596,26 +785,39 @@ 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 // 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) { 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( 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]->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 // Update output transfer buffer length and pipeline frame count (once, not per source)
output_transfer_buffer->increase_buffer_length( output_transfer_buffer->increase_buffer_length(
this_mixer->audio_stream_info_.value().frames_to_bytes(frames_to_mix)); 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_, MixerEventGroupBits::STATE_STOPPING); 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);
output_transfer_buffer.reset(); output_transfer_buffer.reset();
xEventGroupSetBits(this_mixer->event_group_, MixerEventGroupBits::STATE_STOPPED); xEventGroupSetBits(this_mixer->event_group_, MIXER_TASK_STATE_STOPPED);
this_mixer->task_created_ = false;
vTaskDelete(nullptr); vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it
} }
} // namespace mixer_speaker } // namespace mixer_speaker

View File

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

View File

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

View File

@@ -72,53 +72,55 @@ void MS8607Component::setup() {
// I do not know why the device sometimes NACKs the reset command, but // I do not know why the device sometimes NACKs the reset command, but
// try 3 times in case it's a transitory issue on this boot // try 3 times in case it's a transitory issue on this boot
this->set_retry( // Backoff: executes at now, +5ms, +30ms
"reset", 5, 3, this->reset_attempts_remaining_ = 3;
[this](const uint8_t remaining_setup_attempts) { this->reset_interval_ = 5;
ESP_LOGD(TAG, "Resetting both I2C addresses: 0x%02X, 0x%02X", this->address_, this->try_reset_();
this->humidity_device_->get_address()); }
// I believe sending the reset command to both addresses is preferable to
// skipping humidity if PT fails for some reason.
// However, only consider the reset successful if they both ACK
bool const pt_successful = this->write_bytes(MS8607_PT_CMD_RESET, nullptr, 0);
bool const h_successful = this->humidity_device_->write_bytes(MS8607_CMD_H_RESET, nullptr, 0);
if (!(pt_successful && h_successful)) { void MS8607Component::try_reset_() {
ESP_LOGE(TAG, "Resetting I2C devices failed"); ESP_LOGD(TAG, "Resetting both I2C addresses: 0x%02X, 0x%02X", this->address_, this->humidity_device_->get_address());
if (!pt_successful && !h_successful) { // I believe sending the reset command to both addresses is preferable to
this->error_code_ = ErrorCode::PTH_RESET_FAILED; // skipping humidity if PT fails for some reason.
} else if (!pt_successful) { // However, only consider the reset successful if they both ACK
this->error_code_ = ErrorCode::PT_RESET_FAILED; bool const pt_successful = this->write_bytes(MS8607_PT_CMD_RESET, nullptr, 0);
} else { bool const h_successful = this->humidity_device_->write_bytes(MS8607_CMD_H_RESET, nullptr, 0);
this->error_code_ = ErrorCode::H_RESET_FAILED;
}
if (remaining_setup_attempts > 0) { if (!(pt_successful && h_successful)) {
this->status_set_error(); ESP_LOGE(TAG, "Resetting I2C devices failed");
} else { if (!pt_successful && !h_successful) {
this->mark_failed(); this->error_code_ = ErrorCode::PTH_RESET_FAILED;
} } else if (!pt_successful) {
return RetryResult::RETRY; this->error_code_ = ErrorCode::PT_RESET_FAILED;
} } else {
this->error_code_ = ErrorCode::H_RESET_FAILED;
}
this->setup_status_ = SetupStatus::NEEDS_PROM_READ; if (--this->reset_attempts_remaining_ > 0) {
this->error_code_ = ErrorCode::NONE; uint32_t delay = this->reset_interval_;
this->status_clear_error(); this->reset_interval_ *= 5;
this->set_timeout("reset", delay, [this]() { this->try_reset_(); });
this->status_set_error();
} else {
this->mark_failed();
}
return;
}
// 15ms delay matches datasheet, Adafruit_MS8607 & SparkFun_PHT_MS8607_Arduino_Library this->setup_status_ = SetupStatus::NEEDS_PROM_READ;
this->set_timeout("prom-read", 15, [this]() { this->error_code_ = ErrorCode::NONE;
if (this->read_calibration_values_from_prom_()) { this->status_clear_error();
this->setup_status_ = SetupStatus::SUCCESSFUL;
this->status_clear_error();
} else {
this->mark_failed();
return;
}
});
return RetryResult::DONE; // 15ms delay matches datasheet, Adafruit_MS8607 & SparkFun_PHT_MS8607_Arduino_Library
}, this->set_timeout("prom-read", 15, [this]() {
5.0f); // executes at now, +5ms, +25ms if (this->read_calibration_values_from_prom_()) {
this->setup_status_ = SetupStatus::SUCCESSFUL;
this->status_clear_error();
} else {
this->mark_failed();
return;
}
});
} }
void MS8607Component::update() { void MS8607Component::update() {

View File

@@ -44,6 +44,8 @@ class MS8607Component : public PollingComponent, public i2c::I2CDevice {
void set_humidity_device(MS8607HumidityDevice *humidity_device) { humidity_device_ = humidity_device; } void set_humidity_device(MS8607HumidityDevice *humidity_device) { humidity_device_ = humidity_device; }
protected: protected:
/// Attempt to reset both I2C devices, retrying with backoff on failure
void try_reset_();
/** /**
Read and store the Pressure & Temperature calibration settings from the PROM. Read and store the Pressure & Temperature calibration settings from the PROM.
Intended to be called during setup(), this will set the `failure_reason_` Intended to be called during setup(), this will set the `failure_reason_`
@@ -102,6 +104,8 @@ class MS8607Component : public PollingComponent, public i2c::I2CDevice {
enum class SetupStatus; enum class SetupStatus;
/// Current step in the multi-step & possibly delayed setup() process /// Current step in the multi-step & possibly delayed setup() process
SetupStatus setup_status_; SetupStatus setup_status_;
uint32_t reset_interval_{5};
uint8_t reset_attempts_remaining_{0};
}; };
} // namespace ms8607 } // namespace ms8607

View File

@@ -397,11 +397,17 @@ bool Nextion::remove_from_q_(bool report_empty) {
} }
void Nextion::process_serial_() { void Nextion::process_serial_() {
uint8_t d; // 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;
while (this->available()) { this->command_data_.append(reinterpret_cast<const char *>(buf), to_read);
read_byte(&d);
this->command_data_ += d;
} }
} }
// nextion.tech/instruction-set/ // nextion.tech/instruction-set/

View File

@@ -13,9 +13,12 @@ void Pipsolar::setup() {
} }
void Pipsolar::empty_uart_buffer_() { void Pipsolar::empty_uart_buffer_() {
uint8_t byte; uint8_t buf[64];
while (this->available()) { size_t avail;
this->read_byte(&byte); while ((avail = this->available()) > 0) {
if (!this->read_array(buf, std::min(avail, sizeof(buf)))) {
break;
}
} }
} }
@@ -94,32 +97,47 @@ void Pipsolar::loop() {
} }
if (this->state_ == STATE_COMMAND || this->state_ == STATE_POLL) { if (this->state_ == STATE_COMMAND || this->state_ == STATE_POLL) {
while (this->available()) { size_t avail = this->available();
uint8_t byte; while (avail > 0) {
this->read_byte(&byte); uint8_t buf[64];
size_t to_read = std::min(avail, sizeof(buf));
// make sure data and null terminator fit in buffer if (!this->read_array(buf, to_read)) {
if (this->read_pos_ >= PIPSOLAR_READ_BUFFER_LENGTH - 1) {
this->read_pos_ = 0;
this->empty_uart_buffer_();
ESP_LOGW(TAG, "response data too long, discarding.");
break; break;
} }
this->read_buffer_[this->read_pos_] = byte; avail -= to_read;
this->read_pos_++; bool done = false;
for (size_t i = 0; i < to_read; i++) {
uint8_t byte = buf[i];
// end of answer // make sure data and null terminator fit in buffer
if (byte == 0x0D) { if (this->read_pos_ >= PIPSOLAR_READ_BUFFER_LENGTH - 1) {
this->read_buffer_[this->read_pos_] = 0; this->read_pos_ = 0;
this->empty_uart_buffer_(); this->empty_uart_buffer_();
if (this->state_ == STATE_POLL) { ESP_LOGW(TAG, "response data too long, discarding.");
this->state_ = STATE_POLL_COMPLETE; done = true;
break;
} }
if (this->state_ == STATE_COMMAND) { this->read_buffer_[this->read_pos_] = byte;
this->state_ = STATE_COMMAND_COMPLETE; this->read_pos_++;
// end of answer
if (byte == 0x0D) {
this->read_buffer_[this->read_pos_] = 0;
this->empty_uart_buffer_();
if (this->state_ == STATE_POLL) {
this->state_ = STATE_POLL_COMPLETE;
}
if (this->state_ == STATE_COMMAND) {
this->state_ = STATE_COMMAND_COMPLETE;
}
done = true;
break;
} }
} }
} // available if (done) {
break;
}
}
} }
if (this->state_ == STATE_COMMAND) { if (this->state_ == STATE_COMMAND) {
if (millis() - this->command_start_millis_ > esphome::pipsolar::Pipsolar::COMMAND_TIMEOUT) { if (millis() - this->command_start_millis_ > esphome::pipsolar::Pipsolar::COMMAND_TIMEOUT) {

View File

@@ -1,6 +1,11 @@
#include "pulse_counter_sensor.h" #include "pulse_counter_sensor.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#ifdef HAS_PCNT
#include <esp_clk_tree.h>
#include <hal/pcnt_ll.h>
#endif
namespace esphome { namespace esphome {
namespace pulse_counter { namespace pulse_counter {
@@ -56,103 +61,109 @@ pulse_counter_t BasicPulseCounterStorage::read_raw_value() {
#ifdef HAS_PCNT #ifdef HAS_PCNT
bool HwPulseCounterStorage::pulse_counter_setup(InternalGPIOPin *pin) { bool HwPulseCounterStorage::pulse_counter_setup(InternalGPIOPin *pin) {
static pcnt_unit_t next_pcnt_unit = PCNT_UNIT_0;
static pcnt_channel_t next_pcnt_channel = PCNT_CHANNEL_0;
this->pin = pin; this->pin = pin;
this->pin->setup(); this->pin->setup();
this->pcnt_unit = next_pcnt_unit;
this->pcnt_channel = next_pcnt_channel; pcnt_unit_config_t unit_config = {
next_pcnt_unit = pcnt_unit_t(int(next_pcnt_unit) + 1); .low_limit = INT16_MIN,
if (int(next_pcnt_unit) >= PCNT_UNIT_0 + PCNT_UNIT_MAX) { .high_limit = INT16_MAX,
next_pcnt_unit = PCNT_UNIT_0; .flags = {.accum_count = true},
next_pcnt_channel = pcnt_channel_t(int(next_pcnt_channel) + 1); };
esp_err_t error = pcnt_new_unit(&unit_config, &this->pcnt_unit);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Creating PCNT unit failed: %s", esp_err_to_name(error));
return false;
} }
ESP_LOGCONFIG(TAG, pcnt_chan_config_t chan_config = {
" PCNT Unit Number: %u\n" .edge_gpio_num = this->pin->get_pin(),
" PCNT Channel Number: %u", .level_gpio_num = -1,
this->pcnt_unit, this->pcnt_channel); };
error = pcnt_new_channel(this->pcnt_unit, &chan_config, &this->pcnt_channel);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Creating PCNT channel failed: %s", esp_err_to_name(error));
return false;
}
pcnt_count_mode_t rising = PCNT_COUNT_DIS, falling = PCNT_COUNT_DIS; pcnt_channel_edge_action_t rising = PCNT_CHANNEL_EDGE_ACTION_HOLD;
pcnt_channel_edge_action_t falling = PCNT_CHANNEL_EDGE_ACTION_HOLD;
switch (this->rising_edge_mode) { switch (this->rising_edge_mode) {
case PULSE_COUNTER_DISABLE: case PULSE_COUNTER_DISABLE:
rising = PCNT_COUNT_DIS; rising = PCNT_CHANNEL_EDGE_ACTION_HOLD;
break; break;
case PULSE_COUNTER_INCREMENT: case PULSE_COUNTER_INCREMENT:
rising = PCNT_COUNT_INC; rising = PCNT_CHANNEL_EDGE_ACTION_INCREASE;
break; break;
case PULSE_COUNTER_DECREMENT: case PULSE_COUNTER_DECREMENT:
rising = PCNT_COUNT_DEC; rising = PCNT_CHANNEL_EDGE_ACTION_DECREASE;
break; break;
} }
switch (this->falling_edge_mode) { switch (this->falling_edge_mode) {
case PULSE_COUNTER_DISABLE: case PULSE_COUNTER_DISABLE:
falling = PCNT_COUNT_DIS; falling = PCNT_CHANNEL_EDGE_ACTION_HOLD;
break; break;
case PULSE_COUNTER_INCREMENT: case PULSE_COUNTER_INCREMENT:
falling = PCNT_COUNT_INC; falling = PCNT_CHANNEL_EDGE_ACTION_INCREASE;
break; break;
case PULSE_COUNTER_DECREMENT: case PULSE_COUNTER_DECREMENT:
falling = PCNT_COUNT_DEC; falling = PCNT_CHANNEL_EDGE_ACTION_DECREASE;
break; break;
} }
pcnt_config_t pcnt_config = { error = pcnt_channel_set_edge_action(this->pcnt_channel, rising, falling);
.pulse_gpio_num = this->pin->get_pin(),
.ctrl_gpio_num = PCNT_PIN_NOT_USED,
.lctrl_mode = PCNT_MODE_KEEP,
.hctrl_mode = PCNT_MODE_KEEP,
.pos_mode = rising,
.neg_mode = falling,
.counter_h_lim = 0,
.counter_l_lim = 0,
.unit = this->pcnt_unit,
.channel = this->pcnt_channel,
};
esp_err_t error = pcnt_unit_config(&pcnt_config);
if (error != ESP_OK) { if (error != ESP_OK) {
ESP_LOGE(TAG, "Configuring Pulse Counter failed: %s", esp_err_to_name(error)); ESP_LOGE(TAG, "Setting PCNT edge action failed: %s", esp_err_to_name(error));
return false; return false;
} }
if (this->filter_us != 0) { if (this->filter_us != 0) {
uint16_t filter_val = std::min(static_cast<unsigned int>(this->filter_us * 80u), 1023u); uint32_t apb_freq;
ESP_LOGCONFIG(TAG, " Filter Value: %" PRIu32 "us (val=%u)", this->filter_us, filter_val); esp_clk_tree_src_get_freq_hz(SOC_MOD_CLK_APB, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &apb_freq);
error = pcnt_set_filter_value(this->pcnt_unit, filter_val); uint32_t max_glitch_ns = PCNT_LL_MAX_GLITCH_WIDTH * 1000000u / apb_freq;
pcnt_glitch_filter_config_t filter_config = {
.max_glitch_ns = std::min(this->filter_us * 1000u, max_glitch_ns),
};
error = pcnt_unit_set_glitch_filter(this->pcnt_unit, &filter_config);
if (error != ESP_OK) { if (error != ESP_OK) {
ESP_LOGE(TAG, "Setting filter value failed: %s", esp_err_to_name(error)); ESP_LOGE(TAG, "Setting PCNT glitch filter failed: %s", esp_err_to_name(error));
return false;
}
error = pcnt_filter_enable(this->pcnt_unit);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Enabling filter failed: %s", esp_err_to_name(error));
return false; return false;
} }
} }
error = pcnt_counter_pause(this->pcnt_unit); error = pcnt_unit_add_watch_point(this->pcnt_unit, INT16_MIN);
if (error != ESP_OK) { if (error != ESP_OK) {
ESP_LOGE(TAG, "Pausing pulse counter failed: %s", esp_err_to_name(error)); ESP_LOGE(TAG, "Adding PCNT low limit watch point failed: %s", esp_err_to_name(error));
return false; return false;
} }
error = pcnt_counter_clear(this->pcnt_unit); error = pcnt_unit_add_watch_point(this->pcnt_unit, INT16_MAX);
if (error != ESP_OK) { if (error != ESP_OK) {
ESP_LOGE(TAG, "Clearing pulse counter failed: %s", esp_err_to_name(error)); ESP_LOGE(TAG, "Adding PCNT high limit watch point failed: %s", esp_err_to_name(error));
return false; return false;
} }
error = pcnt_counter_resume(this->pcnt_unit);
error = pcnt_unit_enable(this->pcnt_unit);
if (error != ESP_OK) { if (error != ESP_OK) {
ESP_LOGE(TAG, "Resuming pulse counter failed: %s", esp_err_to_name(error)); ESP_LOGE(TAG, "Enabling PCNT unit failed: %s", esp_err_to_name(error));
return false;
}
error = pcnt_unit_clear_count(this->pcnt_unit);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Clearing PCNT unit failed: %s", esp_err_to_name(error));
return false;
}
error = pcnt_unit_start(this->pcnt_unit);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Starting PCNT unit failed: %s", esp_err_to_name(error));
return false; return false;
} }
return true; return true;
} }
pulse_counter_t HwPulseCounterStorage::read_raw_value() { pulse_counter_t HwPulseCounterStorage::read_raw_value() {
pulse_counter_t counter; int count;
pcnt_get_counter_value(this->pcnt_unit, &counter); pcnt_unit_get_count(this->pcnt_unit, &count);
pulse_counter_t ret = counter - this->last_value; pulse_counter_t ret = count - this->last_value;
this->last_value = counter; this->last_value = count;
return ret; return ret;
} }
#endif // HAS_PCNT #endif // HAS_PCNT

View File

@@ -6,14 +6,13 @@
#include <cinttypes> #include <cinttypes>
// TODO: Migrate from legacy PCNT API (driver/pcnt.h) to new PCNT API (driver/pulse_cnt.h) #if defined(USE_ESP32)
// The legacy PCNT API is deprecated in ESP-IDF 5.x. Migration would allow removing the #include <soc/soc_caps.h>
// "driver" IDF component dependency. See: #ifdef SOC_PCNT_SUPPORTED
// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/migration-guides/release-5.x/5.0/peripherals.html#id6 #include <driver/pulse_cnt.h>
#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3)
#include <driver/pcnt.h>
#define HAS_PCNT #define HAS_PCNT
#endif // defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) #endif // SOC_PCNT_SUPPORTED
#endif // USE_ESP32
namespace esphome { namespace esphome {
namespace pulse_counter { namespace pulse_counter {
@@ -24,11 +23,7 @@ enum PulseCounterCountMode {
PULSE_COUNTER_DECREMENT, PULSE_COUNTER_DECREMENT,
}; };
#ifdef HAS_PCNT
using pulse_counter_t = int16_t;
#else // HAS_PCNT
using pulse_counter_t = int32_t; using pulse_counter_t = int32_t;
#endif // HAS_PCNT
struct PulseCounterStorageBase { struct PulseCounterStorageBase {
virtual bool pulse_counter_setup(InternalGPIOPin *pin) = 0; virtual bool pulse_counter_setup(InternalGPIOPin *pin) = 0;
@@ -58,8 +53,8 @@ struct HwPulseCounterStorage : public PulseCounterStorageBase {
bool pulse_counter_setup(InternalGPIOPin *pin) override; bool pulse_counter_setup(InternalGPIOPin *pin) override;
pulse_counter_t read_raw_value() override; pulse_counter_t read_raw_value() override;
pcnt_unit_t pcnt_unit; pcnt_unit_handle_t pcnt_unit{nullptr};
pcnt_channel_t pcnt_channel; pcnt_channel_handle_t pcnt_channel{nullptr};
}; };
#endif // HAS_PCNT #endif // HAS_PCNT

View File

@@ -129,10 +129,7 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config): async def to_code(config):
use_pcnt = config.get(CONF_USE_PCNT) use_pcnt = config.get(CONF_USE_PCNT)
if CORE.is_esp32 and use_pcnt: if CORE.is_esp32 and use_pcnt:
# Re-enable ESP-IDF's legacy driver component (excluded by default to save compile time) include_builtin_idf_component("esp_driver_pcnt")
# Provides driver/pcnt.h header for hardware pulse counter API
# TODO: Remove this once pulse_counter migrates to new PCNT API (driver/pulse_cnt.h)
include_builtin_idf_component("driver")
var = await sensor.new_sensor(config, use_pcnt) var = await sensor.new_sensor(config, use_pcnt)
await cg.register_component(var, config) await cg.register_component(var, config)

View File

@@ -56,17 +56,23 @@ void PylontechComponent::setup() {
void PylontechComponent::update() { this->write_str("pwr\n"); } void PylontechComponent::update() { this->write_str("pwr\n"); }
void PylontechComponent::loop() { void PylontechComponent::loop() {
if (this->available() > 0) { size_t avail = this->available();
if (avail > 0) {
// pylontech sends a lot of data very suddenly // pylontech sends a lot of data very suddenly
// we need to quickly put it all into our own buffer, otherwise the uart's buffer will overflow // we need to quickly put it all into our own buffer, otherwise the uart's buffer will overflow
uint8_t data;
int recv = 0; int recv = 0;
while (this->available() > 0) { uint8_t buf[64];
if (this->read_byte(&data)) { while (avail > 0) {
buffer_[buffer_index_write_] += (char) data; size_t to_read = std::min(avail, sizeof(buf));
recv++; if (!this->read_array(buf, to_read)) {
if (buffer_[buffer_index_write_].back() == static_cast<char>(ASCII_LF) || break;
buffer_[buffer_index_write_].length() >= MAX_DATA_LENGTH_BYTES) { }
avail -= to_read;
recv += to_read;
for (size_t i = 0; i < to_read; i++) {
buffer_[buffer_index_write_] += (char) buf[i];
if (buf[i] == ASCII_LF || buffer_[buffer_index_write_].length() >= MAX_DATA_LENGTH_BYTES) {
// complete line received // complete line received
buffer_index_write_ = (buffer_index_write_ + 1) % NUM_BUFFERS; buffer_index_write_ = (buffer_index_write_ + 1) % NUM_BUFFERS;
} }

View File

@@ -1,4 +1,5 @@
#include "rd03d.h" #include "rd03d.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <cmath> #include <cmath>
@@ -80,37 +81,47 @@ void RD03DComponent::dump_config() {
} }
void RD03DComponent::loop() { void RD03DComponent::loop() {
while (this->available()) { // Read all available bytes in batches to reduce UART call overhead.
uint8_t byte = this->read(); size_t avail = this->available();
ESP_LOGVV(TAG, "Received byte: 0x%02X, buffer_pos: %d", byte, this->buffer_pos_); 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++) {
uint8_t byte = buf[i];
ESP_LOGVV(TAG, "Received byte: 0x%02X, buffer_pos: %d", byte, this->buffer_pos_);
// Check if we're looking for frame header // Check if we're looking for frame header
if (this->buffer_pos_ < FRAME_HEADER_SIZE) { if (this->buffer_pos_ < FRAME_HEADER_SIZE) {
if (byte == FRAME_HEADER[this->buffer_pos_]) { if (byte == FRAME_HEADER[this->buffer_pos_]) {
this->buffer_[this->buffer_pos_++] = byte; this->buffer_[this->buffer_pos_++] = byte;
} else if (byte == FRAME_HEADER[0]) { } else if (byte == FRAME_HEADER[0]) {
// Start over if we see a potential new header // Start over if we see a potential new header
this->buffer_[0] = byte; this->buffer_[0] = byte;
this->buffer_pos_ = 1; this->buffer_pos_ = 1;
} else { } else {
this->buffer_pos_ = 0;
}
continue;
}
// Accumulate data bytes
this->buffer_[this->buffer_pos_++] = byte;
// Check if we have a complete frame
if (this->buffer_pos_ == FRAME_SIZE) {
// Validate footer
if (this->buffer_[FRAME_SIZE - 2] == FRAME_FOOTER[0] && this->buffer_[FRAME_SIZE - 1] == FRAME_FOOTER[1]) {
this->process_frame_();
} else {
ESP_LOGW(TAG, "Invalid frame footer: 0x%02X 0x%02X (expected 0x55 0xCC)", this->buffer_[FRAME_SIZE - 2],
this->buffer_[FRAME_SIZE - 1]);
}
this->buffer_pos_ = 0; this->buffer_pos_ = 0;
} }
continue;
}
// Accumulate data bytes
this->buffer_[this->buffer_pos_++] = byte;
// Check if we have a complete frame
if (this->buffer_pos_ == FRAME_SIZE) {
// Validate footer
if (this->buffer_[FRAME_SIZE - 2] == FRAME_FOOTER[0] && this->buffer_[FRAME_SIZE - 1] == FRAME_FOOTER[1]) {
this->process_frame_();
} else {
ESP_LOGW(TAG, "Invalid frame footer: 0x%02X 0x%02X (expected 0x55 0xCC)", this->buffer_[FRAME_SIZE - 2],
this->buffer_[FRAME_SIZE - 1]);
}
this->buffer_pos_ = 0;
} }
} }
} }

View File

@@ -3,15 +3,11 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
#include <driver/gpio.h> #include <driver/gpio.h>
#include <esp_clk_tree.h>
namespace esphome::remote_receiver { namespace esphome::remote_receiver {
static const char *const TAG = "remote_receiver.esp32"; static const char *const TAG = "remote_receiver.esp32";
#ifdef USE_ESP32_VARIANT_ESP32H2
static const uint32_t RMT_CLK_FREQ = 32000000;
#else
static const uint32_t RMT_CLK_FREQ = 80000000;
#endif
static bool IRAM_ATTR HOT rmt_callback(rmt_channel_handle_t channel, const rmt_rx_done_event_data_t *event, void *arg) { static bool IRAM_ATTR HOT rmt_callback(rmt_channel_handle_t channel, const rmt_rx_done_event_data_t *event, void *arg) {
RemoteReceiverComponentStore *store = (RemoteReceiverComponentStore *) arg; RemoteReceiverComponentStore *store = (RemoteReceiverComponentStore *) arg;
@@ -98,7 +94,10 @@ void RemoteReceiverComponent::setup() {
} }
uint32_t event_size = sizeof(rmt_rx_done_event_data_t); uint32_t event_size = sizeof(rmt_rx_done_event_data_t);
uint32_t max_filter_ns = 255u * 1000 / (RMT_CLK_FREQ / 1000000); uint32_t rmt_freq;
esp_clk_tree_src_get_freq_hz((soc_module_clk_t) RMT_CLK_SRC_DEFAULT, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED,
&rmt_freq);
uint32_t max_filter_ns = UINT8_MAX * 1000u / (rmt_freq / 1000000);
memset(&this->store_.config, 0, sizeof(this->store_.config)); memset(&this->store_.config, 0, sizeof(this->store_.config));
this->store_.config.signal_range_min_ns = std::min(this->filter_us_ * 1000, max_filter_ns); this->store_.config.signal_range_min_ns = std::min(this->filter_us_ * 1000, max_filter_ns);
this->store_.config.signal_range_max_ns = this->idle_us_ * 1000; this->store_.config.signal_range_max_ns = this->idle_us_ * 1000;

View File

@@ -1,5 +1,5 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import audio, esp32, speaker from esphome.components import audio, esp32, socket, speaker
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_BITS_PER_SAMPLE, CONF_BITS_PER_SAMPLE,
@@ -34,7 +34,7 @@ def _set_stream_limits(config):
return config return config
def _validate_audio_compatability(config): def _validate_audio_compatibility(config):
inherit_property_from(CONF_BITS_PER_SAMPLE, CONF_OUTPUT_SPEAKER)(config) inherit_property_from(CONF_BITS_PER_SAMPLE, CONF_OUTPUT_SPEAKER)(config)
inherit_property_from(CONF_NUM_CHANNELS, CONF_OUTPUT_SPEAKER)(config) inherit_property_from(CONF_NUM_CHANNELS, CONF_OUTPUT_SPEAKER)(config)
inherit_property_from(CONF_SAMPLE_RATE, CONF_OUTPUT_SPEAKER)(config) inherit_property_from(CONF_SAMPLE_RATE, CONF_OUTPUT_SPEAKER)(config)
@@ -73,10 +73,13 @@ CONFIG_SCHEMA = cv.All(
) )
FINAL_VALIDATE_SCHEMA = _validate_audio_compatability FINAL_VALIDATE_SCHEMA = _validate_audio_compatibility
async def to_code(config): 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]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
await speaker.register_speaker(var, config) await speaker.register_speaker(var, config)
@@ -86,12 +89,11 @@ async def to_code(config):
cg.add(var.set_buffer_duration(config[CONF_BUFFER_DURATION])) cg.add(var.set_buffer_duration(config[CONF_BUFFER_DURATION]))
if task_stack_in_psram := config.get(CONF_TASK_STACK_IN_PSRAM): if config.get(CONF_TASK_STACK_IN_PSRAM):
cg.add(var.set_task_stack_in_psram(task_stack_in_psram)) cg.add(var.set_task_stack_in_psram(True))
if task_stack_in_psram and config[CONF_TASK_STACK_IN_PSRAM]: esp32.add_idf_sdkconfig_option(
esp32.add_idf_sdkconfig_option( "CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True
"CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True )
)
cg.add(var.set_target_bits_per_sample(config[CONF_BITS_PER_SAMPLE])) cg.add(var.set_target_bits_per_sample(config[CONF_BITS_PER_SAMPLE]))
cg.add(var.set_target_sample_rate(config[CONF_SAMPLE_RATE])) cg.add(var.set_target_sample_rate(config[CONF_SAMPLE_RATE]))

View File

@@ -4,6 +4,8 @@
#include "esphome/components/audio/audio_resampler.h" #include "esphome/components/audio/audio_resampler.h"
#include "esphome/core/application.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
@@ -17,13 +19,17 @@ static const UBaseType_t RESAMPLER_TASK_PRIORITY = 1;
static const uint32_t TRANSFER_BUFFER_DURATION_MS = 50; static const uint32_t TRANSFER_BUFFER_DURATION_MS = 50;
static const uint32_t TASK_DELAY_MS = 20;
static const uint32_t TASK_STACK_SIZE = 3072; static const uint32_t TASK_STACK_SIZE = 3072;
static const uint32_t STATE_TRANSITION_TIMEOUT_MS = 5000;
static const char *const TAG = "resampler_speaker"; static const char *const TAG = "resampler_speaker";
enum ResamplingEventGroupBits : uint32_t { enum ResamplingEventGroupBits : uint32_t {
COMMAND_STOP = (1 << 0), // stops the resampler task COMMAND_STOP = (1 << 0), // signals stop request
COMMAND_START = (1 << 1), // signals start request
COMMAND_FINISH = (1 << 2), // signals finish request (graceful stop)
TASK_COMMAND_STOP = (1 << 5), // signals the task to stop
STATE_STARTING = (1 << 10), STATE_STARTING = (1 << 10),
STATE_RUNNING = (1 << 11), STATE_RUNNING = (1 << 11),
STATE_STOPPING = (1 << 12), STATE_STOPPING = (1 << 12),
@@ -34,9 +40,16 @@ enum ResamplingEventGroupBits : uint32_t {
ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits
}; };
void ResamplerSpeaker::dump_config() {
ESP_LOGCONFIG(TAG,
"Resampler Speaker:\n"
" Target Bits Per Sample: %u\n"
" Target Sample Rate: %" PRIu32 " Hz",
this->target_bits_per_sample_, this->target_sample_rate_);
}
void ResamplerSpeaker::setup() { void ResamplerSpeaker::setup() {
this->event_group_ = xEventGroupCreate(); this->event_group_ = xEventGroupCreate();
if (this->event_group_ == nullptr) { if (this->event_group_ == nullptr) {
ESP_LOGE(TAG, "Failed to create event group"); ESP_LOGE(TAG, "Failed to create event group");
this->mark_failed(); this->mark_failed();
@@ -55,81 +68,155 @@ void ResamplerSpeaker::setup() {
this->audio_output_callback_(new_frames, write_timestamp); this->audio_output_callback_(new_frames, write_timestamp);
} }
}); });
// Start with loop disabled since no task is running and no commands are pending
this->disable_loop();
} }
void ResamplerSpeaker::loop() { void ResamplerSpeaker::loop() {
uint32_t event_group_bits = xEventGroupGetBits(this->event_group_); uint32_t event_group_bits = xEventGroupGetBits(this->event_group_);
// Process commands with priority: STOP > FINISH > START
// This ensures stop commands take precedence over conflicting start commands
if (event_group_bits & ResamplingEventGroupBits::COMMAND_STOP) {
if (this->state_ == speaker::STATE_RUNNING || this->state_ == speaker::STATE_STARTING) {
// Clear STOP, START, and FINISH bits - stop takes precedence
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::COMMAND_STOP |
ResamplingEventGroupBits::COMMAND_START |
ResamplingEventGroupBits::COMMAND_FINISH);
this->waiting_for_output_ = false;
this->enter_stopping_state_();
} else if (this->state_ == speaker::STATE_STOPPED) {
// Already stopped, just clear the command bits
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::COMMAND_STOP |
ResamplingEventGroupBits::COMMAND_START |
ResamplingEventGroupBits::COMMAND_FINISH);
}
// Leave bits set if STATE_STOPPING - will be processed once stopped
} else if (event_group_bits & ResamplingEventGroupBits::COMMAND_FINISH) {
if (this->state_ == speaker::STATE_RUNNING) {
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::COMMAND_FINISH);
this->output_speaker_->finish();
} else if (this->state_ == speaker::STATE_STOPPED) {
// Already stopped, just clear the command bit
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::COMMAND_FINISH);
}
// Leave bit set if transitioning states - will be processed once state allows
} else if (event_group_bits & ResamplingEventGroupBits::COMMAND_START) {
if (this->state_ == speaker::STATE_STOPPED) {
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::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_, ResamplingEventGroupBits::COMMAND_START);
}
// Leave bit set if transitioning states - will be processed once state allows
}
// Re-read bits after command processing (enter_stopping_state_ may have set task bits)
event_group_bits = xEventGroupGetBits(this->event_group_);
if (event_group_bits & ResamplingEventGroupBits::STATE_STARTING) { if (event_group_bits & ResamplingEventGroupBits::STATE_STARTING) {
ESP_LOGD(TAG, "Starting resampler task"); ESP_LOGD(TAG, "Starting");
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::STATE_STARTING); xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::STATE_STARTING);
} }
if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_NO_MEM) { if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_NO_MEM) {
this->status_set_error(LOG_STR("Resampler task failed to allocate the internal buffers")); this->status_set_error(LOG_STR("Not enough memory"));
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_NO_MEM); xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_NO_MEM);
this->state_ = speaker::STATE_STOPPING; this->enter_stopping_state_();
} }
if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_NOT_SUPPORTED) { if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_NOT_SUPPORTED) {
this->status_set_error(LOG_STR("Cannot resample due to an unsupported audio stream")); this->status_set_error(LOG_STR("Unsupported stream"));
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_NOT_SUPPORTED); xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_NOT_SUPPORTED);
this->state_ = speaker::STATE_STOPPING; this->enter_stopping_state_();
} }
if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_FAIL) { if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_FAIL) {
this->status_set_error(LOG_STR("Resampler task failed")); this->status_set_error(LOG_STR("Resampler failure"));
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_FAIL); xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_FAIL);
this->state_ = speaker::STATE_STOPPING; this->enter_stopping_state_();
} }
if (event_group_bits & ResamplingEventGroupBits::STATE_RUNNING) { if (event_group_bits & ResamplingEventGroupBits::STATE_RUNNING) {
ESP_LOGD(TAG, "Started resampler task"); ESP_LOGV(TAG, "Started");
this->status_clear_error(); this->status_clear_error();
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::STATE_RUNNING); xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::STATE_RUNNING);
} }
if (event_group_bits & ResamplingEventGroupBits::STATE_STOPPING) { if (event_group_bits & ResamplingEventGroupBits::STATE_STOPPING) {
ESP_LOGD(TAG, "Stopping resampler task"); ESP_LOGV(TAG, "Stopping");
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::STATE_STOPPING); xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::STATE_STOPPING);
} }
if (event_group_bits & ResamplingEventGroupBits::STATE_STOPPED) { if (event_group_bits & ResamplingEventGroupBits::STATE_STOPPED) {
if (this->delete_task_() == ESP_OK) { this->delete_task_();
ESP_LOGD(TAG, "Stopped resampler task"); ESP_LOGD(TAG, "Stopped");
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ALL_BITS); xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ALL_BITS);
}
} }
switch (this->state_) { switch (this->state_) {
case speaker::STATE_STARTING: { case speaker::STATE_STARTING: {
esp_err_t err = this->start_(); if (!this->waiting_for_output_) {
if (err == ESP_OK) { esp_err_t err = this->start_();
this->status_clear_error(); if (err == ESP_OK) {
this->state_ = speaker::STATE_RUNNING; this->callback_remainder_ = 0; // reset callback remainder
} else { this->status_clear_error();
switch (err) { this->waiting_for_output_ = true;
case ESP_ERR_INVALID_STATE: this->state_start_ms_ = App.get_loop_component_start_time();
this->status_set_error(LOG_STR("Failed to start resampler: resampler task failed to start")); } else {
break; this->set_start_error_(err);
case ESP_ERR_NO_MEM: this->waiting_for_output_ = false;
this->status_set_error(LOG_STR("Failed to start resampler: not enough memory for task stack")); this->enter_stopping_state_();
default: }
this->status_set_error(LOG_STR("Failed to start resampler")); } else {
break; if (this->output_speaker_->is_running()) {
this->state_ = speaker::STATE_RUNNING;
this->waiting_for_output_ = false;
} else if ((App.get_loop_component_start_time() - this->state_start_ms_) > STATE_TRANSITION_TIMEOUT_MS) {
// Timed out waiting for the output speaker to start
this->waiting_for_output_ = false;
this->enter_stopping_state_();
} }
this->state_ = speaker::STATE_STOPPING;
} }
break; break;
} }
case speaker::STATE_RUNNING: case speaker::STATE_RUNNING:
if (this->output_speaker_->is_stopped()) { if (this->output_speaker_->is_stopped()) {
this->state_ = speaker::STATE_STOPPING; this->enter_stopping_state_();
}
break;
case speaker::STATE_STOPPING: {
if ((this->output_speaker_->get_pause_state()) ||
((App.get_loop_component_start_time() - this->state_start_ms_) > STATE_TRANSITION_TIMEOUT_MS)) {
// If output speaker is paused or stopping timeout exceeded, force stop
this->output_speaker_->stop();
} }
if (this->output_speaker_->is_stopped() && (this->task_handle_ == nullptr)) {
// Only transition to stopped state once the output speaker and resampler task are fully stopped
this->waiting_for_output_ = false;
this->state_ = speaker::STATE_STOPPED;
}
break; break;
case speaker::STATE_STOPPING: }
this->stop_();
this->state_ = speaker::STATE_STOPPED;
break;
case speaker::STATE_STOPPED: case speaker::STATE_STOPPED:
event_group_bits = xEventGroupGetBits(this->event_group_);
if (event_group_bits == 0) {
// No pending events, disable loop to save CPU cycles
this->disable_loop();
}
break;
}
}
void ResamplerSpeaker::set_start_error_(esp_err_t err) {
switch (err) {
case ESP_ERR_INVALID_STATE:
this->status_set_error(LOG_STR("Task failed to start"));
break;
case ESP_ERR_NO_MEM:
this->status_set_error(LOG_STR("Not enough memory"));
break;
default:
this->status_set_error(LOG_STR("Failed to start"));
break; break;
} }
} }
@@ -143,16 +230,33 @@ size_t ResamplerSpeaker::play(const uint8_t *data, size_t length, TickType_t tic
if ((this->output_speaker_->is_running()) && (!this->requires_resampling_())) { if ((this->output_speaker_->is_running()) && (!this->requires_resampling_())) {
bytes_written = this->output_speaker_->play(data, length, ticks_to_wait); bytes_written = this->output_speaker_->play(data, length, ticks_to_wait);
} else { } else {
if (this->ring_buffer_.use_count() == 1) { std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_.lock(); if (temp_ring_buffer) {
// Only write to the ring buffer if the reference is valid
bytes_written = temp_ring_buffer->write_without_replacement(data, length, ticks_to_wait); bytes_written = temp_ring_buffer->write_without_replacement(data, length, ticks_to_wait);
} else {
// Delay to avoid repeatedly hammering while waiting for the speaker to start
vTaskDelay(ticks_to_wait);
} }
} }
return bytes_written; return bytes_written;
} }
void ResamplerSpeaker::start() { this->state_ = speaker::STATE_STARTING; } void ResamplerSpeaker::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 ResamplerSpeaker::start() { this->send_command_(ResamplingEventGroupBits::COMMAND_START, true); }
esp_err_t ResamplerSpeaker::start_() { esp_err_t ResamplerSpeaker::start_() {
this->target_stream_info_ = audio::AudioStreamInfo( this->target_stream_info_ = audio::AudioStreamInfo(
@@ -185,7 +289,7 @@ esp_err_t ResamplerSpeaker::start_task_() {
} }
if (this->task_handle_ == nullptr) { if (this->task_handle_ == nullptr) {
this->task_handle_ = xTaskCreateStatic(resample_task, "sample", TASK_STACK_SIZE, (void *) this, this->task_handle_ = xTaskCreateStatic(resample_task, "resampler", TASK_STACK_SIZE, (void *) this,
RESAMPLER_TASK_PRIORITY, this->task_stack_buffer_, &this->task_stack_); RESAMPLER_TASK_PRIORITY, this->task_stack_buffer_, &this->task_stack_);
} }
@@ -196,43 +300,47 @@ esp_err_t ResamplerSpeaker::start_task_() {
return ESP_OK; return ESP_OK;
} }
void ResamplerSpeaker::stop() { this->state_ = speaker::STATE_STOPPING; } void ResamplerSpeaker::stop() { this->send_command_(ResamplingEventGroupBits::COMMAND_STOP); }
void ResamplerSpeaker::stop_() { void ResamplerSpeaker::enter_stopping_state_() {
this->state_ = speaker::STATE_STOPPING;
this->state_start_ms_ = App.get_loop_component_start_time();
if (this->task_handle_ != nullptr) { if (this->task_handle_ != nullptr) {
xEventGroupSetBits(this->event_group_, ResamplingEventGroupBits::COMMAND_STOP); xEventGroupSetBits(this->event_group_, ResamplingEventGroupBits::TASK_COMMAND_STOP);
} }
this->output_speaker_->stop(); this->output_speaker_->stop();
} }
esp_err_t ResamplerSpeaker::delete_task_() { void ResamplerSpeaker::delete_task_() {
if (!this->task_created_) { if (this->task_handle_ != nullptr) {
// Delete the suspended task
vTaskDelete(this->task_handle_);
this->task_handle_ = nullptr; this->task_handle_ = nullptr;
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;
}
return ESP_OK;
} }
return ESP_ERR_INVALID_STATE; if (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);
}
this->task_stack_buffer_ = nullptr;
}
} }
void ResamplerSpeaker::finish() { this->output_speaker_->finish(); } void ResamplerSpeaker::finish() { this->send_command_(ResamplingEventGroupBits::COMMAND_FINISH); }
bool ResamplerSpeaker::has_buffered_data() const { bool ResamplerSpeaker::has_buffered_data() const {
bool has_ring_buffer_data = false; bool has_ring_buffer_data = false;
if (this->requires_resampling_() && (this->ring_buffer_.use_count() > 0)) { if (this->requires_resampling_()) {
has_ring_buffer_data = (this->ring_buffer_.lock()->available() > 0); std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
if (temp_ring_buffer) {
has_ring_buffer_data = (temp_ring_buffer->available() > 0);
}
} }
return (has_ring_buffer_data || this->output_speaker_->has_buffered_data()); return (has_ring_buffer_data || this->output_speaker_->has_buffered_data());
} }
@@ -253,9 +361,8 @@ bool ResamplerSpeaker::requires_resampling_() const {
} }
void ResamplerSpeaker::resample_task(void *params) { void ResamplerSpeaker::resample_task(void *params) {
ResamplerSpeaker *this_resampler = (ResamplerSpeaker *) params; ResamplerSpeaker *this_resampler = static_cast<ResamplerSpeaker *>(params);
this_resampler->task_created_ = true;
xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_STARTING); xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_STARTING);
std::unique_ptr<audio::AudioResampler> resampler = std::unique_ptr<audio::AudioResampler> resampler =
@@ -269,7 +376,7 @@ void ResamplerSpeaker::resample_task(void *params) {
std::shared_ptr<RingBuffer> temp_ring_buffer = std::shared_ptr<RingBuffer> temp_ring_buffer =
RingBuffer::create(this_resampler->audio_stream_info_.ms_to_bytes(this_resampler->buffer_duration_ms_)); RingBuffer::create(this_resampler->audio_stream_info_.ms_to_bytes(this_resampler->buffer_duration_ms_));
if (temp_ring_buffer.use_count() == 0) { if (!temp_ring_buffer) {
err = ESP_ERR_NO_MEM; err = ESP_ERR_NO_MEM;
} else { } else {
this_resampler->ring_buffer_ = temp_ring_buffer; this_resampler->ring_buffer_ = temp_ring_buffer;
@@ -291,7 +398,7 @@ void ResamplerSpeaker::resample_task(void *params) {
while (err == ESP_OK) { while (err == ESP_OK) {
uint32_t event_bits = xEventGroupGetBits(this_resampler->event_group_); uint32_t event_bits = xEventGroupGetBits(this_resampler->event_group_);
if (event_bits & ResamplingEventGroupBits::COMMAND_STOP) { if (event_bits & ResamplingEventGroupBits::TASK_COMMAND_STOP) {
break; break;
} }
@@ -310,8 +417,8 @@ void ResamplerSpeaker::resample_task(void *params) {
xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_STOPPING); xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_STOPPING);
resampler.reset(); resampler.reset();
xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_STOPPED); xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_STOPPED);
this_resampler->task_created_ = false;
vTaskDelete(nullptr); vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it
} }
} // namespace resampler } // namespace resampler

View File

@@ -8,14 +8,16 @@
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include <freertos/event_groups.h>
#include <freertos/FreeRTOS.h> #include <freertos/FreeRTOS.h>
#include <freertos/event_groups.h>
namespace esphome { namespace esphome {
namespace resampler { namespace resampler {
class ResamplerSpeaker : public Component, public speaker::Speaker { class ResamplerSpeaker : public Component, public speaker::Speaker {
public: public:
float get_setup_priority() const override { return esphome::setup_priority::DATA; }
void dump_config() override;
void setup() override; void setup() override;
void loop() override; void loop() override;
@@ -65,13 +67,18 @@ class ResamplerSpeaker : public Component, public speaker::Speaker {
/// ESP_ERR_INVALID_STATE if the task wasn't created /// ESP_ERR_INVALID_STATE if the task wasn't created
esp_err_t start_task_(); esp_err_t start_task_();
/// @brief Stops the output speaker. If the resampling task is running, it sends the stop command. /// @brief Transitions to STATE_STOPPING, records the stopping timestamp, sends the task stop command if the task is
void stop_(); /// running, and stops the output speaker.
void enter_stopping_state_();
/// @brief Deallocates the task stack and resets the pointers. /// @brief Sets the appropriate status error based on the start failure reason.
/// @return ESP_OK if successful void set_start_error_(esp_err_t err);
/// ESP_ERR_INVALID_STATE if the task hasn't stopped itself
esp_err_t delete_task_(); /// @brief Deletes the resampler task if suspended, deallocates the task stack, and resets the related pointers.
void delete_task_();
/// @brief Sends a command via event group bits, enables the loop, and optionally wakes the main loop.
void send_command_(uint32_t command_bit, bool wake_loop = false);
inline bool requires_resampling_() const; inline bool requires_resampling_() const;
static void resample_task(void *params); static void resample_task(void *params);
@@ -83,7 +90,7 @@ class ResamplerSpeaker : public Component, public speaker::Speaker {
speaker::Speaker *output_speaker_{nullptr}; speaker::Speaker *output_speaker_{nullptr};
bool task_stack_in_psram_{false}; bool task_stack_in_psram_{false};
bool task_created_{false}; bool waiting_for_output_{false};
TaskHandle_t task_handle_{nullptr}; TaskHandle_t task_handle_{nullptr};
StaticTask_t task_stack_; StaticTask_t task_stack_;
@@ -98,6 +105,7 @@ class ResamplerSpeaker : public Component, public speaker::Speaker {
uint32_t target_sample_rate_; uint32_t target_sample_rate_;
uint32_t buffer_duration_ms_; uint32_t buffer_duration_ms_;
uint32_t state_start_ms_{0};
uint64_t callback_remainder_{0}; uint64_t callback_remainder_{0};
}; };

View File

@@ -136,14 +136,21 @@ void RFBridgeComponent::loop() {
this->last_bridge_byte_ = now; this->last_bridge_byte_ = now;
} }
while (this->available()) { size_t avail = this->available();
uint8_t byte; while (avail > 0) {
this->read_byte(&byte); uint8_t buf[64];
if (this->parse_bridge_byte_(byte)) { size_t to_read = std::min(avail, sizeof(buf));
ESP_LOGVV(TAG, "Parsed: 0x%02X", byte); if (!this->read_array(buf, to_read)) {
this->last_bridge_byte_ = now; break;
} else { }
this->rx_buffer_.clear(); avail -= to_read;
for (size_t i = 0; i < to_read; i++) {
if (this->parse_bridge_byte_(buf[i])) {
ESP_LOGVV(TAG, "Parsed: 0x%02X", buf[i]);
this->last_bridge_byte_ = now;
} else {
this->rx_buffer_.clear();
}
} }
} }
} }

View File

@@ -106,12 +106,19 @@ void MR24HPC1Component::update_() {
// main loop // main loop
void MR24HPC1Component::loop() { void MR24HPC1Component::loop() {
uint8_t byte; // 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;
// Is there data on the serial port for (size_t i = 0; i < to_read; i++) {
while (this->available()) { this->r24_split_data_frame_(buf[i]); // split data frame
this->read_byte(&byte); }
this->r24_split_data_frame_(byte); // split data frame
} }
if ((this->s_output_info_switch_flag_ == OUTPUT_SWTICH_OFF) && if ((this->s_output_info_switch_flag_ == OUTPUT_SWTICH_OFF) &&

View File

@@ -30,14 +30,21 @@ void MR60BHA2Component::dump_config() {
// main loop // main loop
void MR60BHA2Component::loop() { void MR60BHA2Component::loop() {
uint8_t byte; // 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;
// Is there data on the serial port for (size_t i = 0; i < to_read; i++) {
while (this->available()) { this->rx_message_.push_back(buf[i]);
this->read_byte(&byte); if (!this->validate_message_()) {
this->rx_message_.push_back(byte); this->rx_message_.clear();
if (!this->validate_message_()) { }
this->rx_message_.clear();
} }
} }
} }

View File

@@ -49,12 +49,19 @@ void MR60FDA2Component::setup() {
// main loop // main loop
void MR60FDA2Component::loop() { void MR60FDA2Component::loop() {
uint8_t byte; // 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;
// Is there data on the serial port for (size_t i = 0; i < to_read; i++) {
while (this->available()) { this->split_frame_(buf[i]); // split data frame
this->read_byte(&byte); }
this->split_frame_(byte); // split data frame
} }
} }

View File

@@ -9,6 +9,11 @@ namespace esphome::sensor {
static const char *const TAG = "sensor.filter"; static const char *const TAG = "sensor.filter";
// Filter scheduler IDs.
// 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_ID = 0;
// Filter // Filter
void Filter::input(float value) { void Filter::input(float value) {
ESP_LOGVV(TAG, "Filter(%p)::input(%f)", this, value); ESP_LOGVV(TAG, "Filter(%p)::input(%f)", this, value);
@@ -191,7 +196,7 @@ optional<float> ThrottleAverageFilter::new_value(float value) {
return {}; return {};
} }
void ThrottleAverageFilter::setup() { void ThrottleAverageFilter::setup() {
this->set_interval("throttle_average", this->time_period_, [this]() { this->set_interval(FILTER_ID, this->time_period_, [this]() {
ESP_LOGVV(TAG, "ThrottleAverageFilter(%p)::interval(sum=%f, n=%i)", this, this->sum_, this->n_); ESP_LOGVV(TAG, "ThrottleAverageFilter(%p)::interval(sum=%f, n=%i)", this, this->sum_, this->n_);
if (this->n_ == 0) { if (this->n_ == 0) {
if (this->have_nan_) if (this->have_nan_)
@@ -383,7 +388,7 @@ optional<float> TimeoutFilterConfigured::new_value(float value) {
// DebounceFilter // DebounceFilter
optional<float> DebounceFilter::new_value(float value) { optional<float> DebounceFilter::new_value(float value) {
this->set_timeout("debounce", this->time_period_, [this, value]() { this->output(value); }); this->set_timeout(FILTER_ID, this->time_period_, [this, value]() { this->output(value); });
return {}; return {};
} }
@@ -406,7 +411,7 @@ optional<float> HeartbeatFilter::new_value(float value) {
} }
void HeartbeatFilter::setup() { void HeartbeatFilter::setup() {
this->set_interval("heartbeat", this->time_period_, [this]() { this->set_interval(FILTER_ID, this->time_period_, [this]() {
ESP_LOGVV(TAG, "HeartbeatFilter(%p)::interval(has_value=%s, last_input=%f)", this, YESNO(this->has_value_), ESP_LOGVV(TAG, "HeartbeatFilter(%p)::interval(has_value=%s, last_input=%f)", this, YESNO(this->has_value_),
this->last_input_); this->last_input_);
if (!this->has_value_) if (!this->has_value_)

View File

@@ -16,19 +16,13 @@ namespace esphome::socket {
class BSDSocketImpl final : public Socket { class BSDSocketImpl final : public Socket {
public: public:
BSDSocketImpl(int fd, bool monitor_loop = false) : fd_(fd) { BSDSocketImpl(int fd, bool monitor_loop = false) {
#ifdef USE_SOCKET_SELECT_SUPPORT this->fd_ = fd;
// Register new socket with the application for select() if monitoring requested // Register new socket with the application for select() if monitoring requested
if (monitor_loop && this->fd_ >= 0) { if (monitor_loop && this->fd_ >= 0) {
// Only set loop_monitored_ to true if registration succeeds // Only set loop_monitored_ to true if registration succeeds
this->loop_monitored_ = App.register_socket_fd(this->fd_); this->loop_monitored_ = App.register_socket_fd(this->fd_);
} else {
this->loop_monitored_ = false;
} }
#else
// Without select support, ignore monitor_loop parameter
(void) monitor_loop;
#endif
} }
~BSDSocketImpl() override { ~BSDSocketImpl() override {
if (!this->closed_) { if (!this->closed_) {
@@ -52,12 +46,10 @@ class BSDSocketImpl final : public Socket {
int bind(const struct sockaddr *addr, socklen_t addrlen) override { return ::bind(this->fd_, addr, addrlen); } int bind(const struct sockaddr *addr, socklen_t addrlen) override { return ::bind(this->fd_, addr, addrlen); }
int close() override { int close() override {
if (!this->closed_) { if (!this->closed_) {
#ifdef USE_SOCKET_SELECT_SUPPORT
// Unregister from select() before closing if monitored // Unregister from select() before closing if monitored
if (this->loop_monitored_) { if (this->loop_monitored_) {
App.unregister_socket_fd(this->fd_); App.unregister_socket_fd(this->fd_);
} }
#endif
int ret = ::close(this->fd_); int ret = ::close(this->fd_);
this->closed_ = true; this->closed_ = true;
return ret; return ret;
@@ -130,23 +122,6 @@ class BSDSocketImpl final : public Socket {
::fcntl(this->fd_, F_SETFL, fl); ::fcntl(this->fd_, F_SETFL, fl);
return 0; return 0;
} }
int get_fd() const override { return this->fd_; }
#ifdef USE_SOCKET_SELECT_SUPPORT
bool ready() const override {
if (!this->loop_monitored_)
return true;
return App.is_socket_ready(this->fd_);
}
#endif
protected:
int fd_;
bool closed_{false};
#ifdef USE_SOCKET_SELECT_SUPPORT
bool loop_monitored_{false};
#endif
}; };
// Helper to create a socket with optional monitoring // Helper to create a socket with optional monitoring

View File

@@ -452,6 +452,8 @@ class LWIPRawImpl : public Socket {
errno = ENOSYS; errno = ENOSYS;
return -1; return -1;
} }
bool ready() const override { return this->rx_buf_ != nullptr || this->rx_closed_ || this->pcb_ == nullptr; }
int setblocking(bool blocking) final { int setblocking(bool blocking) final {
if (pcb_ == nullptr) { if (pcb_ == nullptr) {
errno = ECONNRESET; errno = ECONNRESET;
@@ -576,6 +578,8 @@ class LWIPRawListenImpl final : public LWIPRawImpl {
tcp_err(pcb_, LWIPRawImpl::s_err_fn); // Use base class error handler tcp_err(pcb_, LWIPRawImpl::s_err_fn); // Use base class error handler
} }
bool ready() const override { return this->accepted_socket_count_ > 0; }
std::unique_ptr<Socket> accept(struct sockaddr *addr, socklen_t *addrlen) override { std::unique_ptr<Socket> accept(struct sockaddr *addr, socklen_t *addrlen) override {
if (pcb_ == nullptr) { if (pcb_ == nullptr) {
errno = EBADF; errno = EBADF;

View File

@@ -11,19 +11,13 @@ namespace esphome::socket {
class LwIPSocketImpl final : public Socket { class LwIPSocketImpl final : public Socket {
public: public:
LwIPSocketImpl(int fd, bool monitor_loop = false) : fd_(fd) { LwIPSocketImpl(int fd, bool monitor_loop = false) {
#ifdef USE_SOCKET_SELECT_SUPPORT this->fd_ = fd;
// Register new socket with the application for select() if monitoring requested // Register new socket with the application for select() if monitoring requested
if (monitor_loop && this->fd_ >= 0) { if (monitor_loop && this->fd_ >= 0) {
// Only set loop_monitored_ to true if registration succeeds // Only set loop_monitored_ to true if registration succeeds
this->loop_monitored_ = App.register_socket_fd(this->fd_); this->loop_monitored_ = App.register_socket_fd(this->fd_);
} else {
this->loop_monitored_ = false;
} }
#else
// Without select support, ignore monitor_loop parameter
(void) monitor_loop;
#endif
} }
~LwIPSocketImpl() override { ~LwIPSocketImpl() override {
if (!this->closed_) { if (!this->closed_) {
@@ -49,12 +43,10 @@ class LwIPSocketImpl final : public Socket {
int bind(const struct sockaddr *addr, socklen_t addrlen) override { return lwip_bind(this->fd_, addr, addrlen); } int bind(const struct sockaddr *addr, socklen_t addrlen) override { return lwip_bind(this->fd_, addr, addrlen); }
int close() override { int close() override {
if (!this->closed_) { if (!this->closed_) {
#ifdef USE_SOCKET_SELECT_SUPPORT
// Unregister from select() before closing if monitored // Unregister from select() before closing if monitored
if (this->loop_monitored_) { if (this->loop_monitored_) {
App.unregister_socket_fd(this->fd_); App.unregister_socket_fd(this->fd_);
} }
#endif
int ret = lwip_close(this->fd_); int ret = lwip_close(this->fd_);
this->closed_ = true; this->closed_ = true;
return ret; return ret;
@@ -97,23 +89,6 @@ class LwIPSocketImpl final : public Socket {
lwip_fcntl(this->fd_, F_SETFL, fl); lwip_fcntl(this->fd_, F_SETFL, fl);
return 0; return 0;
} }
int get_fd() const override { return this->fd_; }
#ifdef USE_SOCKET_SELECT_SUPPORT
bool ready() const override {
if (!this->loop_monitored_)
return true;
return App.is_socket_ready(this->fd_);
}
#endif
protected:
int fd_;
bool closed_{false};
#ifdef USE_SOCKET_SELECT_SUPPORT
bool loop_monitored_{false};
#endif
}; };
// Helper to create a socket with optional monitoring // Helper to create a socket with optional monitoring

View File

@@ -10,6 +10,10 @@ namespace esphome::socket {
Socket::~Socket() {} Socket::~Socket() {}
#ifdef USE_SOCKET_SELECT_SUPPORT
bool Socket::ready() const { return !this->loop_monitored_ || App.is_socket_ready_(this->fd_); }
#endif
// Platform-specific inet_ntop wrappers // Platform-specific inet_ntop wrappers
#if defined(USE_SOCKET_IMPL_LWIP_TCP) #if defined(USE_SOCKET_IMPL_LWIP_TCP)
// LWIP raw TCP (ESP8266) uses inet_ntoa_r which takes struct by value // LWIP raw TCP (ESP8266) uses inet_ntoa_r which takes struct by value

View File

@@ -63,13 +63,29 @@ class Socket {
virtual int setblocking(bool blocking) = 0; virtual int setblocking(bool blocking) = 0;
virtual int loop() { return 0; }; virtual int loop() { return 0; };
/// Get the underlying file descriptor (returns -1 if not supported) /// Get the underlying file descriptor (returns -1 if not supported)
virtual int get_fd() const { return -1; } /// Non-virtual: only one socket implementation is active per build.
#ifdef USE_SOCKET_SELECT_SUPPORT
int get_fd() const { return this->fd_; }
#else
int get_fd() const { return -1; }
#endif
/// Check if socket has data ready to read /// Check if socket has data ready to read
/// For loop-monitored sockets, checks with the Application's select() results /// For select()-based sockets: non-virtual, checks Application's select() results
/// For non-monitored sockets, always returns true (assumes data may be available) /// For LWIP raw TCP sockets: virtual, checks internal buffer state
#ifdef USE_SOCKET_SELECT_SUPPORT
bool ready() const;
#else
virtual bool ready() const { return true; } virtual bool ready() const { return true; }
#endif
protected:
#ifdef USE_SOCKET_SELECT_SUPPORT
int fd_{-1};
bool closed_{false};
bool loop_monitored_{false};
#endif
}; };
/// Create a socket of the given domain, type and protocol. /// Create a socket of the given domain, type and protocol.

View File

@@ -103,6 +103,20 @@ void SpeakerMediaPlayer::set_playlist_delay_ms(AudioPipelineType pipeline_type,
} }
} }
void SpeakerMediaPlayer::stop_and_unpause_media_() {
this->media_pipeline_->stop();
this->unpause_media_remaining_ = 3;
this->set_interval("unpause_med", 50, [this]() {
if (this->media_pipeline_state_ == AudioPipelineState::STOPPED) {
this->cancel_interval("unpause_med");
this->media_pipeline_->set_pause_state(false);
this->is_paused_ = false;
} else if (--this->unpause_media_remaining_ == 0) {
this->cancel_interval("unpause_med");
}
});
}
void SpeakerMediaPlayer::watch_media_commands_() { void SpeakerMediaPlayer::watch_media_commands_() {
if (!this->is_ready()) { if (!this->is_ready()) {
return; return;
@@ -144,15 +158,7 @@ void SpeakerMediaPlayer::watch_media_commands_() {
if (this->is_paused_) { if (this->is_paused_) {
// If paused, stop the media pipeline and unpause it after confirming its stopped. This avoids playing a // If paused, stop the media pipeline and unpause it after confirming its stopped. This avoids playing a
// short segment of the paused file before starting the new one. // short segment of the paused file before starting the new one.
this->media_pipeline_->stop(); this->stop_and_unpause_media_();
this->set_retry("unpause_med", 50, 3, [this](const uint8_t remaining_attempts) {
if (this->media_pipeline_state_ == AudioPipelineState::STOPPED) {
this->media_pipeline_->set_pause_state(false);
this->is_paused_ = false;
return RetryResult::DONE;
}
return RetryResult::RETRY;
});
} else { } else {
// Not paused, just directly start the file // Not paused, just directly start the file
if (media_command.file.has_value()) { if (media_command.file.has_value()) {
@@ -197,27 +203,21 @@ void SpeakerMediaPlayer::watch_media_commands_() {
this->cancel_timeout("next_ann"); this->cancel_timeout("next_ann");
this->announcement_playlist_.clear(); this->announcement_playlist_.clear();
this->announcement_pipeline_->stop(); this->announcement_pipeline_->stop();
this->set_retry("unpause_ann", 50, 3, [this](const uint8_t remaining_attempts) { this->unpause_announcement_remaining_ = 3;
this->set_interval("unpause_ann", 50, [this]() {
if (this->announcement_pipeline_state_ == AudioPipelineState::STOPPED) { if (this->announcement_pipeline_state_ == AudioPipelineState::STOPPED) {
this->cancel_interval("unpause_ann");
this->announcement_pipeline_->set_pause_state(false); this->announcement_pipeline_->set_pause_state(false);
return RetryResult::DONE; } else if (--this->unpause_announcement_remaining_ == 0) {
this->cancel_interval("unpause_ann");
} }
return RetryResult::RETRY;
}); });
} }
} else { } else {
if (this->media_pipeline_ != nullptr) { if (this->media_pipeline_ != nullptr) {
this->cancel_timeout("next_media"); this->cancel_timeout("next_media");
this->media_playlist_.clear(); this->media_playlist_.clear();
this->media_pipeline_->stop(); this->stop_and_unpause_media_();
this->set_retry("unpause_med", 50, 3, [this](const uint8_t remaining_attempts) {
if (this->media_pipeline_state_ == AudioPipelineState::STOPPED) {
this->media_pipeline_->set_pause_state(false);
this->is_paused_ = false;
return RetryResult::DONE;
}
return RetryResult::RETRY;
});
} }
} }

View File

@@ -112,6 +112,9 @@ class SpeakerMediaPlayer : public Component,
/// media pipelines are defined. /// media pipelines are defined.
inline bool single_pipeline_() { return (this->media_speaker_ == nullptr); } inline bool single_pipeline_() { return (this->media_speaker_ == nullptr); }
/// Stops the media pipeline and polls until stopped to unpause it, avoiding an audible glitch.
void stop_and_unpause_media_();
// Processes commands from media_control_command_queue_. // Processes commands from media_control_command_queue_.
void watch_media_commands_(); void watch_media_commands_();
@@ -141,6 +144,8 @@ class SpeakerMediaPlayer : public Component,
bool is_paused_{false}; bool is_paused_{false};
bool is_muted_{false}; bool is_muted_{false};
uint8_t unpause_media_remaining_{0};
uint8_t unpause_announcement_remaining_{0};
// The amount to change the volume on volume up/down commands // The amount to change the volume on volume up/down commands
float volume_increment_; float volume_increment_;

View File

@@ -3,6 +3,7 @@ import esphome.codegen as cg
from esphome.components import water_heater from esphome.components import water_heater
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_AWAY,
CONF_ID, CONF_ID,
CONF_MODE, CONF_MODE,
CONF_OPTIMISTIC, CONF_OPTIMISTIC,
@@ -18,6 +19,7 @@ from esphome.types import ConfigType
from .. import template_ns from .. import template_ns
CONF_CURRENT_TEMPERATURE = "current_temperature" CONF_CURRENT_TEMPERATURE = "current_temperature"
CONF_IS_ON = "is_on"
TemplateWaterHeater = template_ns.class_( TemplateWaterHeater = template_ns.class_(
"TemplateWaterHeater", cg.Component, water_heater.WaterHeater "TemplateWaterHeater", cg.Component, water_heater.WaterHeater
@@ -51,6 +53,8 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list( cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(
water_heater.validate_water_heater_mode water_heater.validate_water_heater_mode
), ),
cv.Optional(CONF_AWAY): cv.returning_lambda,
cv.Optional(CONF_IS_ON): cv.returning_lambda,
} }
) )
.extend(cv.COMPONENT_SCHEMA) .extend(cv.COMPONENT_SCHEMA)
@@ -98,6 +102,22 @@ async def to_code(config: ConfigType) -> None:
if CONF_SUPPORTED_MODES in config: if CONF_SUPPORTED_MODES in config:
cg.add(var.set_supported_modes(config[CONF_SUPPORTED_MODES])) cg.add(var.set_supported_modes(config[CONF_SUPPORTED_MODES]))
if CONF_AWAY in config:
template_ = await cg.process_lambda(
config[CONF_AWAY],
[],
return_type=cg.optional.template(bool),
)
cg.add(var.set_away_lambda(template_))
if CONF_IS_ON in config:
template_ = await cg.process_lambda(
config[CONF_IS_ON],
[],
return_type=cg.optional.template(bool),
)
cg.add(var.set_is_on_lambda(template_))
@automation.register_action( @automation.register_action(
"water_heater.template.publish", "water_heater.template.publish",
@@ -110,6 +130,8 @@ async def to_code(config: ConfigType) -> None:
cv.Optional(CONF_MODE): cv.templatable( cv.Optional(CONF_MODE): cv.templatable(
water_heater.validate_water_heater_mode water_heater.validate_water_heater_mode
), ),
cv.Optional(CONF_AWAY): cv.templatable(cv.boolean),
cv.Optional(CONF_IS_ON): cv.templatable(cv.boolean),
} }
), ),
) )
@@ -134,4 +156,12 @@ async def water_heater_template_publish_to_code(
template_ = await cg.templatable(mode, args, water_heater.WaterHeaterMode) template_ = await cg.templatable(mode, args, water_heater.WaterHeaterMode)
cg.add(var.set_mode(template_)) cg.add(var.set_mode(template_))
if CONF_AWAY in config:
template_ = await cg.templatable(config[CONF_AWAY], args, bool)
cg.add(var.set_away(template_))
if CONF_IS_ON in config:
template_ = await cg.templatable(config[CONF_IS_ON], args, bool)
cg.add(var.set_is_on(template_))
return var return var

View File

@@ -11,12 +11,15 @@ class TemplateWaterHeaterPublishAction : public Action<Ts...>, public Parented<T
TEMPLATABLE_VALUE(float, current_temperature) TEMPLATABLE_VALUE(float, current_temperature)
TEMPLATABLE_VALUE(float, target_temperature) TEMPLATABLE_VALUE(float, target_temperature)
TEMPLATABLE_VALUE(water_heater::WaterHeaterMode, mode) TEMPLATABLE_VALUE(water_heater::WaterHeaterMode, mode)
TEMPLATABLE_VALUE(bool, away)
TEMPLATABLE_VALUE(bool, is_on)
void play(const Ts &...x) override { void play(const Ts &...x) override {
if (this->current_temperature_.has_value()) { if (this->current_temperature_.has_value()) {
this->parent_->set_current_temperature(this->current_temperature_.value(x...)); this->parent_->set_current_temperature(this->current_temperature_.value(x...));
} }
bool needs_call = this->target_temperature_.has_value() || this->mode_.has_value(); bool needs_call = this->target_temperature_.has_value() || this->mode_.has_value() || this->away_.has_value() ||
this->is_on_.has_value();
if (needs_call) { if (needs_call) {
auto call = this->parent_->make_call(); auto call = this->parent_->make_call();
if (this->target_temperature_.has_value()) { if (this->target_temperature_.has_value()) {
@@ -25,6 +28,12 @@ class TemplateWaterHeaterPublishAction : public Action<Ts...>, public Parented<T
if (this->mode_.has_value()) { if (this->mode_.has_value()) {
call.set_mode(this->mode_.value(x...)); call.set_mode(this->mode_.value(x...));
} }
if (this->away_.has_value()) {
call.set_away(this->away_.value(x...));
}
if (this->is_on_.has_value()) {
call.set_on(this->is_on_.value(x...));
}
call.perform(); call.perform();
} else { } else {
this->parent_->publish_state(); this->parent_->publish_state();

View File

@@ -17,7 +17,7 @@ void TemplateWaterHeater::setup() {
} }
} }
if (!this->current_temperature_f_.has_value() && !this->target_temperature_f_.has_value() && if (!this->current_temperature_f_.has_value() && !this->target_temperature_f_.has_value() &&
!this->mode_f_.has_value()) !this->mode_f_.has_value() && !this->away_f_.has_value() && !this->is_on_f_.has_value())
this->disable_loop(); this->disable_loop();
} }
@@ -32,6 +32,12 @@ water_heater::WaterHeaterTraits TemplateWaterHeater::traits() {
if (this->target_temperature_f_.has_value()) { if (this->target_temperature_f_.has_value()) {
traits.add_feature_flags(water_heater::WATER_HEATER_SUPPORTS_TARGET_TEMPERATURE); traits.add_feature_flags(water_heater::WATER_HEATER_SUPPORTS_TARGET_TEMPERATURE);
} }
if (this->away_f_.has_value()) {
traits.set_supports_away_mode(true);
}
if (this->is_on_f_.has_value()) {
traits.add_feature_flags(water_heater::WATER_HEATER_SUPPORTS_ON_OFF);
}
return traits; return traits;
} }
@@ -62,6 +68,22 @@ void TemplateWaterHeater::loop() {
} }
} }
auto away = this->away_f_.call();
if (away.has_value()) {
if (*away != this->is_away()) {
this->set_state_flag_(water_heater::WATER_HEATER_STATE_AWAY, *away);
changed = true;
}
}
auto is_on = this->is_on_f_.call();
if (is_on.has_value()) {
if (*is_on != this->is_on()) {
this->set_state_flag_(water_heater::WATER_HEATER_STATE_ON, *is_on);
changed = true;
}
}
if (changed) { if (changed) {
this->publish_state(); this->publish_state();
} }
@@ -90,6 +112,17 @@ void TemplateWaterHeater::control(const water_heater::WaterHeaterCall &call) {
} }
} }
if (call.get_away().has_value()) {
if (this->optimistic_) {
this->set_state_flag_(water_heater::WATER_HEATER_STATE_AWAY, *call.get_away());
}
}
if (call.get_on().has_value()) {
if (this->optimistic_) {
this->set_state_flag_(water_heater::WATER_HEATER_STATE_ON, *call.get_on());
}
}
this->set_trigger_.trigger(); this->set_trigger_.trigger();
if (this->optimistic_) { if (this->optimistic_) {

View File

@@ -24,6 +24,8 @@ class TemplateWaterHeater : public Component, public water_heater::WaterHeater {
this->target_temperature_f_.set(std::forward<F>(f)); this->target_temperature_f_.set(std::forward<F>(f));
} }
template<typename F> void set_mode_lambda(F &&f) { this->mode_f_.set(std::forward<F>(f)); } template<typename F> void set_mode_lambda(F &&f) { this->mode_f_.set(std::forward<F>(f)); }
template<typename F> void set_away_lambda(F &&f) { this->away_f_.set(std::forward<F>(f)); }
template<typename F> void set_is_on_lambda(F &&f) { this->is_on_f_.set(std::forward<F>(f)); }
void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
void set_restore_mode(TemplateWaterHeaterRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } void set_restore_mode(TemplateWaterHeaterRestoreMode restore_mode) { this->restore_mode_ = restore_mode; }
@@ -49,6 +51,8 @@ class TemplateWaterHeater : public Component, public water_heater::WaterHeater {
TemplateLambda<float> current_temperature_f_; TemplateLambda<float> current_temperature_f_;
TemplateLambda<float> target_temperature_f_; TemplateLambda<float> target_temperature_f_;
TemplateLambda<water_heater::WaterHeaterMode> mode_f_; TemplateLambda<water_heater::WaterHeaterMode> mode_f_;
TemplateLambda<bool> away_f_;
TemplateLambda<bool> is_on_f_;
TemplateWaterHeaterRestoreMode restore_mode_{WATER_HEATER_NO_RESTORE}; TemplateWaterHeaterRestoreMode restore_mode_{WATER_HEATER_NO_RESTORE};
water_heater::WaterHeaterModeMask supported_modes_; water_heater::WaterHeaterModeMask supported_modes_;
bool optimistic_{true}; bool optimistic_{true};

View File

@@ -251,7 +251,7 @@ void Tormatic::stop_at_target_() {
// Read a GateStatus from the unit. The unit only sends messages in response to // Read a GateStatus from the unit. The unit only sends messages in response to
// status requests or commands, so a message needs to be sent first. // status requests or commands, so a message needs to be sent first.
optional<GateStatus> Tormatic::read_gate_status_() { optional<GateStatus> Tormatic::read_gate_status_() {
if (this->available() < static_cast<int>(sizeof(MessageHeader))) { if (this->available() < sizeof(MessageHeader)) {
return {}; return {};
} }

View File

@@ -31,10 +31,19 @@ void Tuya::setup() {
} }
void Tuya::loop() { void Tuya::loop() {
while (this->available()) { // Read all available bytes in batches to reduce UART call overhead.
uint8_t c; size_t avail = this->available();
this->read_byte(&c); uint8_t buf[64];
this->handle_char_(c); 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->handle_char_(buf[i]);
}
} }
process_command_queue_(); process_command_queue_();
} }

View File

@@ -3,12 +3,16 @@
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include <cinttypes> #include <cinttypes>
namespace esphome::uart { namespace esphome::uart {
static const char *const TAG = "uart"; static const char *const TAG = "uart";
// UART parity strings indexed by UARTParityOptions enum (0-2): NONE, EVEN, ODD
PROGMEM_STRING_TABLE(UARTParityStrings, "NONE", "EVEN", "ODD", "UNKNOWN");
void UARTDevice::check_uart_settings(uint32_t baud_rate, uint8_t stop_bits, UARTParityOptions parity, void UARTDevice::check_uart_settings(uint32_t baud_rate, uint8_t stop_bits, UARTParityOptions parity,
uint8_t data_bits) { uint8_t data_bits) {
if (this->parent_->get_baud_rate() != baud_rate) { if (this->parent_->get_baud_rate() != baud_rate) {
@@ -30,16 +34,7 @@ void UARTDevice::check_uart_settings(uint32_t baud_rate, uint8_t stop_bits, UART
} }
const LogString *parity_to_str(UARTParityOptions parity) { const LogString *parity_to_str(UARTParityOptions parity) {
switch (parity) { return UARTParityStrings::get_log_str(static_cast<uint8_t>(parity), UARTParityStrings::LAST_INDEX);
case UART_CONFIG_PARITY_NONE:
return LOG_STR("NONE");
case UART_CONFIG_PARITY_EVEN:
return LOG_STR("EVEN");
case UART_CONFIG_PARITY_ODD:
return LOG_STR("ODD");
default:
return LOG_STR("UNKNOWN");
}
} }
} // namespace esphome::uart } // namespace esphome::uart

View File

@@ -43,7 +43,7 @@ class UARTDevice {
return res; return res;
} }
int available() { return this->parent_->available(); } size_t available() { return this->parent_->available(); }
void flush() { this->parent_->flush(); } void flush() { this->parent_->flush(); }

View File

@@ -5,13 +5,13 @@ namespace esphome::uart {
static const char *const TAG = "uart"; static const char *const TAG = "uart";
bool UARTComponent::check_read_timeout_(size_t len) { bool UARTComponent::check_read_timeout_(size_t len) {
if (this->available() >= int(len)) if (this->available() >= len)
return true; return true;
uint32_t start_time = millis(); uint32_t start_time = millis();
while (this->available() < int(len)) { while (this->available() < len) {
if (millis() - start_time > 100) { if (millis() - start_time > 100) {
ESP_LOGE(TAG, "Reading from UART timed out at byte %u!", this->available()); ESP_LOGE(TAG, "Reading from UART timed out at byte %zu!", this->available());
return false; return false;
} }
yield(); yield();

View File

@@ -69,7 +69,7 @@ class UARTComponent {
// Pure virtual method to return the number of bytes available for reading. // Pure virtual method to return the number of bytes available for reading.
// @return Number of available bytes. // @return Number of available bytes.
virtual int available() = 0; virtual size_t available() = 0;
// Pure virtual method to block until all bytes have been written to the UART bus. // Pure virtual method to block until all bytes have been written to the UART bus.
virtual void flush() = 0; virtual void flush() = 0;

View File

@@ -206,7 +206,7 @@ bool ESP8266UartComponent::read_array(uint8_t *data, size_t len) {
#endif #endif
return true; return true;
} }
int ESP8266UartComponent::available() { size_t ESP8266UartComponent::available() {
if (this->hw_serial_ != nullptr) { if (this->hw_serial_ != nullptr) {
return this->hw_serial_->available(); return this->hw_serial_->available();
} else { } else {
@@ -329,11 +329,14 @@ uint8_t ESP8266SoftwareSerial::peek_byte() {
void ESP8266SoftwareSerial::flush() { void ESP8266SoftwareSerial::flush() {
// Flush is a NO-OP with software serial, all bytes are written immediately. // Flush is a NO-OP with software serial, all bytes are written immediately.
} }
int ESP8266SoftwareSerial::available() { size_t ESP8266SoftwareSerial::available() {
int avail = int(this->rx_in_pos_) - int(this->rx_out_pos_); // Read volatile rx_in_pos_ once to avoid TOCTOU race with ISR.
if (avail < 0) // When in >= out, data is contiguous: [out..in).
return avail + this->rx_buffer_size_; // When in < out, data wraps: [out..buf_size) + [0..in).
return avail; size_t in = this->rx_in_pos_;
if (in >= this->rx_out_pos_)
return in - this->rx_out_pos_;
return this->rx_buffer_size_ - this->rx_out_pos_ + in;
} }
} // namespace esphome::uart } // namespace esphome::uart

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