Compare commits

...

20 Commits

Author SHA1 Message Date
J. Nick Koston b4ad0eb86b [esp32_ble] Fix boot loop when the hosted co-processor does not answer BT bring-up (#17429) 2026-07-07 14:59:18 +00:00
Clyde Stubbs 7f0e826c32 [lvgl] Add paused option to suppress updates on boot (#16973)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-07-07 14:50:36 +00:00
Remco van Essen 76ee3fe887 [audio_file] Accept mp1/mp2 puremagic detections as MP3 (#17436)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-07 08:56:48 -04:00
Oliver Kleinecke af4a6e7ec3 [usb_uart] Fix FTDI RX data stall / corruption and input restart reliability (#17348)
Co-authored-by: Oliver Kleinecke <kleinecke.oliver@googlemail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 21:55:49 +10:00
luar123 40c3a4320f [core] add const for litre per hour (#17389)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-07 21:44:44 +10:00
J. Nick Koston 3c2dad67f4 [network] Fix logged use_address with MAC suffix and build it at runtime (#17432) 2026-07-07 02:07:34 -05:00
Remco van Essen f823a23ea4 [pcm5122] Add analog gain, channel mixing, volume range, standby/powerdown switch, and XSMT enable pin support (#17313) 2026-07-07 00:15:37 -05:00
Keith Burzinski 9aed1d2700 [esp32] Add NVS encryption (HMAC scheme) (#17004) 2026-07-07 16:04:38 +12:00
Clyde Stubbs 9857d508d9 [light] Preserve brightness on turn-off. (#17103)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-07 15:57:06 +12:00
Clyde Stubbs ad7c980c4b [lvgl] Continue activity while display busy (#17374) 2026-07-07 03:16:29 +00:00
J. Nick Koston e8d37e5bd3 [libretiny] Use standard logger tag names (#17431) 2026-07-06 20:55:12 -04:00
J. Nick Koston a36c3063b2 [web_server] Use known message length in SSE send path (#17400) 2026-07-06 20:55:02 -04:00
J. Nick Koston 902cf6a679 [analyze_memory] Report aliased RAM symbols once in the RAM strings report (#17397) 2026-07-06 20:54:53 -04:00
J. Nick Koston a10e005bb4 [esp8266] Strip lwIP glue dhcp stub message strings from DRAM (#17395) 2026-07-06 20:54:44 -04:00
Keith Burzinski d9998eff20 [esp32] Add software OTA downgrade protection (#17315)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 20:39:18 -04:00
Clyde Stubbs 2dd7ac090f [mipi_spi] Add M5STACK ATOM3SR display (#17344) 2026-07-06 20:37:44 -04:00
Jesse Hills c4689989c7 Mark configurable classes as final (20/21: wts01-zephyr_ble_server) (#16971) 2026-07-06 19:42:18 -04:00
Bonne Eggleston ebff49072e [modbus] Store ModbusFrame inline to cut per-frame heap churn (#17282)
Co-authored-by: Claude <noreply@anthropic.com>
2026-07-06 23:12:25 +00:00
esphome[bot] 0b311962b5 Bump bundled esphome-device-builder to 1.2.0 (#17430) 2026-07-06 17:09:51 -05:00
Jonathan Swoboda b22def399f [espnow] Add max_payload_size option for ESP-NOW v2 frames (#17360) 2026-07-06 17:08:56 -04:00
109 changed files with 1697 additions and 335 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ RUN \
-r /requirements.txt
# Install the ESPHome Device Builder dashboard.
RUN uv pip install --no-cache-dir esphome-device-builder==1.1.0
RUN uv pip install --no-cache-dir esphome-device-builder==1.2.0
RUN \
platformio settings set enable_telemetry No \
+34 -10
View File
@@ -8,7 +8,7 @@ memory-constrained platforms like ESP8266.
from __future__ import annotations
from collections import defaultdict
from dataclasses import dataclass
from dataclasses import dataclass, field
import logging
from pathlib import Path
import re
@@ -65,6 +65,7 @@ class RamSymbol:
size: int
section: str
demangled: str = "" # Demangled name, set after batch demangling
aliases: list[str] = field(default_factory=list) # Other names at same address
class RamStringsAnalyzer:
@@ -235,6 +236,11 @@ class RamStringsAnalyzer:
except (subprocess.CalledProcessError, FileNotFoundError):
return
# Track symbols by address so aliases (multiple names for the same
# object, e.g. the newlib __lock___* mutexes that all alias one
# StaticSemaphore_t) are reported once instead of once per name.
symbols_by_addr: dict[int, RamSymbol] = {}
for line in output.split("\n"):
parts = line.split()
if len(parts) < 4:
@@ -253,6 +259,18 @@ class RamStringsAnalyzer:
if sym_type not in DATA_SYMBOL_TYPES:
continue
if (existing := symbols_by_addr.get(addr)) is not None:
# Prefer a global (uppercase type) name as the primary so
# nm output order can't hide it behind a local alias.
if sym_type.isupper() and existing.sym_type.islower():
existing.aliases.append(existing.name)
existing.name = name
existing.sym_type = sym_type
else:
existing.aliases.append(name)
existing.size = max(existing.size, size)
continue
# Check if symbol is in a RAM section
for section_name in self.ram_sections:
if section_name not in self.sections:
@@ -260,15 +278,15 @@ class RamStringsAnalyzer:
section = self.sections[section_name]
if section.address <= addr < section.address + section.size:
self.ram_symbols.append(
RamSymbol(
name=name,
sym_type=sym_type,
address=addr,
size=size,
section=section_name,
)
symbol = RamSymbol(
name=name,
sym_type=sym_type,
address=addr,
size=size,
section=section_name,
)
symbols_by_addr[addr] = symbol
self.ram_symbols.append(symbol)
break
def _demangle_symbols(self) -> None:
@@ -436,7 +454,13 @@ class RamStringsAnalyzer:
for symbol in largest_symbols:
# Use demangled name if available, otherwise raw name
display_name = symbol.demangled or symbol.name
name_display = display_name[:49] if len(display_name) > 49 else display_name
# Truncate the name, not the alias note, so merged aliases stay
# visible even for long demangled C++ names.
alias_note = f" (+{len(symbol.aliases)} aliases)" if symbol.aliases else ""
max_name_len = 49 - len(alias_note)
if len(display_name) > max_name_len:
display_name = display_name[:max_name_len]
name_display = display_name + alias_note
lines.append(
f"{name_display:<50} {symbol.sym_type:<6} {symbol.size:>8} B {symbol.section}"
)
+2 -1
View File
@@ -240,12 +240,13 @@ void __attribute__((flatten)) APIServer::accept_new_connections_() {
}
void APIServer::dump_config() {
char addr_buf[network::USE_ADDRESS_BUFFER_SIZE];
ESP_LOGCONFIG(TAG,
"Server:\n"
" Address: %s:%u\n"
" Listen backlog: %u\n"
" Max connections: %u",
network::get_use_address(), this->port_, this->listen_backlog_, MAX_API_CONNECTIONS);
network::get_use_address_to(addr_buf), this->port_, this->listen_backlog_, MAX_API_CONNECTIONS);
#ifdef USE_API_NOISE
ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_.has_psk()));
if (!this->noise_ctx_.has_psk()) {
+3 -1
View File
@@ -113,7 +113,9 @@ def read_audio_file_and_type(file_config: ConfigType) -> tuple[bytes, MockObj]:
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["NONE"]
if file_type == "wav":
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["WAV"]
elif file_type in ("mp3", "mpeg", "mpga"):
elif file_type in ("mp1", "mp2", "mp3", "mpeg", "mpga"):
# With puremagic >=2.0 this can cause some MP3 (Layer III) files to be labeled as "mp1"/"mp2".
# Treat those labels as MP3 so we still pick the MP3 decoder.
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["MP3"]
elif file_type == "flac":
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["FLAC"]
+1
View File
@@ -16,6 +16,7 @@ CONF_COLOR_DEPTH = "color_depth"
CONF_CRC_ENABLE = "crc_enable"
CONF_DATA_BITS = "data_bits"
CONF_DRAW_ROUNDING = "draw_rounding"
CONF_ENABLE_OTA_DOWNGRADE_PROTECTION = "enable_ota_downgrade_protection"
CONF_ENABLED = "enabled"
CONF_GYROSCOPE_ODR = "gyroscope_odr"
CONF_GYROSCOPE_RANGE = "gyroscope_range"
+129
View File
@@ -11,6 +11,7 @@ from typing import Any
from esphome import yaml_util
import esphome.codegen as cg
from esphome.components.const import CONF_ENABLE_OTA_DOWNGRADE_PROTECTION
import esphome.config_validation as cv
from esphome.const import (
CONF_ADVANCED,
@@ -29,6 +30,7 @@ from esphome.const import (
CONF_PATH,
CONF_PLATFORM_VERSION,
CONF_PLATFORMIO_OPTIONS,
CONF_PROJECT,
CONF_REF,
CONF_SAFE_MODE,
CONF_SIZE,
@@ -107,7 +109,9 @@ CONF_ENGINEERING_SAMPLE = "engineering_sample"
CONF_INCLUDE_BUILTIN_IDF_COMPONENTS = "include_builtin_idf_components"
CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert"
CONF_EXECUTE_FROM_PSRAM = "execute_from_psram"
CONF_KEY_ID = "key_id"
CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision"
CONF_NVS_ENCRYPTION = "nvs_encryption"
CONF_RELEASE = "release"
CONF_SIGNED_OTA_VERIFICATION = "signed_ota_verification"
CONF_SIGNING_KEY = "signing_key"
@@ -165,6 +169,20 @@ SIGNED_OTA_V1_ECDSA_VARIANTS = {
VARIANT_ESP32,
}
# NVS encryption (HMAC peripheral scheme) is only available on variants that
# expose the HMAC peripheral (SOC_HMAC_SUPPORTED in soc_caps.h). The original
# ESP32 and ESP32-C2 do not have it. New variants with an HMAC peripheral
# should be added here.
NVS_ENCRYPTION_HMAC_VARIANTS = {
VARIANT_ESP32S2,
VARIANT_ESP32S3,
VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6,
VARIANT_ESP32H2,
VARIANT_ESP32P4,
}
COMPILER_OPTIMIZATIONS = {
"DEBUG": "CONFIG_COMPILER_OPTIMIZATION_DEBUG",
"NONE": "CONFIG_COMPILER_OPTIMIZATION_NONE",
@@ -1098,6 +1116,50 @@ def _detect_variant(value):
return value
def _ota_downgrade_protection_errors(
project_version: str | None, signed_ota_enabled: bool
) -> list[cv.Invalid]:
"""Validate prerequisites for OTA downgrade protection.
Called only when the feature is enabled. Returns a ``cv.Invalid`` for each
unmet requirement: a dotted-numeric project version (the firmware version
compared on-device) and signed OTA (so the embedded version cannot be
forged).
"""
path = [CONF_FRAMEWORK, CONF_ADVANCED, CONF_ENABLE_OTA_DOWNGRADE_PROTECTION]
errs: list[cv.Invalid] = []
if not project_version:
errs.append(
cv.Invalid(
f"'{CONF_ENABLE_OTA_DOWNGRADE_PROTECTION}' requires a "
f"'{CONF_PROJECT}' with a '{CONF_VERSION}' to be set in the "
f"'{CONF_ESPHOME}' section; this version is the firmware version "
"compared during OTA.",
path=path,
)
)
elif not re.fullmatch(r"\d+(\.\d+)*", project_version):
# The on-device comparison parses dotted-numeric versions only.
errs.append(
cv.Invalid(
f"'{CONF_ENABLE_OTA_DOWNGRADE_PROTECTION}' requires the "
f"'{CONF_PROJECT}' '{CONF_VERSION}' to be dotted-numeric (such "
f"as '1.2.3'), got '{project_version}'.",
path=path,
)
)
if not signed_ota_enabled:
errs.append(
cv.Invalid(
f"'{CONF_ENABLE_OTA_DOWNGRADE_PROTECTION}' requires "
f"'{CONF_SIGNED_OTA_VERIFICATION}' to be enabled; without signed "
"OTA the embedded version cannot be trusted.",
path=path,
)
)
return errs
def final_validate(config):
# Imported locally to avoid circular import issues
from esphome.components.psram import DOMAIN as PSRAM_DOMAIN
@@ -1303,6 +1365,37 @@ def final_validate(config):
"Binaries will NOT be signed automatically during build. "
"You must sign them externally before flashing."
)
if (nvs_enc := advanced.get(CONF_NVS_ENCRYPTION)) is not None:
variant = config[CONF_VARIANT]
if variant in NVS_ENCRYPTION_HMAC_VARIANTS:
_LOGGER.warning(
"NVS encryption will burn an HMAC key into eFuse key block %d on the "
"first boot of each device. This is PERMANENT and IRREVERSIBLE: "
"the block cannot be erased or reused afterwards. Enabling (or "
"later disabling) encryption also wipes any previously saved "
"preferences once, because the older data can no longer be read.",
nvs_enc[CONF_KEY_ID],
)
else:
supported = ", ".join(
sorted(VARIANT_FRIENDLY[v] for v in NVS_ENCRYPTION_HMAC_VARIANTS)
)
errs.append(
cv.Invalid(
f"NVS encryption (HMAC scheme) is not supported on "
f"{VARIANT_FRIENDLY[variant]} (it has no HMAC peripheral). "
f"Supported variants: {supported}.",
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_NVS_ENCRYPTION],
)
)
if advanced[CONF_ENABLE_OTA_DOWNGRADE_PROTECTION]:
project = full_config[CONF_ESPHOME].get(CONF_PROJECT)
errs.extend(
_ota_downgrade_protection_errors(
project[CONF_VERSION] if project else None,
bool(advanced.get(CONF_SIGNED_OTA_VERIFICATION)),
)
)
if errs:
raise cv.MultipleInvalid(errs)
@@ -1540,6 +1633,9 @@ FRAMEWORK_SCHEMA = cv.Schema(
min=8192, max=32768
),
cv.Optional(CONF_ENABLE_OTA_ROLLBACK, default=True): cv.boolean,
cv.Optional(
CONF_ENABLE_OTA_DOWNGRADE_PROTECTION, default=False
): cv.boolean,
cv.Optional(CONF_SIGNED_OTA_VERIFICATION): cv.All(
cv.Schema(
{
@@ -1552,6 +1648,15 @@ FRAMEWORK_SCHEMA = cv.Schema(
),
cv.has_exactly_one_key(CONF_SIGNING_KEY, CONF_VERIFICATION_KEY),
),
cv.Optional(CONF_NVS_ENCRYPTION): cv.Schema(
{
# eFuse key block (0-5) that stores the HMAC key from
# which the NVS encryption keys are derived. The block is
# written on first boot if empty -- an irreversible
# operation -- so it must be chosen explicitly.
cv.Required(CONF_KEY_ID): cv.int_range(min=0, max=5),
}
),
cv.Optional(
CONF_USE_FULL_CERTIFICATE_BUNDLE, default=False
): cv.boolean,
@@ -2358,6 +2463,16 @@ async def to_code(config):
add_idf_sdkconfig_option("CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE", True)
cg.add_define("USE_OTA_ROLLBACK")
# Enable software OTA downgrade protection. Embed the project version into
# the image's esp_app_desc_t so the OTA backend can compare it against the
# running version (final_validate guarantees a dotted-numeric project
# version and that signed OTA is enabled).
if advanced[CONF_ENABLE_OTA_DOWNGRADE_PROTECTION]:
project_version = CORE.config[CONF_ESPHOME][CONF_PROJECT][CONF_VERSION]
add_idf_sdkconfig_option("CONFIG_APP_PROJECT_VER_FROM_CONFIG", True)
add_idf_sdkconfig_option("CONFIG_APP_PROJECT_VER", project_version)
cg.add_define("USE_OTA_DOWNGRADE_PROTECTION")
# Enable signed app verification without hardware secure boot
if signed_ota := advanced.get(CONF_SIGNED_OTA_VERIFICATION):
add_idf_sdkconfig_option("CONFIG_SECURE_SIGNED_APPS_NO_SECURE_BOOT", True)
@@ -2384,6 +2499,20 @@ async def to_code(config):
cg.add_define("USE_OTA_SIGNED_VERIFICATION")
# Encrypt NVS using the HMAC peripheral scheme. The NVS encryption keys are
# derived at runtime from an HMAC key stored in the configured eFuse block
# (no flash encryption required). The HMAC key is generated and burned into
# the eFuse block on first boot if it is empty. With the scheme selected,
# nvs_sec_provider registers it at startup and the default nvs_flash_init()
# (used in esp32/preferences.cpp) transparently performs the secure init, so
# no C++ changes are needed.
if (nvs_enc := advanced.get(CONF_NVS_ENCRYPTION)) is not None:
add_idf_sdkconfig_option("CONFIG_NVS_ENCRYPTION", True)
add_idf_sdkconfig_option("CONFIG_NVS_SEC_KEY_PROTECT_USING_HMAC", True)
add_idf_sdkconfig_option(
"CONFIG_NVS_SEC_HMAC_EFUSE_KEY_ID", nvs_enc[CONF_KEY_ID]
)
cg.add_define("ESPHOME_LOOP_TASK_STACK_SIZE", advanced[CONF_LOOP_TASK_STACK_SIZE])
cg.add_define(
+47 -5
View File
@@ -9,6 +9,8 @@
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
#include <esp_bt.h>
#else
#include "esphome/components/watchdog/watchdog.h"
#include <cinttypes>
extern "C" {
#include <esp_hosted.h>
#include <esp_hosted_misc.h>
@@ -33,6 +35,19 @@ namespace esphome::esp32_ble {
static const char *const TAG = "esp32_ble";
#ifdef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
// Bringing up the remote BT controller issues synchronous RPCs to the
// co-processor with 5 second response timeouts, and the default task watchdog
// is also 5 seconds. If the co-processor firmware does not answer (for example
// factory firmware without Bluetooth support), the watchdog would reboot the
// device before the RPC could return an error, causing a boot loop. Raise the
// watchdog for the duration of the bring-up so failures surface as error
// returns instead. 60 seconds covers the worst case: transport reconnect
// (up to ~20s), version preflight (1s), controller init/enable (5s each) and
// the bluedroid host bring-up over the hosted HCI transport.
static constexpr uint32_t HOSTED_BT_WDT_TIMEOUT_MS = 60000;
#endif
// GAP event groups for deduplication across gap_event_handler and dispatch_gap_event_
#define GAP_SCAN_COMPLETE_EVENTS \
case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: \
@@ -164,6 +179,9 @@ void ESP32BLE::advertising_init_() {
bool ESP32BLE::ble_setup_() {
esp_err_t err;
#ifdef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
watchdog::WatchdogManager wdt(HOSTED_BT_WDT_TIMEOUT_MS);
#endif
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
if (esp_bt_controller_get_status() != ESP_BT_CONTROLLER_STATUS_ENABLED) {
// start bt controller
@@ -192,15 +210,35 @@ bool ESP32BLE::ble_setup_() {
esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT);
#else
esp_hosted_connect_to_slave(); // NOLINT
if (esp_hosted_connect_to_slave() != ESP_OK) { // NOLINT
ESP_LOGE(TAG, "Co-processor transport failed; BLE disabled");
return false;
}
// Fast preflight (1 second RPC timeout): verifies the co-processor answers
// RPCs at all before the 5 second timeout BT controller RPCs below, and
// before hosted_hci_bluedroid_open(), which aborts if the transport is down.
esp_hosted_coprocessor_fwver_t fw_ver{};
if (esp_hosted_get_coprocessor_fwversion(&fw_ver) != ESP_OK) {
ESP_LOGE(TAG, "Co-processor not responding; BLE disabled. Update its firmware with the esp32_hosted "
"update component");
return false;
}
ESP_LOGD(TAG, "Co-processor firmware %" PRIu32 ".%" PRIu32 ".%" PRIu32, fw_ver.major1, fw_ver.minor1, fw_ver.patch1);
if (esp_hosted_bt_controller_init() != ESP_OK) {
ESP_LOGW(TAG, "esp_hosted_bt_controller_init failed");
ESP_LOGE(TAG,
"BT controller init failed; co-processor firmware %" PRIu32 ".%" PRIu32 ".%" PRIu32
" may lack BT support. Update it with the esp32_hosted update component; BLE disabled",
fw_ver.major1, fw_ver.minor1, fw_ver.patch1);
return false;
}
if (esp_hosted_bt_controller_enable() != ESP_OK) {
ESP_LOGW(TAG, "esp_hosted_bt_controller_enable failed");
ESP_LOGE(TAG,
"BT controller enable failed; co-processor firmware %" PRIu32 ".%" PRIu32 ".%" PRIu32
" may lack BT support. Update it with the esp32_hosted update component; BLE disabled",
fw_ver.major1, fw_ver.minor1, fw_ver.patch1);
return false;
}
@@ -332,6 +370,10 @@ bool ESP32BLE::ble_setup_() {
}
bool ESP32BLE::ble_dismantle_() {
#ifdef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
// Same 5 second RPCs as the bring-up path; see HOSTED_BT_WDT_TIMEOUT_MS
watchdog::WatchdogManager wdt(HOSTED_BT_WDT_TIMEOUT_MS);
#endif
esp_err_t err = esp_bluedroid_disable();
if (err != ESP_OK) {
// ESP_ERR_INVALID_STATE means Bluedroid is already disabled, which is fine
@@ -377,12 +419,12 @@ bool ESP32BLE::ble_dismantle_() {
}
#else
if (esp_hosted_bt_controller_disable() != ESP_OK) {
ESP_LOGW(TAG, "esp_hosted_bt_controller_disable failed");
ESP_LOGE(TAG, "esp_hosted_bt_controller_disable failed");
return false;
}
if (esp_hosted_bt_controller_deinit(false) != ESP_OK) {
ESP_LOGW(TAG, "esp_hosted_bt_controller_deinit failed");
ESP_LOGE(TAG, "esp_hosted_bt_controller_deinit failed");
return false;
}
@@ -18,6 +18,8 @@ from esphome.const import (
from esphome.cpp_generator import add_define
CODEOWNERS = ["@swoboda1337"]
# esp32_ble raises the task watchdog around the remote BT controller bring-up
AUTO_LOAD = ["watchdog"]
CONF_ACTIVE_HIGH = "active_high"
CONF_BUS_WIDTH = "bus_width"
+6
View File
@@ -332,6 +332,12 @@ async def to_code(config):
for symbol in ("vprintf", "printf", "fprintf"):
cg.add_build_flag(f"-Wl,--wrap={symbol}")
# Wrap the lwIP2 glue's do-nothing dhcp_cleanup()/dhcp_release() stubs so the
# linker can drop their "STUB: ..." message strings from DRAM.
# See lwip_glue_stubs.cpp for implementation.
for symbol in ("dhcp_cleanup", "dhcp_release"):
cg.add_build_flag(f"-Wl,--wrap={symbol}")
# Wrap Arduino's millis() so all callers (including Arduino libraries and ISR
# handlers) use our fast accumulator instead of the expensive 4x 64-bit multiply
# implementation in the Arduino ESP8266 core.
@@ -0,0 +1,35 @@
/*
* Linker wrap stubs for the lwIP2 glue's dead DHCP entry points.
*
* The ESP8266 SDK blobs call dhcp_cleanup() and dhcp_release() when the
* station leaves an access point (cnx_sta_leave, wifi_station_dhcpc_stop).
* In the prebuilt lwIP2 glue (liblwip2-*.a, glue-esp/lwip-esp.c) these are
* stubs whose only effect is printing "STUB: dhcp_cleanup" and
* "STUB: dhcp_release"; the real DHCP teardown happens through lwIP2's
* renamed dhcp_cleanup_LWIP2()/dhcp_release_LWIP2() functions.
*
* On ESP8266 .rodata lives in DRAM, so those message strings waste scarce
* RAM. Wrapping the stubs with silent equivalents lets the linker garbage
* collect the glue stub bodies together with their strings.
*
* Saves 38 bytes of RAM and removes the "STUB:" log noise on Wi-Fi
* disconnect. Behavior is otherwise unchanged.
*/
#if defined(USE_ESP8266)
namespace esphome::esp8266 {}
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
extern "C" {
// The callers are closed-source SDK blobs; the netif argument is unused.
void __wrap_dhcp_cleanup(void * /*netif*/) {}
// The glue stub returns ERR_ABRT (-8; lwIP 1.4 err_t is a signed char).
signed char __wrap_dhcp_release(void * /*netif*/) { return -8; }
} // extern "C"
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
#endif // USE_ESP8266
@@ -94,11 +94,12 @@ void ESPHomeOTAComponent::setup() {
}
void ESPHomeOTAComponent::dump_config() {
char addr_buf[network::USE_ADDRESS_BUFFER_SIZE];
ESP_LOGCONFIG(TAG,
"Over-The-Air updates:\n"
" Address: %s:%u\n"
" Version: %d",
network::get_use_address(), this->port_, USE_OTA_VERSION);
network::get_use_address_to(addr_buf), this->port_, USE_OTA_VERSION);
#ifdef USE_OTA_PASSWORD
if (!this->password_.empty()) {
ESP_LOGCONFIG(TAG, " Password configured");
+29 -3
View File
@@ -41,7 +41,7 @@ DeletePeerAction = espnow_ns.class_("DeletePeerAction", automation.Action)
ESPNowHandlerTrigger = automation.Trigger.template(
ESPNowRecvInfoConstRef,
cg.uint8.operator("const").operator("ptr"),
cg.uint8,
cg.uint16,
)
OnUnknownPeerTrigger = espnow_ns.class_(
@@ -56,6 +56,20 @@ OnBroadcastTrigger = espnow_ns.class_(
CONF_AUTO_ADD_PEER = "auto_add_peer"
CONF_MAX_PAYLOAD_SIZE = "max_payload_size"
# Payload limits of ESP-NOW v1 and v2 frames. The radio negotiates the
# protocol version per peer on its own; the option only sizes this device's
# packet buffers, whose static RAM cost is proportional to it (~8 KB at 250
# bytes, ~44 KB at 1470).
ESPNOW_PAYLOAD_V1 = 250
ESPNOW_PAYLOAD_V2 = 1470
# Config-time cap for action payloads. The per-device limit is the
# ``max_payload_size`` option, which the action schema cannot see; send()
# enforces it at runtime.
MAX_ESPNOW_PACKET_SIZE = ESPNOW_PAYLOAD_V2
CONF_PEERS = "peers"
CONF_ON_SENT = "on_sent"
CONF_ON_UNKNOWN_PEER = "on_unknown_peer"
@@ -63,7 +77,15 @@ CONF_ON_BROADCAST = "on_broadcast"
CONF_CONTINUE_ON_ERROR = "continue_on_error"
CONF_WAIT_FOR_SENT = "wait_for_sent"
MAX_ESPNOW_PACKET_SIZE = 250 # Maximum size of the payload in bytes
def _validate_max_payload_size(value: int) -> int:
if value > ESPNOW_PAYLOAD_V1:
return cv.require_framework_version(
esp_idf=cv.Version(5, 4, 0),
esp32_arduino=cv.Version(3, 2, 0),
extra_message="ESP-NOW v2 frames need an ESP-NOW v2 capable framework",
)(value)
return value
def validate_channel(value):
@@ -78,6 +100,9 @@ CONFIG_SCHEMA = cv.All(
cv.GenerateID(): cv.declare_id(ESPNowComponent),
cv.OnlyWithout(CONF_CHANNEL, CONF_WIFI): validate_channel,
cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean,
cv.Optional(CONF_MAX_PAYLOAD_SIZE, default=ESPNOW_PAYLOAD_V1): cv.All(
cv.int_range(min=1, max=ESPNOW_PAYLOAD_V2), _validate_max_payload_size
),
cv.Optional(CONF_AUTO_ADD_PEER, default=False): cv.boolean,
cv.Optional(CONF_PEERS): cv.ensure_list(cv.mac_address),
cv.Optional(CONF_ON_UNKNOWN_PEER): automation.validate_automation(
@@ -113,7 +138,7 @@ async def _trigger_to_code(config):
[
(ESPNowRecvInfoConstRef, "info"),
(cg.uint8.operator("const").operator("ptr"), "data"),
(cg.uint8, "size"),
(cg.uint16, "size"),
],
config,
)
@@ -125,6 +150,7 @@ async def to_code(config):
await cg.register_component(var, config)
cg.add_define("USE_ESPNOW")
cg.add_define("USE_ESPNOW_MAX_PAYLOAD_SIZE", config[CONF_MAX_PAYLOAD_SIZE])
if wifi_channel := config.get(CONF_CHANNEL):
cg.add(var.set_wifi_channel(wifi_channel))
+6 -6
View File
@@ -119,7 +119,7 @@ template<typename... Ts> class SetChannelAction final : public Action<Ts...>, pu
}
};
class OnReceiveTrigger final : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint8_t>,
class OnReceiveTrigger final : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint16_t>,
public ESPNowReceivedPacketHandler {
public:
explicit OnReceiveTrigger(std::array<uint8_t, ESP_NOW_ETH_ALEN> address) : has_address_(true) {
@@ -128,7 +128,7 @@ class OnReceiveTrigger final : public Trigger<const ESPNowRecvInfo &, const uint
explicit OnReceiveTrigger() {}
bool on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override {
bool on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) override {
bool match = !this->has_address_ || (memcmp(this->address_, info.src_addr, ESP_NOW_ETH_ALEN) == 0);
if (!match)
return false;
@@ -141,15 +141,15 @@ class OnReceiveTrigger final : public Trigger<const ESPNowRecvInfo &, const uint
bool has_address_{false};
uint8_t address_[ESP_NOW_ETH_ALEN]{};
};
class OnUnknownPeerTrigger final : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint8_t>,
class OnUnknownPeerTrigger final : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint16_t>,
public ESPNowUnknownPeerHandler {
public:
bool on_unknown_peer(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override {
bool on_unknown_peer(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) override {
this->trigger(info, data, size);
return false; // Return false to continue processing other internal handlers
}
};
class OnBroadcastTrigger final : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint8_t>,
class OnBroadcastTrigger final : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint16_t>,
public ESPNowBroadcastHandler {
public:
explicit OnBroadcastTrigger(std::array<uint8_t, ESP_NOW_ETH_ALEN> address) : has_address_(true) {
@@ -157,7 +157,7 @@ class OnBroadcastTrigger final : public Trigger<const ESPNowRecvInfo &, const ui
}
explicit OnBroadcastTrigger() {}
bool on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override {
bool on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) override {
bool match = !this->has_address_ || (memcmp(this->address_, info.src_addr, ESP_NOW_ETH_ALEN) == 0);
if (!match)
return false;
@@ -4,6 +4,7 @@
#include "espnow_err.h"
#include <algorithm>
#include <cinttypes>
#include "esphome/core/application.h"
@@ -96,9 +97,9 @@ void on_send_report(const uint8_t *mac_addr, esp_now_send_status_t status)
void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int size) {
// Drop oversized frames before copying. ESP-NOW v2 peers (IDF >= 5.4 builds a
// v2 stack with no opt-out) can send up to ESP_NOW_MAX_DATA_LEN_V2 (1470 B),
// but our receive buffer is ESP_NOW_MAX_DATA_LEN (250 B); copying a larger
// frame would overflow packet_.receive.data.
if (size < 0 || size > ESP_NOW_MAX_DATA_LEN) {
// but the receive buffer only fits v2 frames with ``max_payload_size``; copying a
// larger frame would overflow packet_.receive.data.
if (size < 0 || size > ESPNOW_MAX_DATA_LEN) {
global_esp_now->receive_packet_queue_.increment_dropped_count();
return;
}
@@ -285,11 +286,14 @@ void ESPNowComponent::loop() {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char src_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
char dst_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
// Cap the hex dump at a v1 frame: a full v2 frame would need a
// ~4.4 KB stack buffer.
char hex_buf[format_hex_pretty_size(ESP_NOW_MAX_DATA_LEN)];
format_mac_addr_upper(info.src_addr, src_buf);
format_mac_addr_upper(info.des_addr, dst_buf);
ESP_LOGV(TAG, "<<< [%s -> %s] %s", src_buf, dst_buf,
format_hex_pretty_to(hex_buf, packet->packet_.receive.data, packet->packet_.receive.size));
format_hex_pretty_to(hex_buf, packet->packet_.receive.data,
std::min<uint16_t>(packet->packet_.receive.size, ESP_NOW_MAX_DATA_LEN)));
#endif
if (memcmp(info.des_addr, ESPNOW_BROADCAST_ADDR, ESP_NOW_ETH_ALEN) == 0) {
for (auto *handler : this->broadcast_handlers_) {
@@ -362,7 +366,7 @@ esp_err_t ESPNowComponent::send(const uint8_t *peer_address, const uint8_t *payl
return ESP_ERR_ESPNOW_PEER_NOT_SET;
} else if (memcmp(peer_address, this->own_address_, ESP_NOW_ETH_ALEN) == 0) {
return ESP_ERR_ESPNOW_OWN_ADDRESS;
} else if (size > ESP_NOW_MAX_DATA_LEN) {
} else if (size > ESPNOW_MAX_DATA_LEN) {
return ESP_ERR_ESPNOW_DATA_SIZE;
} else if (!esp_now_is_peer_exist(peer_address)) {
if (memcmp(peer_address, ESPNOW_BROADCAST_ADDR, ESP_NOW_ETH_ALEN) == 0 || this->auto_add_peer_) {
+3 -3
View File
@@ -62,7 +62,7 @@ class ESPNowUnknownPeerHandler {
/// @param data Pointer to the received data payload
/// @param size Size of the received data in bytes
/// @return true if the packet was handled, false otherwise
virtual bool on_unknown_peer(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0;
virtual bool on_unknown_peer(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) = 0;
};
/// Handler interface for receiving ESPNow packets
@@ -74,7 +74,7 @@ class ESPNowReceivedPacketHandler {
/// @param data Pointer to the received data payload
/// @param size Size of the received data in bytes
/// @return true if the packet was handled, false otherwise
virtual bool on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0;
virtual bool on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) = 0;
};
/// Handler interface for receiving ESPNow broadcast packets
/// Components should inherit from this class to handle incoming ESPNow data
@@ -85,7 +85,7 @@ class ESPNowBroadcastHandler {
/// @param data Pointer to the received data payload
/// @param size Size of the received data in bytes
/// @return true if the packet was handled, false otherwise
virtual bool on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0;
virtual bool on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) = 0;
};
class ESPNowComponent final : public Component {
+26 -9
View File
@@ -19,6 +19,23 @@ namespace esphome::espnow {
static const uint8_t ESPNOW_BROADCAST_ADDR[ESP_NOW_ETH_ALEN] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
static const uint8_t ESPNOW_MULTICAST_ADDR[ESP_NOW_ETH_ALEN] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE};
// Maximum payload this component sends and receives, from the
// ``max_payload_size`` option. The radio stack speaks ESP-NOW v2 regardless
// (negotiated per peer); payloads beyond the v1 limit (250 bytes) are opt-in
// because the packet pools are statically sized from this, so their RAM cost
// is proportional (~8 KB at 250 bytes, ~44 KB at the v2 limit of 1470).
#ifndef USE_ESPNOW_MAX_PAYLOAD_SIZE
#define USE_ESPNOW_MAX_PAYLOAD_SIZE ESP_NOW_MAX_DATA_LEN
#endif
static constexpr uint16_t ESPNOW_MAX_DATA_LEN = USE_ESPNOW_MAX_PAYLOAD_SIZE;
#ifdef ESP_NOW_MAX_DATA_LEN_V2
static_assert(ESPNOW_MAX_DATA_LEN <= ESP_NOW_MAX_DATA_LEN_V2,
"espnow max_payload_size cannot exceed the ESP-NOW v2 frame limit");
#else
static_assert(ESPNOW_MAX_DATA_LEN <= ESP_NOW_MAX_DATA_LEN,
"espnow max_payload_size beyond 250 bytes requires an ESP-IDF with ESP-NOW v2 support (5.4+)");
#endif
struct WifiPacketRxControl {
int8_t rssi; // Received Signal Strength Indicator (RSSI) of packet, unit: dBm
uint32_t timestamp; // Timestamp in microseconds when the packet was received, precise only if modem sleep or
@@ -78,10 +95,10 @@ class ESPNowPacket {
union {
// NOLINTNEXTLINE(readability-identifier-naming)
struct received_data {
ESPNowRecvInfo info; // Information about the received packet
uint8_t data[ESP_NOW_MAX_DATA_LEN]; // Data received in the packet
uint8_t size; // Size of the received data
WifiPacketRxControl rx_ctrl; // Status of the received packet
ESPNowRecvInfo info; // Information about the received packet
uint8_t data[ESPNOW_MAX_DATA_LEN]; // Data received in the packet
uint16_t size; // Size of the received data
WifiPacketRxControl rx_ctrl; // Status of the received packet
} receive;
// NOLINTNEXTLINE(readability-identifier-naming)
@@ -144,15 +161,15 @@ class ESPNowSendPacket {
this->callback_ = nullptr; // Reset callback
}
uint8_t address_[ESP_NOW_ETH_ALEN]{0}; // MAC address of the peer to send the packet to
uint8_t data_[ESP_NOW_MAX_DATA_LEN]{0}; // Data to send
uint8_t size_{0}; // Size of the data to send, must be <= ESP_NOW_MAX_DATA_LEN
send_callback_t callback_{nullptr}; // Callback to call when the send operation is complete
uint8_t address_[ESP_NOW_ETH_ALEN]{0}; // MAC address of the peer to send the packet to
uint8_t data_[ESPNOW_MAX_DATA_LEN]{0}; // Data to send
uint16_t size_{0}; // Size of the data to send, must be <= ESPNOW_MAX_DATA_LEN
send_callback_t callback_{nullptr}; // Callback to call when the send operation is complete
private:
void init_data_(const uint8_t *peer_address, const uint8_t *payload, size_t size) {
memcpy(this->address_, peer_address, ESP_NOW_ETH_ALEN);
if (size > ESP_NOW_MAX_DATA_LEN) {
if (size > ESPNOW_MAX_DATA_LEN) {
this->size_ = 0;
return;
}
@@ -42,8 +42,8 @@ void ESPNowTransport::send_packet(const std::vector<uint8_t> &buf) const {
return;
}
if (buf.size() > ESP_NOW_MAX_DATA_LEN) {
ESP_LOGE(TAG, "Packet too large: %zu bytes (max %d)", buf.size(), ESP_NOW_MAX_DATA_LEN);
if (buf.size() > ESPNOW_MAX_DATA_LEN) {
ESP_LOGE(TAG, "Packet too large: %zu bytes (max %u)", buf.size(), (unsigned) ESPNOW_MAX_DATA_LEN);
return;
}
@@ -55,8 +55,8 @@ void ESPNowTransport::send_packet(const std::vector<uint8_t> &buf) const {
});
}
bool ESPNowTransport::on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) {
ESP_LOGV(TAG, "Received packet of size %u from %02X:%02X:%02X:%02X:%02X:%02X", size, info.src_addr[0],
bool ESPNowTransport::on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) {
ESP_LOGV(TAG, "Received packet of size %u from %02X:%02X:%02X:%02X:%02X:%02X", (unsigned) size, info.src_addr[0],
info.src_addr[1], info.src_addr[2], info.src_addr[3], info.src_addr[4], info.src_addr[5]);
if (data == nullptr || size == 0) {
@@ -70,9 +70,9 @@ bool ESPNowTransport::on_receive(const ESPNowRecvInfo &info, const uint8_t *data
return false; // Allow other handlers to run
}
bool ESPNowTransport::on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) {
ESP_LOGV(TAG, "Received broadcast packet of size %u from %02X:%02X:%02X:%02X:%02X:%02X", size, info.src_addr[0],
info.src_addr[1], info.src_addr[2], info.src_addr[3], info.src_addr[4], info.src_addr[5]);
bool ESPNowTransport::on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) {
ESP_LOGV(TAG, "Received broadcast packet of size %u from %02X:%02X:%02X:%02X:%02X:%02X", (unsigned) size,
info.src_addr[0], info.src_addr[1], info.src_addr[2], info.src_addr[3], info.src_addr[4], info.src_addr[5]);
if (data == nullptr || size == 0) {
ESP_LOGW(TAG, "Received empty or null broadcast packet");
@@ -24,12 +24,12 @@ class ESPNowTransport final : public packet_transport::PacketTransport,
}
// ESPNow handler interface
bool on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override;
bool on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override;
bool on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) override;
bool on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) override;
protected:
void send_packet(const std::vector<uint8_t> &buf) const override;
size_t get_max_packet_size() override { return ESP_NOW_MAX_DATA_LEN; }
size_t get_max_packet_size() override { return ESPNOW_MAX_DATA_LEN; }
bool should_send() override;
peer_address_t peer_address_{{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}};
+2 -2
View File
@@ -4,7 +4,7 @@ import logging
from esphome import automation, pins
from esphome.automation import Condition
import esphome.codegen as cg
from esphome.components.network import ip_address_literal
from esphome.components.network import add_use_address, ip_address_literal
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv
from esphome.const import (
@@ -543,7 +543,7 @@ async def to_code(config):
await _to_code_rp2040(var, config)
cg.add(var.set_type(ETHERNET_TYPES[config[CONF_TYPE]]))
cg.add(var.set_use_address(config[CONF_USE_ADDRESS]))
add_use_address(var, config[CONF_USE_ADDRESS])
# enable_on_boot defaults to true in C++ - only set if false
if not config[CONF_ENABLE_ON_BOOT]:
cg.add(var.set_enable_on_boot(False))
@@ -145,6 +145,8 @@ class EthernetComponent final : public Component {
network::IPAddresses get_ip_addresses();
network::IPAddress get_dns_address(uint8_t num);
/// Returns nullptr when no explicit use_address is configured and the address is
/// derived at runtime from the device name (see network::get_use_address_to()).
const char *get_use_address() const { return this->use_address_; }
void set_use_address(const char *use_address) { this->use_address_ = use_address; }
void get_eth_mac_address_raw(uint8_t *mac);
@@ -346,7 +348,7 @@ class EthernetComponent final : public Component {
private:
// Stores a pointer to a string literal (static storage duration).
// ONLY set from Python-generated code with string literals - never dynamic strings.
const char *use_address_{""};
const char *use_address_{nullptr};
};
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
+1 -1
View File
@@ -21,6 +21,7 @@ from esphome.const import (
UNIT_EMPTY,
UNIT_KELVIN,
UNIT_KILOWATT,
UNIT_LITRE_PER_HOUR,
)
CODEOWNERS = ["@cfeenstra1024"]
@@ -37,7 +38,6 @@ CONF_TEMP2 = "temp2"
CONF_TEMP_DIFF = "temp_diff"
UNIT_GIGA_JOULE = "GJ"
UNIT_LITRE_PER_HOUR = "l/h"
# Note: The sensor units are set automatically based un the received data from the meter
CONFIG_SCHEMA = (
@@ -5,7 +5,7 @@
namespace esphome::libretiny {
static const char *const TAG = "lt.gpio";
static const char *const TAG = "libretiny.gpio";
static int IRAM_ATTR flags_to_mode(gpio::Flags flags) {
if (flags == gpio::FLAG_INPUT) {
@@ -6,7 +6,7 @@
namespace esphome::libretiny {
static const char *const TAG = "lt.component";
static const char *const TAG = "libretiny";
void LTComponent::dump_config() {
ESP_LOGCONFIG(TAG,
+10 -8
View File
@@ -213,17 +213,19 @@ LightColorValues LightCall::validate_() {
// Flag whether an explicit turn off was requested, in which case we'll also stop the effect.
bool explicit_turn_off_request = this->has_state() && !this->state_;
// Turn off when brightness is set to zero, and reset brightness (so that it has nonzero brightness when turned on).
if (this->has_brightness() && this->brightness_ == 0.0f) {
// Treat zero brightness as an implicit turn-off when no state was explicitly requested.
if (this->has_brightness() && this->brightness_ == 0.0f && !this->has_state()) {
this->state_ = false;
this->set_flag_(FLAG_HAS_STATE);
if (color_mode & ColorCapability::BRIGHTNESS) {
// Reset brightness so the light has nonzero brightness when turned back on.
}
// Make sure a turn-on makes the light visible: if the resulting brightness would be zero
// (e.g. restored from a brightness=0 turn-off), reset it to full brightness.
if (this->has_state() && this->state_ && (color_mode & ColorCapability::BRIGHTNESS)) {
float brightness = this->has_brightness() ? this->brightness_ : this->parent_->remote_values.get_brightness();
if (brightness == 0.0f) {
this->brightness_ = 1.0f;
} else {
// Light doesn't support brightness; clear the flag to avoid a spurious
// "brightness not supported" warning during capability validation.
this->clear_flag_(FLAG_HAS_BRIGHTNESS);
this->set_flag_(FLAG_HAS_BRIGHTNESS);
}
}
+6
View File
@@ -414,6 +414,10 @@ async def to_code(configs):
await cg.register_component(lv_component, config)
if rotation := config.get(CONF_ROTATION):
cg.add(lv_component.set_rotation(rotation))
if paused := config[df.CONF_PAUSED]:
cg.add(lv_component.set_paused(paused, False))
if refr_time := config.get(df.CONF_REFRESH_INTERVAL):
cg.add(lv_component.set_refresh_interval(refr_time.total_milliseconds))
Widget.create(config[CONF_ID], lv_component, LvScrActType(), config)
lv_scr_act = get_screen_active(lv_component)
@@ -598,6 +602,7 @@ LVGL_TOP_LEVEL_SCHEMA = (
cv.Optional(df.CONF_DEFAULT_FONT, default="montserrat_14"): lvalid.lv_font,
cv.Optional(df.CONF_FULL_REFRESH, default=False): cv.boolean,
cv.Optional(df.CONF_UPDATE_WHEN_DISPLAY_IDLE, default=False): cv.boolean,
cv.Optional(df.CONF_REFRESH_INTERVAL): cv.positive_time_period_milliseconds,
cv.Optional(CONF_DRAW_ROUNDING, default=2): cv.positive_int,
cv.Optional(CONF_BUFFER_SIZE, default=0): cv.percentage,
cv.Optional(CONF_ROTATION): validate_rotation,
@@ -642,6 +647,7 @@ LVGL_TOP_LEVEL_SCHEMA = (
cv.Optional(df.CONF_KEYPADS, default=None): KEYPADS_CONFIG,
cv.GenerateID(df.CONF_DEFAULT_GROUP): cv.declare_id(lv_group_t),
cv.Optional(df.CONF_RESUME_ON_INPUT, default=True): cv.boolean,
cv.Optional(df.CONF_PAUSED, default=False): cv.boolean,
}
)
.extend(DISP_BG_SCHEMA)
+2
View File
@@ -758,12 +758,14 @@ CONF_PAD_COLUMN = "pad_column"
CONF_PAGE = "page"
CONF_PAGE_WRAP = "page_wrap"
CONF_PASSWORD_MODE = "password_mode"
CONF_PAUSED = "paused"
CONF_PIVOT_X = "pivot_x"
CONF_PIVOT_Y = "pivot_y"
CONF_PLACEHOLDER_TEXT = "placeholder_text"
CONF_POINTS = "points"
CONF_PREVIOUS = "previous"
CONF_RADIUS = "radius"
CONF_REFRESH_INTERVAL = "refresh_interval"
CONF_REPEAT_COUNT = "repeat_count"
CONF_RECOLOR = "recolor"
CONF_RESUME_ON_INPUT = "resume_on_input"
+38 -22
View File
@@ -401,7 +401,10 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_data *ptr) {
}
void LvglComponent::flush_cb_(lv_display_t *disp_drv, const lv_area_t *area, uint8_t *color_p) {
if (!this->is_paused()) {
// no guard here for display busy, since LVGL will not call flush_cb until the refresh timer fires,
// and while the display is busy this is reset to 5 minutes. If that expires and the display is still
// busy there are bigger problems.
if (!this->paused_) {
auto now = millis();
this->draw_buffer_(area, reinterpret_cast<lv_color_data *>(color_p));
ESP_LOGV(TAG, "flush_cb, area=%d/%d, %d/%d took %dms", (int) area->x1, (int) area->y1,
@@ -620,20 +623,20 @@ void LvKeyboardType::set_obj(lv_obj_t *lv_obj) {
void LvglComponent::draw_end_() {
if (this->draw_end_callback_ != nullptr)
this->draw_end_callback_->trigger();
// Only reachable once the display is idle again: while busy, the display's refr_timer_ is
// paused (see loop()), so LVGL never renders/flushes and this event never fires.
if (this->update_when_display_idle_) {
for (auto *disp : this->displays_)
disp->update();
}
}
bool LvglComponent::is_paused() const {
if (this->paused_)
return true;
if (this->update_when_display_idle_) {
for (auto *disp : this->displays_) {
if (!disp->is_idle())
return true;
}
bool LvglComponent::displays_busy_() const {
if (!this->update_when_display_idle_)
return false;
for (auto *disp : this->displays_) {
if (!disp->is_idle())
return true;
}
return false;
}
@@ -777,6 +780,8 @@ void LvglComponent::setup() {
if (this->draw_end_callback_ != nullptr || this->update_when_display_idle_) {
lv_display_add_event_cb(this->disp_, render_end_cb, LV_EVENT_REFR_READY, this);
}
this->refr_timer_ = lv_display_get_refr_timer(this->disp_);
lv_timer_set_period(this->refr_timer_, this->refr_timer_period_);
#if LV_USE_LOG
lv_log_register_print_cb([](lv_log_level_t level, const char *buf) {
auto next = strchr(buf, ')');
@@ -802,21 +807,32 @@ void LvglComponent::update() {
}
void LvglComponent::loop() {
if (this->is_paused()) {
if (this->paused_ && this->show_snow_)
if (this->paused_) {
if (this->show_snow_)
this->write_random_();
} else {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
auto now = millis();
lv_timer_handler();
auto elapsed = millis() - now;
if (elapsed > 15) {
ESP_LOGV(TAG, "lv_timer_handler took %dms", (int) (millis() - now));
}
#else
lv_timer_handler();
#endif
return;
}
// Pause/resume the display's own refresh timer to track its busy state. While paused, LVGL
// still keeps track of invalidated areas but won't render or flush them, so nothing needs to
// be discarded or replayed: once resumed, the accumulated areas are simply drawn as normal.
// Input events and other timers keep being processed below regardless of this state.
if (this->update_when_display_idle_) {
bool busy = this->displays_busy_();
if (busy && !this->refr_timer_paused_) {
this->refr_timer_paused_ = true;
// calling lv_timer_pause() here would be ineffective; LVGL pauses and resumes the timer based on its own internal
// state, which is not aware of the display's busy state. Instead, we extend the timer period to avoid it firing
// while the display is busy.
lv_timer_set_period(this->refr_timer_, 5 * 60 * 1000);
} else if (!busy && this->refr_timer_paused_) {
this->refr_timer_paused_ = false;
lv_timer_set_period(this->refr_timer_, this->refr_timer_period_);
// Don't wait for the timer's next natural period: refresh right away now that the
// display is idle again.
lv_timer_ready(this->refr_timer_);
}
}
lv_timer_handler();
}
#ifdef USE_LVGL_ANIMIMG
+18 -2
View File
@@ -214,9 +214,14 @@ class LvglComponent final : public PollingComponent {
// @param paused If true, pause the display. If false, resume the display.
// @param show_snow If true, show the snow effect when paused.
void set_paused(bool paused, bool show_snow);
void set_refresh_interval(uint32_t period) {
this->refr_timer_period_ = period;
if (this->refr_timer_ != nullptr)
lv_timer_set_period(this->refr_timer_, period);
}
// Returns true if the display is explicitly paused, or a blocking display update is in progress.
bool is_paused() const;
// Returns true if the display has been explicitly paused via set_paused().
bool is_paused() const { return this->paused_; }
// If the display is paused and we have resume_on_input_ set to true, resume the display.
void maybe_wakeup() {
if (this->paused_ && this->resume_on_input_) {
@@ -299,6 +304,9 @@ class LvglComponent final : public PollingComponent {
// Not checking for non-null callback since the
// LVGL callback that calls it is not set in that case
void draw_start_() const { this->draw_start_callback_->trigger(); }
// Returns true if update_when_display_idle is enabled and at least one underlying display
// component is currently busy (e.g. mid-refresh).
bool displays_busy_() const;
void write_random_();
void draw_buffer_(const lv_area_t *area, lv_color_data *ptr);
@@ -316,6 +324,14 @@ class LvglComponent final : public PollingComponent {
uint8_t *draw_buf_{};
lv_display_t *disp_{};
// The display's own periodic refresh timer, effectively paused while the display is busy (see
// displays_busy_()) so LVGL neither renders nor flushes to it, without losing track of
// invalidated areas. Other timers (indev reading, animations, ...) keep running as normal.
lv_timer_t *refr_timer_{};
// Tracks whether refr_timer_ is currently paused, so loop() can detect the busy -> idle edge
// and kick off an immediate refresh instead of waiting for the timer's next natural period.
bool refr_timer_paused_{};
uint32_t refr_timer_period_{16};
uint16_t width_{};
uint16_t height_{};
bool paused_{};
-72
View File
@@ -10,7 +10,6 @@ from esphome.components.mipi import (
GMCTR,
GMCTRN1,
GMCTRP1,
IDMOFF,
IFCTR,
IFMODE,
INVCTR,
@@ -23,7 +22,6 @@ from esphome.components.mipi import (
PWCTR5,
PWSET,
PWSETN,
SETEXTC,
VMCTR,
VMCTR1,
VMCTR2,
@@ -32,60 +30,6 @@ from esphome.components.mipi import (
)
from esphome.components.spi import TYPE_OCTAL
DriverChip(
"M5CORE",
width=320,
height=240,
cs_pin=14,
dc_pin=27,
reset_pin=33,
initsequence=(
(SETEXTC, 0xFF, 0x93, 0x42),
(PWCTR1, 0x12, 0x12),
(PWCTR2, 0x03),
(VMCTR1, 0xF2),
(IFMODE, 0xE0),
(0xF6, 0x01, 0x00, 0x00),
(
GMCTRP1,
0x00,
0x0C,
0x11,
0x04,
0x11,
0x08,
0x37,
0x89,
0x4C,
0x06,
0x0C,
0x0A,
0x2E,
0x34,
0x0F,
),
(
GMCTRN1,
0x00,
0x0B,
0x11,
0x05,
0x13,
0x09,
0x33,
0x67,
0x48,
0x07,
0x0E,
0x0B,
0x2E,
0x33,
0x0F,
),
(DFUNCTR, 0x08, 0x82, 0x1D, 0x04),
(IDMOFF,),
),
)
ILI9341 = DriverChip(
"ILI9341",
mirror_x=True,
@@ -174,22 +118,6 @@ ILI9342 = DriverChip(
),
)
# M5Stack Core2 uses ILI9341 chip - mirror_x disabled for correct orientation
ILI9341.extend(
"M5CORE2",
# Reset native dimensions due to axis swap.
native_width=320,
native_height=240,
width=320,
height=240,
mirror_x=False,
cs_pin=5,
dc_pin=15,
invert_colors=True,
pixel_mode="18bit",
data_rate="40MHz",
)
DriverChip(
"ILI9481",
mirror_x=True,
@@ -0,0 +1,71 @@
from esphome.components.mipi import (
DFUNCTR,
GMCTRN1,
GMCTRP1,
IDMOFF,
IFMODE,
PWCTR1,
PWCTR2,
SETEXTC,
VMCTR1,
DriverChip,
)
from .ili import ILI9341, ST7789V
# fmt: off
DriverChip(
"M5CORE",
width=320,
height=240,
cs_pin=14,
dc_pin=27,
reset_pin=33,
initsequence=(
(SETEXTC, 0xFF, 0x93, 0x42),
(PWCTR1, 0x12, 0x12),
(PWCTR2, 0x03),
(VMCTR1, 0xF2),
(IFMODE, 0xE0),
(0xF6, 0x01, 0x00, 0x00),
(GMCTRP1, 0x00, 0x0C, 0x11, 0x04, 0x11, 0x08, 0x37, 0x89, 0x4C, 0x06, 0x0C, 0x0A, 0x2E, 0x34, 0x0F,),
(GMCTRN1, 0x00, 0x0B, 0x11, 0x05, 0x13, 0x09, 0x33, 0x67, 0x48, 0x07, 0x0E, 0x0B, 0x2E, 0x33, 0x0F,),
(DFUNCTR, 0x08, 0x82, 0x1D, 0x04),
(IDMOFF,),
),
)
# M5Stack Core2 uses ILI9341 chip - mirror_x disabled for correct orientation
ILI9341.extend(
"M5CORE2",
# Reset native dimensions due to axis swap.
native_width=320,
native_height=240,
width=320,
height=240,
mirror_x=False,
cs_pin=5,
dc_pin=15,
invert_colors=True,
pixel_mode="18bit",
data_rate="40MHz",
)
GC9107 = ST7789V.extend(
"GC9107",
width=128,
height=128,
offset_width=2,
offset_height=1,
pad_width=2,
pad_height=1,
)
GC9107.extend(
"M5STACK-ATOMS3R-GC9107",
data_rate="40MHz",
invert_colors=True,
reset_pin=48,
dc_pin=42,
cs_pin=14,
)
+13 -12
View File
@@ -51,7 +51,7 @@ void ModbusClientHub::loop() {
// If we're past the send_wait_time timeout and response buffer doesn't have the start of the expected response
if (this->waiting_for_response_.has_value()) {
ModbusDeviceCommand &wfr = this->waiting_for_response_.value();
uint8_t expected_address = wfr.frame.data.get()[0];
uint8_t expected_address = wfr.frame.data.data()[0];
if (this->last_receive_check_ - this->last_send_ > this->last_send_tx_offset_ + this->send_wait_time_ &&
(this->rx_buffer_.empty() || this->rx_buffer_[0] != expected_address)) {
ESP_LOGW(TAG, "Stop waiting for response from %" PRIu8 " %" PRIu32 "ms after last send", expected_address,
@@ -270,8 +270,8 @@ void ModbusClientHub::process_modbus_server_frame(uint8_t address, uint8_t funct
// Check if the response matches the expected address and function code
ModbusDeviceCommand &wfr = this->waiting_for_response_.value();
uint8_t expected_address = wfr.frame.data.get()[0];
uint8_t expected_function_code = wfr.frame.data.get()[1];
uint8_t expected_address = wfr.frame.data.data()[0];
uint8_t expected_function_code = wfr.frame.data.data()[1];
if (expected_address != address || expected_function_code != (function_code & FUNCTION_CODE_MASK)) {
ESP_LOGW(TAG,
"Received incorrect frame address %" PRIu8 " <> %" PRIu8 " or function code 0x%X <> 0x%X, %" PRIu32
@@ -458,7 +458,7 @@ bool Modbus::send_frame_(const ModbusFrame &frame) {
ESP_LOGE(TAG, "Attempted to send while transmission blocked");
return false;
}
if (frame.size > MAX_FRAME_SIZE) {
if (frame.size() > MAX_FRAME_SIZE) {
ESP_LOGE(TAG, "Attempted to send frame larger than max frame size of %" PRIu16 " bytes", MAX_FRAME_SIZE);
return false;
}
@@ -470,13 +470,13 @@ bool Modbus::send_frame_(const ModbusFrame &frame) {
if (this->flow_control_pin_ != nullptr) {
this->flow_control_pin_->digital_write(true);
this->write_array(frame.data.get(), frame.size);
this->write_array(frame.data.data(), frame.size());
this->flush();
this->flow_control_pin_->digital_write(false);
this->last_send_tx_offset_ = 0;
} else {
this->write_array(frame.data.get(), frame.size);
this->last_send_tx_offset_ = frame.size * MODBUS_BITS_PER_CHAR * MS_PER_SEC / this->parent_->get_baud_rate() + 1;
this->write_array(frame.data.data(), frame.size());
this->last_send_tx_offset_ = frame.size() * MODBUS_BITS_PER_CHAR * MS_PER_SEC / this->parent_->get_baud_rate() + 1;
}
uint32_t now = millis();
@@ -484,7 +484,7 @@ bool Modbus::send_frame_(const ModbusFrame &frame) {
char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)];
#endif
ESP_LOGV(TAG, "Write: %s %" PRIu32 "ms after last send, %" PRIu32 "ms after last receive",
format_hex_pretty_to(hex_buf, frame.data.get(), frame.size), now - this->last_send_,
format_hex_pretty_to(hex_buf, frame.data.data(), frame.size()), now - this->last_send_,
now - this->last_modbus_byte_);
this->last_send_ = now;
return true;
@@ -590,12 +590,13 @@ void ModbusClientHub::queue_raw_(uint8_t address, const uint8_t *pdu, uint16_t p
void ModbusClientHub::clear_tx_queue_for_address(uint8_t address, bool clear_sent) {
// Remove any pending commands for this address from the tx buffer
auto &tx_buffer = this->tx_buffer_;
tx_buffer.erase(std::remove_if(tx_buffer.begin(), tx_buffer.end(),
[address](const ModbusDeviceCommand &cmd) { return cmd.frame.data[0] == address; }),
tx_buffer.end());
tx_buffer.erase(
std::remove_if(tx_buffer.begin(), tx_buffer.end(),
[address](const ModbusDeviceCommand &cmd) { return cmd.frame.data.data()[0] == address; }),
tx_buffer.end());
if (clear_sent && this->waiting_for_response_.has_value() && this->waiting_for_response_.value().device) {
if (this->waiting_for_response_.value().frame.data[0] == address) {
if (this->waiting_for_response_.value().frame.data.data()[0] == address) {
ESP_LOGV(TAG, "Clearing waiting for response for address %" PRIu8, address);
// Invalidate the waiting device so it won't process a response.
this->waiting_for_response_.value().device = nullptr;
+19 -11
View File
@@ -18,19 +18,27 @@ namespace esphome::modbus {
static constexpr uint16_t MODBUS_TX_BUFFER_SIZE = 15;
static constexpr uint16_t MODBUS_TX_MAX_DELAY_MS = 5;
struct ModbusFrame {
// Frame with exact-size allocation to avoid std::vector overhead
std::unique_ptr<uint8_t[]> data;
uint16_t size; // Modbus RTU max is 256 bytes
// Typical frames -- reads and single-register/coil writes -- are exactly 8 bytes
// (address + 5-byte PDU + 2-byte CRC) and fit inline with no heap allocation.
static constexpr uint16_t MODBUS_FRAME_INLINE_SIZE = 8;
ModbusFrame(uint8_t address, const uint8_t *pdu, uint16_t pdu_len)
: data(std::make_unique<uint8_t[]>(pdu_len + 3)), size(pdu_len + 3) {
data[0] = address;
memcpy(data.get() + 1, pdu, pdu_len);
auto crc = crc16(data.get(), pdu_len + 1);
data[pdu_len + 1] = crc >> 0;
data[pdu_len + 2] = crc >> 8;
struct ModbusFrame {
// Frame held in a small-buffer-optimized buffer. Typical frames fit inline; only larger
// multi-register or custom frames spill to a single heap allocation. This keeps the common,
// high-frequency tx traffic off the heap entirely, avoiding per-frame alloc/free churn.
// The buffer tracks its own length, so no separate size field is needed.
SmallInlineBuffer<MODBUS_FRAME_INLINE_SIZE> data; // Modbus RTU max is 256 bytes
ModbusFrame(uint8_t address, const uint8_t *pdu, uint16_t pdu_len) {
uint8_t *buf = this->data.init(pdu_len + 3);
buf[0] = address;
memcpy(buf + 1, pdu, pdu_len);
auto crc = crc16(buf, pdu_len + 1);
buf[pdu_len + 1] = crc >> 0;
buf[pdu_len + 2] = crc >> 8;
}
uint16_t size() const { return static_cast<uint16_t>(this->data.size()); }
};
class Modbus : public uart::UARTDevice, public Component {
+13
View File
@@ -59,6 +59,19 @@ def ip_address_literal(ip: str | int | None) -> cg.MockObj:
return IPAddress(str(ip))
def add_use_address(var: cg.MockObj, use_address: str) -> None:
"""Generate a set_use_address() call only when the address must be baked in.
The default "<name>.local" is not stored in the firmware; it is rebuilt at
runtime from the device name (see network::get_use_address_to()), which also
picks up the MAC suffix when name_add_mac_suffix is enabled. A compile-time
string could never include that suffix, so baking it in would log the wrong
address.
"""
if use_address != f"{CORE.name}.local":
cg.add(var.set_use_address(use_address))
def require_high_performance_networking() -> None:
"""Request high performance networking for network and WiFi.
+23
View File
@@ -1,5 +1,7 @@
#include "util.h"
#include "esphome/core/application.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#ifdef USE_NETWORK
namespace esphome::network {
@@ -20,6 +22,27 @@ bool is_disabled() {
return false;
}
const char *get_use_address_to(std::span<char, USE_ADDRESS_BUFFER_SIZE> buf) {
// Global component pointers are guaranteed to be set by component constructors when USE_* is defined
const char *addr = nullptr;
#if defined(USE_ETHERNET)
addr = ethernet::global_eth_component->get_use_address();
#elif defined(USE_MODEM)
addr = modem::global_modem_component->get_use_address();
#elif defined(USE_WIFI)
addr = wifi::global_wifi_component->get_use_address();
#elif defined(USE_OPENTHREAD)
addr = openthread::global_openthread_component->get_use_address();
#endif
if (addr != nullptr && addr[0] != '\0')
return addr;
// No explicit use_address configured: the address is the runtime device name
// (which includes the MAC suffix when name_add_mac_suffix is enabled) plus ".local"
const auto &name = App.get_name();
make_name_with_suffix_to(buf.data(), buf.size(), name.c_str(), name.size(), '.', "local", 5);
return buf.data();
}
network::IPAddresses get_ip_addresses() {
#ifdef USE_ETHERNET
if (ethernet::global_eth_component != nullptr)
+7 -24
View File
@@ -1,6 +1,7 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_NETWORK
#include <span>
#include <string>
#include "esphome/core/helpers.h"
#include "ip_address.h"
@@ -53,30 +54,12 @@ ESPHOME_ALWAYS_INLINE inline bool is_connected() {
/// Return whether the network is disabled (only wifi for now)
bool is_disabled();
/// Get the active network hostname
ESPHOME_ALWAYS_INLINE inline const char *get_use_address() {
// Global component pointers are guaranteed to be set by component constructors when USE_* is defined
#ifdef USE_ETHERNET
return ethernet::global_eth_component->get_use_address();
#endif
#ifdef USE_MODEM
return modem::global_modem_component->get_use_address();
#endif
#ifdef USE_WIFI
return wifi::global_wifi_component->get_use_address();
#endif
#ifdef USE_OPENTHREAD
return openthread::global_openthread_component->get_use_address();
#endif
#if !defined(USE_ETHERNET) && !defined(USE_MODEM) && !defined(USE_WIFI) && !defined(USE_OPENTHREAD)
// Fallback when no network component is defined (e.g., host platform)
return "";
#endif
}
/// Buffer size for get_use_address_to(): 63-char DNS label + ".local" + null terminator
static constexpr size_t USE_ADDRESS_BUFFER_SIZE = 70;
/// Get the active network address for logging. Returns the explicitly configured
/// use_address when one was set, otherwise formats "<name>.local" from the runtime
/// device name into buf (so it includes the MAC suffix from name_add_mac_suffix).
const char *get_use_address_to(std::span<char, USE_ADDRESS_BUFFER_SIZE> buf);
IPAddresses get_ip_addresses();
} // namespace esphome::network
+2 -1
View File
@@ -14,6 +14,7 @@ from esphome.components.esp32 import (
require_vfs_select,
)
from esphome.components.mdns import MDNSComponent, enable_mdns_storage
from esphome.components.network import add_use_address
from esphome.components.zephyr import zephyr_add_prj_conf
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv
@@ -288,7 +289,7 @@ async def to_code(config):
enable_mdns_storage()
ot = cg.new_Pvariable(config[CONF_ID])
cg.add(ot.set_use_address(config[CONF_USE_ADDRESS]))
add_use_address(ot, config[CONF_USE_ADDRESS])
await cg.register_component(ot, config)
if (poll_period := config.get(CONF_POLL_PERIOD)) is not None:
cg.add(ot.set_poll_period(poll_period))
+3 -1
View File
@@ -39,6 +39,8 @@ class OpenThreadComponent final : public Component {
void on_factory_reset(std::function<void()> callback);
void defer_factory_reset_external_callback();
/// Returns nullptr when no explicit use_address is configured and the address is
/// derived at runtime from the device name (see network::get_use_address_to()).
const char *get_use_address() const { return this->use_address_; }
void set_use_address(const char *use_address) { this->use_address_ = use_address; }
#if CONFIG_OPENTHREAD_MTD
@@ -76,7 +78,7 @@ class OpenThreadComponent final : public Component {
private:
// Stores a pointer to a string literal (static storage duration).
// ONLY set from Python-generated code with string literals - never dynamic strings.
const char *use_address_{""};
const char *use_address_{nullptr};
};
extern OpenThreadComponent *global_openthread_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
+28
View File
@@ -2,6 +2,34 @@
namespace esphome::ota {
bool version_is_older(const char *candidate, const char *reference) {
if (candidate == nullptr || reference == nullptr)
return false;
while (true) {
uint32_t a = 0;
while (*candidate >= '0' && *candidate <= '9') {
a = a * 10 + static_cast<uint32_t>(*candidate - '0');
candidate++;
}
uint32_t b = 0;
while (*reference >= '0' && *reference <= '9') {
b = b * 10 + static_cast<uint32_t>(*reference - '0');
reference++;
}
if (a != b)
return a < b;
// Components equal so far; advance past a single separator on each side.
const bool a_more = (*candidate == '.');
const bool b_more = (*reference == '.');
if (a_more)
candidate++;
if (b_more)
reference++;
if (!a_more && !b_more)
return false; // Both strings exhausted with all components equal.
}
}
#ifdef USE_OTA_STATE_LISTENER
OTAGlobalCallback *global_ota_callback{nullptr}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
+15
View File
@@ -46,9 +46,24 @@ enum OTAResponseTypes {
OTA_RESPONSE_ERROR_PARTITION_TABLE_UPDATE = 0x90,
OTA_RESPONSE_ERROR_BOOTLOADER_VERIFY = 0x91,
OTA_RESPONSE_ERROR_BOOTLOADER_UPDATE = 0x92,
OTA_RESPONSE_ERROR_VERSION_DOWNGRADE = 0x93,
OTA_RESPONSE_ERROR_UNKNOWN = 0xFF,
};
/** Compare two dotted-numeric version strings (such as "1.2.3").
*
* Returns true when @p candidate represents a strictly older (lower) firmware
* version than @p reference. Each dot-separated component is parsed as an
* integer and compared left-to-right; absent trailing components count as 0,
* so "1.2" and "1.2.0" are equal. Equal versions return false so that
* re-flashing the same version is permitted.
*
* Used for software OTA downgrade protection. Inputs come from the project
* version embedded in the signed firmware image, which is validated to be
* dotted-numeric at config time. Non-digit characters terminate a component.
*/
bool version_is_older(const char *candidate, const char *reference);
enum OTAState {
OTA_COMPLETED = 0,
OTA_STARTED,
@@ -9,6 +9,9 @@
#include <esp_ota_ops.h>
#include <esp_task_wdt.h>
#include <spi_flash_mmap.h>
#ifdef USE_OTA_DOWNGRADE_PROTECTION
#include <esp_app_desc.h>
#endif
namespace esphome::ota {
@@ -159,6 +162,23 @@ OTAResponseTypes IDFOTABackend::end() {
}
#endif
if (err == ESP_OK) {
#ifdef USE_OTA_DOWNGRADE_PROTECTION
// The image is written and (when signing is enabled) signature-verified by
// esp_ota_end(), so its embedded project version can be trusted. Reject the
// update if it is older than the running version by leaving the boot
// partition unchanged -- the staged image simply never boots.
esp_app_desc_t incoming;
esp_err_t desc_err = esp_ota_get_partition_description(this->partition_, &incoming);
if (desc_err != ESP_OK) {
// Couldn't read the staged image's version, so the comparison is skipped.
// Warn so the bypassed check is observable rather than silent.
ESP_LOGW(TAG, "Downgrade protection: could not read image version (err=0x%X); allowing update", desc_err);
} else if (version_is_older(incoming.version, ESPHOME_PROJECT_VERSION)) {
ESP_LOGE(TAG, "Rejecting downgrade: image version '%s' is older than running version '%s'", incoming.version,
ESPHOME_PROJECT_VERSION);
return OTA_RESPONSE_ERROR_VERSION_DOWNGRADE;
}
#endif
err = esp_ota_set_boot_partition(this->partition_);
if (err == ESP_OK) {
return OTA_RESPONSE_OK;
+49 -2
View File
@@ -5,6 +5,7 @@ from esphome.components.audio_dac import AudioDac
import esphome.config_validation as cv
from esphome.const import (
CONF_BITS_PER_SAMPLE,
CONF_ENABLE_PIN,
CONF_ID,
CONF_INPUT,
CONF_INVERTED,
@@ -16,6 +17,11 @@ from esphome.const import (
CODEOWNERS = ["@remcom"]
DEPENDENCIES = ["i2c"]
CONF_ANALOG_GAIN = "analog_gain"
CONF_CHANNEL_MIX = "channel_mix"
CONF_VOLUME_MIN_DB = "volume_min_db"
CONF_VOLUME_MAX_DB = "volume_max_db"
pcm5122_ns = cg.esphome_ns.namespace("pcm5122")
PCM5122 = pcm5122_ns.class_("PCM5122", AudioDac, cg.Component, i2c.I2CDevice)
CONF_PCM5122 = "pcm5122"
@@ -27,26 +33,60 @@ PCM5122_BITS_PER_SAMPLE_ENUM = {
32: pcm5122_bits_per_sample.PCM5122_BITS_PER_SAMPLE_32,
}
pcm5122_analog_gain = pcm5122_ns.enum("PCM5122AnalogGain")
PCM5122_ANALOG_GAIN_ENUM = {
"0db": pcm5122_analog_gain.PCM5122_ANALOG_GAIN_0DB,
"-6db": pcm5122_analog_gain.PCM5122_ANALOG_GAIN_MINUS_6DB,
}
pcm5122_channel_mix = pcm5122_ns.enum("PCM5122ChannelMix")
PCM5122_CHANNEL_MIX_ENUM = {
"stereo": pcm5122_channel_mix.PCM5122_CHANNEL_MIX_STEREO,
"left": pcm5122_channel_mix.PCM5122_CHANNEL_MIX_LEFT_ONLY,
"right": pcm5122_channel_mix.PCM5122_CHANNEL_MIX_RIGHT_ONLY,
"swapped": pcm5122_channel_mix.PCM5122_CHANNEL_MIX_SWAPPED,
}
_validate_bits = cv.float_with_unit("bits", "bit")
def _validate_volume_range(config):
if config[CONF_VOLUME_MIN_DB] >= config[CONF_VOLUME_MAX_DB]:
raise cv.Invalid(f"{CONF_VOLUME_MIN_DB} must be less than {CONF_VOLUME_MAX_DB}")
return config
PCM5122GPIOPin = pcm5122_ns.class_(
"PCM5122GPIOPin",
cg.GPIOPin,
cg.Parented.template(PCM5122),
)
CONFIG_SCHEMA = (
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(PCM5122),
cv.Optional(CONF_BITS_PER_SAMPLE, default="16bit"): cv.All(
_validate_bits, cv.enum(PCM5122_BITS_PER_SAMPLE_ENUM)
),
cv.Optional(CONF_ANALOG_GAIN, default="0db"): cv.enum(
PCM5122_ANALOG_GAIN_ENUM, lower=True
),
cv.Optional(CONF_CHANNEL_MIX, default="stereo"): cv.enum(
PCM5122_CHANNEL_MIX_ENUM, lower=True
),
cv.Optional(CONF_VOLUME_MIN_DB, default="-52.5dB"): cv.All(
cv.decibel, cv.float_range(min=-103.0, max=24.0)
),
cv.Optional(CONF_VOLUME_MAX_DB, default="0dB"): cv.All(
cv.decibel, cv.float_range(min=-103.0, max=24.0)
),
cv.Optional(CONF_ENABLE_PIN): pins.gpio_output_pin_schema,
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(i2c.i2c_device_schema(0x4D))
.extend(i2c.i2c_device_schema(0x4D)),
_validate_volume_range,
)
@@ -96,3 +136,10 @@ async def to_code(config):
await i2c.register_i2c_device(var, config)
cg.add(var.set_bits_per_sample(config[CONF_BITS_PER_SAMPLE]))
cg.add(var.set_analog_gain(config[CONF_ANALOG_GAIN]))
cg.add(var.set_channel_mix(config[CONF_CHANNEL_MIX]))
cg.add(var.set_volume_min_db(config[CONF_VOLUME_MIN_DB]))
cg.add(var.set_volume_max_db(config[CONF_VOLUME_MAX_DB]))
if enable_pin_config := config.get(CONF_ENABLE_PIN):
enable_pin = await cg.gpio_pin_expression(enable_pin_config)
cg.add(var.set_enable_pin(enable_pin))
+99 -5
View File
@@ -10,6 +10,12 @@ namespace esphome::pcm5122 {
static const char *const TAG = "pcm5122";
void PCM5122::setup() {
// Hold XSMT low (soft mute asserted) until init completes
if (this->enable_pin_ != nullptr) {
this->enable_pin_->setup();
this->enable_pin_->digital_write(false);
}
// Select page 0 and verify chip presence via I2C ACK
if (!this->select_page_(0)) {
ESP_LOGE(TAG, "Write failed");
@@ -51,7 +57,22 @@ void PCM5122::setup() {
}
this->reg(PCM5122_REG_AUDIO_FORMAT) = PCM5122_AUDIO_FORMAT_I2S | alen;
if (!this->write_channel_mix_()) {
this->mark_failed();
return;
}
if (!this->write_analog_gain_()) {
this->mark_failed();
return;
}
// PLL reference clock: BCK
if (!this->select_page_(0)) {
ESP_LOGE(TAG, "Write failed");
this->mark_failed();
return;
}
optional<uint8_t> pll_ref = this->read_byte(PCM5122_REG_PLL_REF);
if (!pll_ref.has_value()) {
ESP_LOGE(TAG, "Failed to read PLL_REF");
@@ -67,15 +88,40 @@ void PCM5122::setup() {
this->mark_failed();
return;
}
// Release XSMT (soft un-mute) now that init has completed
if (this->enable_pin_ != nullptr) {
this->enable_pin_->digital_write(true);
}
}
void PCM5122::dump_config() {
const char *channel_mix_str;
switch (this->channel_mix_) {
case PCM5122_CHANNEL_MIX_LEFT_ONLY:
channel_mix_str = "left only";
break;
case PCM5122_CHANNEL_MIX_RIGHT_ONLY:
channel_mix_str = "right only";
break;
case PCM5122_CHANNEL_MIX_SWAPPED:
channel_mix_str = "swapped";
break;
default:
channel_mix_str = "stereo";
break;
}
ESP_LOGCONFIG(TAG, "Audio DAC:");
LOG_I2C_DEVICE(this);
ESP_LOGCONFIG(TAG,
" Bits per sample: %u\n"
" Analog gain: %s\n"
" Channel mix: %s\n"
" Volume range: %.1f dB to %.1f dB\n"
" Muted: %s",
this->bits_per_sample_, YESNO(this->is_muted_));
this->bits_per_sample_, this->analog_gain_ == PCM5122_ANALOG_GAIN_0DB ? "0 dB" : "-6 dB",
channel_mix_str, this->volume_min_db_, this->volume_max_db_, YESNO(this->is_muted_));
LOG_PIN(" Enable Pin: ", this->enable_pin_);
}
bool PCM5122::set_mute_off() {
@@ -118,11 +164,11 @@ bool PCM5122::write_mute_() {
}
bool PCM5122::write_volume_() {
// DVOL register: 0x00 = +24 dB, 0x30 = 0 dB, 0xFF = mute (-0.5 dB/step).
// Note: volume=0.0 maps to -52.5 dB (still audible), not true silence.
// DVOL register: 0x00 = +24 dB, 0x30 = 0 dB, 0xFE = -103 dB, 0xFF = mute (-0.5 dB/step).
// Note: volume=0.0 maps to volume_min_db_, which is not true silence unless set to -103 dB.
// Use set_mute_on() for silence.
const uint8_t dvol_max_volume = 0x30; // 0 dB at full scale
const uint8_t dvol_min_volume = 0x99; // -52.5 dB at minimum
const uint8_t dvol_max_volume = static_cast<uint8_t>(lroundf(0x30 - this->volume_max_db_ * 2.0f));
const uint8_t dvol_min_volume = static_cast<uint8_t>(lroundf(0x30 - this->volume_min_db_ * 2.0f));
const uint8_t volume_byte =
dvol_max_volume + static_cast<uint8_t>(lroundf((1.0f - this->volume_) * (dvol_min_volume - dvol_max_volume)));
@@ -137,4 +183,52 @@ bool PCM5122::write_volume_() {
return true;
}
bool PCM5122::write_analog_gain_() {
uint8_t gain_byte = this->analog_gain_;
if (!this->select_page_(1) || !this->write_byte(PCM5122_REG_ANALOG_GAIN, gain_byte)) {
ESP_LOGE(TAG, "Writing analog gain failed");
return false;
}
return true;
}
bool PCM5122::write_channel_mix_() {
uint8_t channel_mix_byte = this->channel_mix_;
if (!this->select_page_(0) || !this->write_byte(PCM5122_REG_DAC_DATA_PATH, channel_mix_byte)) {
ESP_LOGE(TAG, "Writing channel mix failed");
return false;
}
return true;
}
bool PCM5122::set_standby(bool enable) {
bool prev_standby = this->standby_;
this->standby_ = enable;
if (!this->write_power_control_()) {
this->standby_ = prev_standby;
return false;
}
return true;
}
bool PCM5122::set_powerdown(bool enable) {
bool prev_powerdown = this->powerdown_;
this->powerdown_ = enable;
if (!this->write_power_control_()) {
this->powerdown_ = prev_powerdown;
return false;
}
return true;
}
bool PCM5122::write_power_control_() {
uint8_t power_byte =
(this->standby_ ? PCM5122_POWER_CONTROL_RQST : 0) | (this->powerdown_ ? PCM5122_POWER_CONTROL_RQPD : 0);
if (!this->select_page_(0) || !this->write_byte(PCM5122_REG_POWER_CONTROL, power_byte)) {
ESP_LOGE(TAG, "Writing power control failed");
return false;
}
return true;
}
} // namespace esphome::pcm5122
+47 -2
View File
@@ -3,6 +3,7 @@
#include "esphome/components/audio_dac/audio_dac.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/component.h"
#include "esphome/core/gpio.h"
#include "esphome/core/hal.h"
namespace esphome::pcm5122 {
@@ -10,11 +11,13 @@ namespace esphome::pcm5122 {
// Page 0 register addresses
static const uint8_t PCM5122_REG_PAGE_SELECT = 0x00;
static const uint8_t PCM5122_REG_RESET = 0x01;
static const uint8_t PCM5122_REG_POWER_CONTROL = 0x02;
static const uint8_t PCM5122_REG_MUTE = 0x03;
static const uint8_t PCM5122_REG_GPIO_ENABLE = 0x08;
static const uint8_t PCM5122_REG_PLL_REF = 0x0D;
static const uint8_t PCM5122_REG_ERROR_DETECT = 0x25;
static const uint8_t PCM5122_REG_AUDIO_FORMAT = 0x28;
static const uint8_t PCM5122_REG_DAC_DATA_PATH = 0x2A;
static const uint8_t PCM5122_REG_DVOL_LEFT = 0x3D;
static const uint8_t PCM5122_REG_DVOL_RIGHT = 0x3E;
static const uint8_t PCM5122_REG_GPIO_OUTPUT_SELECT = 0x50; // Base address; GPIO n uses offset n-1
@@ -23,6 +26,9 @@ static const uint8_t PCM5122_REG_GPIO_OUTPUT = 0x56;
static const uint8_t PCM5122_REG_GPIO_INVERT = 0x57;
static const uint8_t PCM5122_REG_GPIO_INPUT = 0x77;
// Page 1 register addresses
static const uint8_t PCM5122_REG_ANALOG_GAIN = 0x02;
// Register values for init sequence
static const uint8_t PCM5122_RESET_MODULES = 0x10; // RSTM: reset audio modules
static const uint8_t PCM5122_AUDIO_FORMAT_I2S = 0x00; // AFMT = I2S (bits [5:4] = 00)
@@ -35,12 +41,33 @@ static const uint8_t PCM5122_ERROR_DETECT_DISABLE_DIV_AUTOSET = (1 << 1);
static const uint8_t PCM5122_PLL_REF_MASK = (7 << 4); // SREF bits [6:4]
static const uint8_t PCM5122_PLL_REF_SOURCE_BCK = (1 << 4); // SREF = 001 (BCK)
// Page 0, Register 2 (Power Control): RQST = standby request, RQPD = powerdown request (§10.5.3)
static const uint8_t PCM5122_POWER_CONTROL_RQST = (1 << 4);
static const uint8_t PCM5122_POWER_CONTROL_RQPD = (1 << 0);
// Page 1, Register 2 (Analog Gain Control): LAGN/RAGN select 0 dB or -6 dB analog gain (§8.3.5.5)
static const uint8_t PCM5122_ANALOG_GAIN_LAGN = (1 << 4);
static const uint8_t PCM5122_ANALOG_GAIN_RAGN = (1 << 0);
enum PCM5122BitsPerSample : uint8_t {
PCM5122_BITS_PER_SAMPLE_16 = 16,
PCM5122_BITS_PER_SAMPLE_24 = 24,
PCM5122_BITS_PER_SAMPLE_32 = 32,
};
enum PCM5122AnalogGain : uint8_t {
PCM5122_ANALOG_GAIN_0DB = 0x00,
PCM5122_ANALOG_GAIN_MINUS_6DB = PCM5122_ANALOG_GAIN_LAGN | PCM5122_ANALOG_GAIN_RAGN,
};
// Page 0, Register 0x2A (DAC Data Path): AUPL/AUPR select which channel's data feeds each output (§7.4.2.42)
enum PCM5122ChannelMix : uint8_t {
PCM5122_CHANNEL_MIX_STEREO = 0x11, // Left data -> left out, right data -> right out
PCM5122_CHANNEL_MIX_LEFT_ONLY = 0x12, // Left data -> both outputs
PCM5122_CHANNEL_MIX_RIGHT_ONLY = 0x21, // Right data -> both outputs
PCM5122_CHANNEL_MIX_SWAPPED = 0x22, // Left/right outputs swapped
};
class PCM5122 final : public audio_dac::AudioDac, public Component, public i2c::I2CDevice {
public:
void setup() override;
@@ -48,6 +75,11 @@ class PCM5122 final : public audio_dac::AudioDac, public Component, public i2c::
float get_setup_priority() const override { return setup_priority::IO; }
void set_bits_per_sample(PCM5122BitsPerSample bits_per_sample) { this->bits_per_sample_ = bits_per_sample; }
void set_analog_gain(PCM5122AnalogGain analog_gain) { this->analog_gain_ = analog_gain; }
void set_channel_mix(PCM5122ChannelMix channel_mix) { this->channel_mix_ = channel_mix; }
void set_volume_min_db(float volume_min_db) { this->volume_min_db_ = volume_min_db; }
void set_volume_max_db(float volume_max_db) { this->volume_max_db_ = volume_max_db; }
void set_enable_pin(GPIOPin *enable_pin) { this->enable_pin_ = enable_pin; }
bool set_mute_off() override;
bool set_mute_on() override;
@@ -56,17 +88,30 @@ class PCM5122 final : public audio_dac::AudioDac, public Component, public i2c::
bool is_muted() override;
float volume() override;
bool set_standby(bool enable);
bool set_powerdown(bool enable);
friend class PCM5122GPIOPin;
protected:
bool select_page_(uint8_t page);
bool write_mute_();
bool write_volume_();
bool write_analog_gain_();
bool write_channel_mix_();
bool write_power_control_();
float volume_{1.0f}; // Matches chip post-reset DVOL default (0x30 = 0 dB)
int16_t current_page_{-1}; // -1 = unknown; cached to skip redundant page-select writes
GPIOPin *enable_pin_{nullptr};
float volume_{1.0f}; // Matches chip post-reset DVOL default (0x30 = 0 dB)
float volume_min_db_{-52.5f}; // Matches the previous hardcoded minimum (0x99)
float volume_max_db_{0.0f}; // Matches the previous hardcoded maximum (0x30)
int16_t current_page_{-1}; // -1 = unknown; cached to skip redundant page-select writes
bool is_muted_{false};
bool standby_{false};
bool powerdown_{false};
PCM5122BitsPerSample bits_per_sample_{PCM5122_BITS_PER_SAMPLE_16};
PCM5122AnalogGain analog_gain_{PCM5122_ANALOG_GAIN_0DB};
PCM5122ChannelMix channel_mix_{PCM5122_CHANNEL_MIX_STEREO};
};
} // namespace esphome::pcm5122
@@ -0,0 +1,32 @@
import esphome.codegen as cg
from esphome.components import switch
import esphome.config_validation as cv
from esphome.const import CONF_POWER_MODE, ENTITY_CATEGORY_CONFIG
from ..audio_dac import CONF_PCM5122, PCM5122, pcm5122_ns
PCM5122PowerSwitch = pcm5122_ns.class_("PCM5122PowerSwitch", switch.Switch)
pcm5122_power_switch_mode = pcm5122_ns.enum("PCM5122PowerSwitchMode")
PCM5122_POWER_SWITCH_MODE_ENUM = {
"standby": pcm5122_power_switch_mode.PCM5122_POWER_SWITCH_MODE_STANDBY,
"powerdown": pcm5122_power_switch_mode.PCM5122_POWER_SWITCH_MODE_POWERDOWN,
}
CONFIG_SCHEMA = switch.switch_schema(
PCM5122PowerSwitch,
entity_category=ENTITY_CATEGORY_CONFIG,
).extend(
{
cv.GenerateID(CONF_PCM5122): cv.use_id(PCM5122),
cv.Optional(CONF_POWER_MODE, default="powerdown"): cv.enum(
PCM5122_POWER_SWITCH_MODE_ENUM, lower=True
),
}
)
async def to_code(config):
var = await switch.new_switch(config)
await cg.register_parented(var, config[CONF_PCM5122])
cg.add(var.set_power_mode(config[CONF_POWER_MODE]))
@@ -0,0 +1,12 @@
#include "power_switch.h"
namespace esphome::pcm5122 {
void PCM5122PowerSwitch::write_state(bool state) {
bool ok = (this->mode_ == PCM5122_POWER_SWITCH_MODE_STANDBY) ? this->parent_->set_standby(state)
: this->parent_->set_powerdown(state);
if (ok)
this->publish_state(state);
}
} // namespace esphome::pcm5122
@@ -0,0 +1,24 @@
#pragma once
#include "esphome/components/switch/switch.h"
#include "../pcm5122.h"
namespace esphome::pcm5122 {
enum PCM5122PowerSwitchMode : uint8_t {
PCM5122_POWER_SWITCH_MODE_STANDBY,
PCM5122_POWER_SWITCH_MODE_POWERDOWN,
};
class PCM5122PowerSwitch final : public switch_::Switch, public Parented<PCM5122> {
public:
void set_power_mode(PCM5122PowerSwitchMode mode) { this->mode_ = mode; }
protected:
void write_state(bool state) override;
PCM5122PowerSwitchMode mode_{PCM5122_POWER_SWITCH_MODE_POWERDOWN};
};
} // namespace esphome::pcm5122
+38 -14
View File
@@ -3,6 +3,7 @@
#include "usb_uart.h"
#include "usb/usb_host.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#include "esphome/components/uart/uart_debugger.h"
#include "esphome/components/bytebuffer/bytebuffer.h"
@@ -396,7 +397,14 @@ int USBUartTypeFT23XX::set_dtr_rts_(USBUartChannel *channel) {
}
void USBUartTypeFT23XX::start_input(USBUartChannel *channel) {
if (!channel->initialised_.load() || channel->input_started_.load())
if (!channel->initialised_.load())
return;
// Use compare_exchange_strong to avoid a check-then-act race: start_input() is called
// from both the USB task (self-restart on success) and the main loop (backpressure
// restart), so a plain load()/store() pair can let both threads submit a transfer.
auto started = false;
if (!channel->input_started_.compare_exchange_strong(started, true))
return;
const auto *ep = channel->cdc_dev_.in_ep;
@@ -408,39 +416,55 @@ void USBUartTypeFT23XX::start_input(USBUartChannel *channel) {
return;
}
// FTDI prepends a 2-byte modem/line status header to every bulk IN packet.
size_t uart_data_len = (status.data_len > 2) ? (status.data_len - 2) : 0;
if (uart_data_len > 0) {
ESP_LOGV(TAG, "RX callback: Received %zu bytes, channel=%d", uart_data_len, channel->index_);
if (!channel->dummy_receiver_) {
// Copy the entire received UART payload into the ring buffer in one
// operation to avoid per-byte overhead and reduce the chance of
// heap activity in hot paths.
channel->input_buffer_.push(status.data + 2, uart_data_len);
UsbDataChunk *chunk = this->chunk_pool_.allocate();
if (chunk == nullptr) {
this->usb_data_queue_.increment_dropped_count();
channel->input_started_.store(false);
// Queue is full — wake the main loop to drain it, then let read_array()
// retrigger start_input() rather than spinning here in the USB task.
this->enable_loop_soon_any_context();
App.wake_loop_threadsafe();
return;
}
// Strip the 2-byte FTDI header before queuing.
memcpy(chunk->data, status.data + 2, uart_data_len);
chunk->length = static_cast<uint16_t>(uart_data_len);
chunk->channel = channel;
this->usb_data_queue_.push(chunk);
#ifdef USE_UART_DEBUGGER
if (channel->debug_) {
// Debug path creates a temporary vector for logging only; this is
// acceptable because debug mode is opt-in and not used in release.
uart::UARTDebug::log_hex(uart::UART_DIRECTION_RX,
std::vector<uint8_t>(status.data + 2, status.data + 2 + uart_data_len), ',',
channel->debug_prefix_);
}
#endif
this->enable_loop_soon_any_context();
App.wake_loop_threadsafe();
}
} else {
} else if (status.data_len >= 2) {
ESP_LOGVV(TAG, "RX: Status packet, modem=0x%02X line=0x%02X, ch=%d", status.data[0], status.data[1],
channel->index_);
}
channel->input_started_.store(false);
if (channel->dummy_receiver_ ||
channel->input_buffer_.get_free_space() >= channel->cdc_dev_.in_ep->wMaxPacketSize) {
this->start_input(channel);
}
this->start_input(channel);
};
channel->input_started_.store(true);
this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize);
if (!this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize)) {
ESP_LOGE(TAG, "RX transfer submission failed for ep=0x%02X", ep->bEndpointAddress);
channel->input_started_.store(false);
}
}
void USBUartTypeFT23XX::on_rx_overflow(USBUartChannel *channel) {
ESP_LOGW(TAG, "RX buffer overflow on channel %d, clearing to resync", channel->index_);
channel->input_buffer_.clear();
}
void USBUartTypeFT23XX::enable_channels() {
+5
View File
@@ -228,6 +228,11 @@ void USBUartComponent::loop() {
}
#endif
// If there is not enough space for the full chunk, let the device subclass
// handle it (e.g. FTDI clears the buffer to prevent mid-telegram corruption).
if (channel->input_buffer_.get_free_space() < chunk->length) {
this->on_rx_overflow(channel);
}
// Push data to ring buffer (now safe in main loop)
channel->input_buffer_.push(chunk->data, chunk->length);
+7 -2
View File
@@ -192,9 +192,13 @@ class USBUartComponent : public usb_host::USBClient {
void add_channel(USBUartChannel *channel) { this->channels_.push_back(channel); }
void start_input(USBUartChannel *channel);
virtual void start_input(USBUartChannel *channel);
void start_output(USBUartChannel *channel);
// Called from loop() when input_buffer_ has insufficient space for the incoming chunk.
// Default is a no-op; override in device-specific subclasses that need resync on overflow.
virtual void on_rx_overflow(USBUartChannel *channel) {}
// Lock-free data transfer from USB task to main loop
static constexpr int USB_DATA_QUEUE_SIZE = 32;
LockFreeQueue<UsbDataChunk, USB_DATA_QUEUE_SIZE> usb_data_queue_;
@@ -248,7 +252,8 @@ class USBUartTypeFT23XX : public USBUartTypeCdcAcm {
public:
USBUartTypeFT23XX(uint16_t vid, uint16_t pid) : USBUartTypeCdcAcm(vid, pid) {}
void start_input(USBUartChannel *channel);
void start_input(USBUartChannel *channel) override;
void on_rx_overflow(USBUartChannel *channel) override;
protected:
std::vector<CdcEps> parse_descriptors(usb_device_handle_t dev_hdl) override;
+14 -12
View File
@@ -257,8 +257,10 @@ void DeferredUpdateEventSource::deferrable_send_state(void *source, const char *
}
// used for logs plus the initial ping/config
void DeferredUpdateEventSource::try_send_nodefer(const char *message, const char *event, uint32_t id,
uint32_t reconnect) {
void DeferredUpdateEventSource::try_send_nodefer(const char *message, size_t message_len, const char *event,
uint32_t id, uint32_t reconnect) {
// ESPAsyncWebServer's send() only accepts null-terminated strings
(void) message_len;
this->send(message, event, id, reconnect);
}
@@ -279,10 +281,10 @@ void DeferredUpdateEventSourceList::deferrable_send_state(void *source, const ch
}
}
void DeferredUpdateEventSourceList::try_send_nodefer(const char *message, const char *event, uint32_t id,
uint32_t reconnect) {
void DeferredUpdateEventSourceList::try_send_nodefer(const char *message, size_t message_len, const char *event,
uint32_t id, uint32_t reconnect) {
for (DeferredUpdateEventSource *dues : *this) {
dues->try_send_nodefer(message, event, id, reconnect);
dues->try_send_nodefer(message, message_len, event, id, reconnect);
}
}
@@ -304,7 +306,7 @@ void DeferredUpdateEventSourceList::on_client_connect_(DeferredUpdateEventSource
// Configure reconnect timeout and send config
// this should always go through since the AsyncEventSourceClient event queue is empty on connect
auto message = ws->get_config_json();
source->try_send_nodefer(message.c_str(), "ping", millis(), 30000);
source->try_send_nodefer(message.c_str(), message.size(), "ping", millis(), 30000);
#ifdef USE_WEBSERVER_SORTING
for (auto &group : ws->sorting_groups_) {
@@ -315,7 +317,7 @@ void DeferredUpdateEventSourceList::on_client_connect_(DeferredUpdateEventSource
auto group_msg = builder.serialize();
// up to 31 groups should be able to be queued initially without defer
source->try_send_nodefer(group_msg.c_str(), "sorting_group");
source->try_send_nodefer(group_msg.c_str(), group_msg.size(), "sorting_group");
}
#endif
@@ -395,8 +397,8 @@ void WebServer::setup() {
return;
char buf[32];
auto uptime = static_cast<uint32_t>(millis_64() / 1000);
buf_append_printf(buf, sizeof(buf), 0, "{\"uptime\":%" PRIu32 "}", uptime);
this->events_.try_send_nodefer(buf, "ping", millis(), 30000);
size_t len = buf_append_printf(buf, sizeof(buf), 0, "{\"uptime\":%" PRIu32 "}", uptime);
this->events_.try_send_nodefer(buf, len, "ping", millis(), 30000);
});
}
void WebServer::loop() {
@@ -414,16 +416,16 @@ void WebServer::loop() {
void WebServer::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) {
(void) level;
(void) tag;
(void) message_len;
this->events_.try_send_nodefer(message, "log", millis());
this->events_.try_send_nodefer(message, message_len, "log", millis());
}
#endif
void WebServer::dump_config() {
char addr_buf[network::USE_ADDRESS_BUFFER_SIZE];
ESP_LOGCONFIG(TAG,
"Web Server:\n"
" Address: %s:%u",
network::get_use_address(), this->base_->get_port());
network::get_use_address_to(addr_buf), this->base_->get_port());
}
float WebServer::get_setup_priority() const { return setup_priority::WIFI - 1.0f; }
+4 -2
View File
@@ -160,7 +160,8 @@ class DeferredUpdateEventSource final : public AsyncEventSource {
void loop();
void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator);
void try_send_nodefer(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0);
void try_send_nodefer(const char *message, size_t message_len, const char *event = nullptr, uint32_t id = 0,
uint32_t reconnect = 0);
};
class DeferredUpdateEventSourceList final : public std::list<DeferredUpdateEventSource *> {
@@ -173,7 +174,8 @@ class DeferredUpdateEventSourceList final : public std::list<DeferredUpdateEvent
bool loop();
void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator);
void try_send_nodefer(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0);
void try_send_nodefer(const char *message, size_t message_len, const char *event = nullptr, uint32_t id = 0,
uint32_t reconnect = 0);
void add_new_client(WebServer *ws, AsyncWebServerRequest *request);
};
@@ -546,10 +546,11 @@ void AsyncEventSource::adopt_pending_sessions_main_loop_() {
}
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
void AsyncEventSource::try_send_nodefer(const char *message, const char *event, uint32_t id, uint32_t reconnect) {
void AsyncEventSource::try_send_nodefer(const char *message, size_t message_len, const char *event, uint32_t id,
uint32_t reconnect) {
for (auto *ses : this->sessions_) {
if (ses->fd_.load() != 0) { // Skip dead sessions
ses->try_send_nodefer(message, event, id, reconnect);
ses->try_send_nodefer(message, message_len, event, id, reconnect);
}
}
}
@@ -600,7 +601,7 @@ void AsyncEventSourceResponse::start_session_main_loop_() {
// tcp send buffer is empty on connect, so these should always go through
auto message = ws->get_config_json();
this->try_send_nodefer(message.c_str(), "ping", millis(), 30000);
this->try_send_nodefer(message.c_str(), message.size(), "ping", millis(), 30000);
#ifdef USE_WEBSERVER_SORTING
for (auto &group : ws->sorting_groups_) {
@@ -612,7 +613,7 @@ void AsyncEventSourceResponse::start_session_main_loop_() {
// a (very) large number of these should be able to be queued initially without defer
// since the only thing in the send buffer at this point is the initial ping/config
this->try_send_nodefer(message.c_str(), "sorting_group");
this->try_send_nodefer(message.c_str(), message.size(), "sorting_group");
}
#endif
@@ -647,7 +648,7 @@ void AsyncEventSourceResponse::process_deferred_queue_() {
while (!deferred_queue_.empty()) {
DeferredEvent &de = deferred_queue_.front();
auto message = de.message_generator_(web_server_, de.source_);
if (this->try_send_nodefer(message.c_str(), "state")) {
if (this->try_send_nodefer(message.c_str(), message.size(), "state")) {
// O(n) but memory efficiency is more important than speed here which is why std::vector was chosen
deferred_queue_.erase(deferred_queue_.begin());
} else {
@@ -718,7 +719,7 @@ void AsyncEventSourceResponse::loop() {
this->entities_iterator_.advance();
}
bool AsyncEventSourceResponse::try_send_nodefer(const char *message, const char *event, uint32_t id,
bool AsyncEventSourceResponse::try_send_nodefer(const char *message, size_t message_len, const char *event, uint32_t id,
uint32_t reconnect) {
if (this->fd_.load() == 0) {
return false;
@@ -764,19 +765,18 @@ bool AsyncEventSourceResponse::try_send_nodefer(const char *message, const char
// Fast path: check if message contains any newlines at all
// Most SSE messages (JSON state updates) have no newlines
const char *first_n = strchr(message, '\n');
const char *first_r = strchr(message, '\r');
const char *first_n = static_cast<const char *>(memchr(message, '\n', message_len));
const char *first_r = static_cast<const char *>(memchr(message, '\r', message_len));
if (first_n == nullptr && first_r == nullptr) {
// No newlines - fast path (most common case)
event_buffer_.append("data: ", sizeof("data: ") - 1);
event_buffer_.append(message);
event_buffer_.append(message, message_len);
event_buffer_.append(CRLF_STR CRLF_STR, CRLF_LEN * 2); // data line + blank line terminator
} else {
// Has newlines - handle multi-line message
const char *line_start = message;
size_t msg_len = strlen(message);
const char *msg_end = message + msg_len;
const char *msg_end = message + message_len;
// Reuse the first search results
const char *next_n = first_n;
@@ -789,7 +789,7 @@ bool AsyncEventSourceResponse::try_send_nodefer(const char *message, const char
if (next_n == nullptr && next_r == nullptr) {
// No more line breaks - output remaining text as final line
event_buffer_.append("data: ", sizeof("data: ") - 1);
event_buffer_.append(line_start);
event_buffer_.append(line_start, msg_end - line_start);
event_buffer_.append(CRLF_STR, CRLF_LEN);
break;
}
@@ -828,8 +828,8 @@ bool AsyncEventSourceResponse::try_send_nodefer(const char *message, const char
}
// Search for next newlines only in remaining string
next_n = strchr(line_start, '\n');
next_r = strchr(line_start, '\r');
next_n = static_cast<const char *>(memchr(line_start, '\n', msg_end - line_start));
next_r = static_cast<const char *>(memchr(line_start, '\r', msg_end - line_start));
}
// Terminate message with blank line
@@ -884,7 +884,7 @@ void AsyncEventSourceResponse::deferrable_send_state(void *source, const char *e
deq_push_back_with_dedup_(source, message_generator);
} else {
auto message = message_generator(web_server_, source);
if (!this->try_send_nodefer(message.c_str(), "state")) {
if (!this->try_send_nodefer(message.c_str(), message.size(), "state")) {
deq_push_back_with_dedup_(source, message_generator);
}
}
@@ -291,7 +291,8 @@ class AsyncEventSourceResponse {
friend class AsyncEventSource;
public:
bool try_send_nodefer(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0);
bool try_send_nodefer(const char *message, size_t message_len, const char *event = nullptr, uint32_t id = 0,
uint32_t reconnect = 0);
void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator);
void loop();
@@ -343,7 +344,8 @@ class AsyncEventSource : public AsyncWebHandler {
// NOLINTNEXTLINE(readability-identifier-naming)
void onConnect(connect_handler_t &&cb) { this->on_connect_ = std::move(cb); }
void try_send_nodefer(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0);
void try_send_nodefer(const char *message, size_t message_len, const char *event = nullptr, uint32_t id = 0,
uint32_t reconnect = 0);
void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator);
/// Returns true if there are sessions remaining (including pending cleanup).
bool loop();
+2 -1
View File
@@ -14,6 +14,7 @@ from esphome.components.esp32 import (
request_wifi,
)
from esphome.components.network import (
add_use_address,
has_high_performance_networking,
ip_address_literal,
)
@@ -585,7 +586,7 @@ def wifi_network(config, ap, static_ip):
@coroutine_with_priority(CoroPriority.COMMUNICATION)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_use_address(config[CONF_USE_ADDRESS]))
add_use_address(var, config[CONF_USE_ADDRESS])
# Track if any network uses Enterprise authentication
has_eap = False
+3 -1
View File
@@ -501,6 +501,8 @@ class WiFiComponent final : public Component {
network::IPAddress get_dns_address(int num);
network::IPAddresses get_ip_addresses();
/// Returns nullptr when no explicit use_address is configured and the address is
/// derived at runtime from the device name (see network::get_use_address_to()).
const char *get_use_address() const { return this->use_address_; }
void set_use_address(const char *use_address) { this->use_address_ = use_address; }
@@ -996,7 +998,7 @@ class WiFiComponent final : public Component {
private:
// Stores a pointer to a string literal (static storage duration).
// ONLY set from Python-generated code with string literals - never dynamic strings.
const char *use_address_{""};
const char *use_address_{nullptr};
};
extern WiFiComponent *global_wifi_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
+1 -1
View File
@@ -8,7 +8,7 @@ namespace esphome::wts01 {
constexpr uint8_t PACKET_SIZE = 9;
class WTS01Sensor : public sensor::Sensor, public uart::UARTDevice, public Component {
class WTS01Sensor final : public sensor::Sensor, public uart::UARTDevice, public Component {
public:
void loop() override;
void dump_config() override;
+1 -1
View File
@@ -6,7 +6,7 @@
namespace esphome::x9c {
class X9cOutput : public output::FloatOutput, public Component {
class X9cOutput final : public output::FloatOutput, public Component {
public:
void set_cs_pin(InternalGPIOPin *pin) { cs_pin_ = pin; }
void set_inc_pin(InternalGPIOPin *pin) { inc_pin_ = pin; }
+1 -1
View File
@@ -6,7 +6,7 @@
namespace esphome::xdb401 {
class XDB401Component : public PollingComponent, public i2c::I2CDevice {
class XDB401Component final : public PollingComponent, public i2c::I2CDevice {
public:
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; }
void set_pressure_sensor(sensor::Sensor *pressure_sensor) { this->pressure_sensor_ = pressure_sensor; }
+1 -1
View File
@@ -20,7 +20,7 @@ enum XGZP68XXOversampling : uint8_t {
XGZP68XX_OVERSAMPLING_UNKNOWN = (uint8_t) -1,
};
class XGZP68XXComponent : public PollingComponent, public sensor::Sensor, public i2c::I2CDevice {
class XGZP68XXComponent final : public PollingComponent, public sensor::Sensor, public i2c::I2CDevice {
public:
SUB_SENSOR(temperature)
SUB_SENSOR(pressure)
+1 -1
View File
@@ -72,7 +72,7 @@ optional<XiaomiParseResult> parse_xiaomi_header(const esp32_ble_tracker::Service
bool decrypt_xiaomi_payload(std::vector<uint8_t> &raw, const uint8_t *bindkey, const uint64_t &address);
bool report_xiaomi_results(const optional<XiaomiParseResult> &result, const char *address);
class XiaomiListener : public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiListener final : public esp32_ble_tracker::ESPBTDeviceListener {
public:
bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
};
@@ -9,7 +9,7 @@
namespace esphome::xiaomi_cgdk2 {
class XiaomiCGDK2 : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiCGDK2 final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { address_ = address; };
void set_bindkey(const char *bindkey);
+1 -1
View File
@@ -9,7 +9,7 @@
namespace esphome::xiaomi_cgg1 {
class XiaomiCGG1 : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiCGG1 final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { address_ = address; }
void set_bindkey(const char *bindkey);
@@ -10,9 +10,9 @@
namespace esphome::xiaomi_cgpr1 {
class XiaomiCGPR1 : public Component,
public binary_sensor::BinarySensorInitiallyOff,
public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiCGPR1 final : public Component,
public binary_sensor::BinarySensorInitiallyOff,
public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { address_ = address; }
void set_bindkey(const char *bindkey);
@@ -9,7 +9,7 @@
namespace esphome::xiaomi_gcls002 {
class XiaomiGCLS002 : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiGCLS002 final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { address_ = address; }
@@ -9,7 +9,7 @@
namespace esphome::xiaomi_hhccjcy01 {
class XiaomiHHCCJCY01 : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiHHCCJCY01 final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { address_ = address; }
@@ -8,7 +8,7 @@
namespace esphome::xiaomi_hhccjcy10 {
class XiaomiHHCCJCY10 : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiHHCCJCY10 final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { this->address_ = address; }
@@ -9,7 +9,7 @@
namespace esphome::xiaomi_hhccpot002 {
class XiaomiHHCCPOT002 : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiHHCCPOT002 final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { address_ = address; }
@@ -9,7 +9,7 @@
namespace esphome::xiaomi_jqjcy01ym {
class XiaomiJQJCY01YM : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiJQJCY01YM final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { address_ = address; }
@@ -9,7 +9,7 @@
namespace esphome::xiaomi_lywsd02 {
class XiaomiLYWSD02 : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiLYWSD02 final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { address_ = address; }
@@ -9,7 +9,7 @@
namespace esphome::xiaomi_lywsd02mmc {
class XiaomiLYWSD02MMC : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiLYWSD02MMC final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { this->address_ = address; }
void set_bindkey(const char *bindkey);
@@ -9,7 +9,7 @@
namespace esphome::xiaomi_lywsd03mmc {
class XiaomiLYWSD03MMC : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiLYWSD03MMC final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { address_ = address; };
void set_bindkey(const char *bindkey);
@@ -9,7 +9,7 @@
namespace esphome::xiaomi_lywsdcgq {
class XiaomiLYWSDCGQ : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiLYWSDCGQ final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { address_ = address; }
@@ -9,7 +9,7 @@
namespace esphome::xiaomi_mhoc303 {
class XiaomiMHOC303 : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiMHOC303 final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { address_ = address; }
@@ -9,7 +9,7 @@
namespace esphome::xiaomi_mhoc401 {
class XiaomiMHOC401 : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiMHOC401 final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { address_ = address; };
void set_bindkey(const char *bindkey);
@@ -16,7 +16,7 @@ struct ParseResult {
optional<float> impedance;
};
class XiaomiMiscale : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiMiscale final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { address_ = address; };
@@ -10,9 +10,9 @@
namespace esphome::xiaomi_mjyd02yla {
class XiaomiMJYD02YLA : public Component,
public binary_sensor::BinarySensorInitiallyOff,
public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiMJYD02YLA final : public Component,
public binary_sensor::BinarySensorInitiallyOff,
public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { address_ = address; }
void set_bindkey(const char *bindkey);
@@ -9,9 +9,9 @@
namespace esphome::xiaomi_mue4094rt {
class XiaomiMUE4094RT : public Component,
public binary_sensor::BinarySensorInitiallyOff,
public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiMUE4094RT final : public Component,
public binary_sensor::BinarySensorInitiallyOff,
public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { address_ = address; }
@@ -15,7 +15,7 @@
namespace esphome::xiaomi_rtcgq02lm {
class XiaomiRTCGQ02LM : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiRTCGQ02LM final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { address_ = address; };
void set_bindkey(const char *bindkey);
@@ -10,9 +10,9 @@
namespace esphome::xiaomi_wx08zm {
class XiaomiWX08ZM : public Component,
public binary_sensor::BinarySensorInitiallyOff,
public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiWX08ZM final : public Component,
public binary_sensor::BinarySensorInitiallyOff,
public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { address_ = address; }
@@ -9,7 +9,7 @@
namespace esphome::xiaomi_xmwsdj04mmc {
class XiaomiXMWSDJ04MMC : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiXMWSDJ04MMC final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
public:
void set_address(uint64_t address) { this->address_ = address; }
void set_bindkey(const char *bindkey);
+2 -2
View File
@@ -17,7 +17,7 @@ enum {
XL9535_CONFIG_PORT_1_REGISTER = 0x07,
};
class XL9535Component : public Component, public i2c::I2CDevice {
class XL9535Component final : public Component, public i2c::I2CDevice {
public:
bool digital_read(uint8_t pin);
void digital_write(uint8_t pin, bool value);
@@ -28,7 +28,7 @@ class XL9535Component : public Component, public i2c::I2CDevice {
float get_setup_priority() const override { return setup_priority::IO; }
};
class XL9535GPIOPin : public GPIOPin {
class XL9535GPIOPin final : public GPIOPin {
public:
void set_parent(XL9535Component *parent) { this->parent_ = parent; }
void set_pin(uint8_t pin) { this->pin_ = pin; }
@@ -11,9 +11,9 @@ namespace esphome::xpt2046 {
using namespace touchscreen;
class XPT2046Component : public Touchscreen,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_2MHZ> {
class XPT2046Component final : public Touchscreen,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_2MHZ> {
public:
/// Set the threshold for the touch detection.
void set_threshold(int16_t threshold) { this->threshold_ = threshold; }
+1 -1
View File
@@ -9,7 +9,7 @@
namespace esphome::yashima {
class YashimaClimate : public climate::Climate, public Component {
class YashimaClimate final : public climate::Climate, public Component {
public:
void setup() override;
void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) {
+1 -1
View File
@@ -7,7 +7,7 @@
namespace esphome::zephyr {
class CdcAcm : public Component {
class CdcAcm final : public Component {
public:
CdcAcm();
void setup() override;
+1 -1
View File
@@ -16,7 +16,7 @@ struct ZephyrGPIOInterrupt {
void *arg{nullptr};
};
class ZephyrGPIOPin : public InternalGPIOPin {
class ZephyrGPIOPin final : public InternalGPIOPin {
public:
ZephyrGPIOPin(const device *gpio, int gpio_size, const char *pin_name_prefix) {
this->gpio_ = gpio;
@@ -6,7 +6,7 @@
namespace esphome::zephyr_ble_server {
class BLEServer : public Component {
class BLEServer final : public Component {
public:
void setup() override;
void dump_config() override;
@@ -21,7 +21,7 @@ class BLEServer : public Component {
CallbackManager<void(uint32_t)> passkey_cb_;
};
template<typename... Ts> class BLENumericComparisonReplyAction : public Action<Ts...> {
template<typename... Ts> class BLENumericComparisonReplyAction final : public Action<Ts...> {
public:
explicit BLENumericComparisonReplyAction(BLEServer *parent) : parent_(parent) {}
+1
View File
@@ -1255,6 +1255,7 @@ UNIT_KILOVOLT_AMPS_REACTIVE_HOURS = "kvarh"
UNIT_KILOWATT = "kW"
UNIT_KILOWATT_HOURS = "kWh"
UNIT_LITRE = "L"
UNIT_LITRE_PER_HOUR = "L/h"
UNIT_LITRE_PER_SECOND = "L/s"
UNIT_LUX = "lx"
UNIT_MEGAJOULE = "MJ"
+3
View File
@@ -239,9 +239,12 @@
#define ESPHOME_TASK_LOG_BUFFER_SIZE 768
#define USE_OTA_ROLLBACK
#define USE_OTA_SIGNED_VERIFICATION
#define USE_OTA_DOWNGRADE_PROTECTION
#define USE_ESP32_MIN_CHIP_REVISION_SET
#define USE_ESP32_RTC_PREFERENCES
#define USE_ESP32_SRAM1_AS_IRAM
#define USE_ESPNOW
#define USE_ESPNOW_MAX_PAYLOAD_SIZE 1470
#define USE_BLUETOOTH_PROXY
#define BLUETOOTH_PROXY_MAX_CONNECTIONS 3
+8 -3
View File
@@ -184,8 +184,10 @@ template<size_t InlineSize = 8> class SmallInlineBuffer {
SmallInlineBuffer(const SmallInlineBuffer &) = delete;
SmallInlineBuffer &operator=(const SmallInlineBuffer &) = delete;
/// Set buffer contents, allocating heap if needed
void set(const uint8_t *src, size_t size) {
/// Resize to `size` bytes of (uninitialized) storage and return a writable pointer to fill.
/// Allocates heap only when `size` exceeds the inline capacity. Use this when the contents are
/// built in place (e.g. assembling a frame and appending a checksum) to avoid a staging copy.
uint8_t *init(size_t size) {
// Free existing heap allocation if switching from heap to inline or different heap size
if (!this->is_inline_() && (size <= InlineSize || size != this->len_)) {
delete[] this->heap_;
@@ -196,9 +198,12 @@ template<size_t InlineSize = 8> class SmallInlineBuffer {
this->heap_ = new uint8_t[size]; // NOLINT(cppcoreguidelines-owning-memory)
}
this->len_ = size;
memcpy(this->data(), src, size);
return this->data();
}
/// Set buffer contents, allocating heap if needed
void set(const uint8_t *src, size_t size) { memcpy(this->init(size), src, size); }
uint8_t *data() { return this->is_inline_() ? this->inline_ : this->heap_; }
const uint8_t *data() const { return this->is_inline_() ? this->inline_ : this->heap_; }
size_t size() const { return this->len_; }
+6
View File
@@ -52,6 +52,7 @@ RESPONSE_ERROR_PARTITION_TABLE_VERIFY = 0x8F
RESPONSE_ERROR_PARTITION_TABLE_UPDATE = 0x90
RESPONSE_ERROR_BOOTLOADER_VERIFY = 0x91
RESPONSE_ERROR_BOOTLOADER_UPDATE = 0x92
RESPONSE_ERROR_VERSION_DOWNGRADE = 0x93
RESPONSE_ERROR_UNKNOWN = 0xFF
OTA_VERSION_1_0 = 1
@@ -157,6 +158,11 @@ _ERROR_MESSAGES: dict[int, str] = {
"the bootloader update without rebooting the device. If the device "
"fails to boot, recover it via a serial flash."
),
RESPONSE_ERROR_VERSION_DOWNGRADE: (
"The device rejected the update because it has OTA downgrade protection "
"enabled: the new firmware's version must be newer than the version the "
"device is currently running."
),
RESPONSE_ERROR_UNKNOWN: "Unknown error from ESP",
}
@@ -0,0 +1,10 @@
esphome:
name: test
esp32:
variant: esp32s3
framework:
type: esp-idf
advanced:
nvs_encryption:
key_id: 0
+71
View File
@@ -13,6 +13,7 @@ from esphome.components.esp32 import (
VARIANT_ESP32,
VARIANTS,
NetworkSdkconfigData,
_ota_downgrade_protection_errors,
_reconcile_network_sdkconfig,
)
from esphome.components.esp32.const import (
@@ -174,6 +175,29 @@ def test_esp32_default_toolchain_is_esp_idf(
r"'ignore_efuse_mac_crc' is not supported on ESP32S3 @ data\['framework'\]\['advanced'\]\['ignore_efuse_mac_crc'\]",
id="ignore_efuse_mac_crc_only_on_esp32",
),
pytest.param(
{
"variant": "esp32",
"board": "esp32dev",
"framework": {
"type": "esp-idf",
"advanced": {"nvs_encryption": {"key_id": 0}},
},
},
r"NVS encryption \(HMAC scheme\) is not supported on ESP32 .* @ data\['framework'\]\['advanced'\]\['nvs_encryption'\]",
id="nvs_encryption_unsupported_on_esp32",
),
pytest.param(
{
"variant": "esp32s3",
"framework": {
"type": "esp-idf",
"advanced": {"nvs_encryption": {"key_id": 6}},
},
},
r"value must be at most 5 .* @ data\['framework'\]\['advanced'\]\['nvs_encryption'\]\['key_id'\]",
id="nvs_encryption_key_id_out_of_range",
),
],
)
def test_esp32_configuration_errors(
@@ -213,6 +237,21 @@ def test_execute_from_psram_p4_sdkconfig(
assert "CONFIG_SPIRAM_RODATA" not in sdkconfig
def test_nvs_encryption_sdkconfig(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test that nvs_encryption sets the HMAC scheme sdkconfig options."""
generate_main(component_config_path("nvs_encryption_s3.yaml"))
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
assert sdkconfig.get("CONFIG_NVS_ENCRYPTION") is True
assert sdkconfig.get("CONFIG_NVS_SEC_KEY_PROTECT_USING_HMAC") is True
assert sdkconfig.get("CONFIG_NVS_SEC_HMAC_EFUSE_KEY_ID") == 0
# The permanent/irreversible eFuse burn is warned about at config time.
assert "PERMANENT and IRREVERSIBLE" in caplog.text
@pytest.mark.parametrize(
("fixture", "expect_warning"),
[
@@ -560,3 +599,35 @@ def test_network_wifi_ble_coexistence_reconciles_end_to_end(
assert sdkconfig.get("CONFIG_LWIP_DHCPS") is False
# WiFi present alongside BT -> WiFi stack must stay enabled.
assert "CONFIG_ESP_WIFI_ENABLED" not in sdkconfig
def test_downgrade_protection_passes_with_numeric_version_and_signing() -> None:
assert _ota_downgrade_protection_errors("1.2.3", signed_ota_enabled=True) == []
def test_downgrade_protection_accepts_calendar_version() -> None:
assert _ota_downgrade_protection_errors("2024.12.0", signed_ota_enabled=True) == []
def test_downgrade_protection_requires_project_version() -> None:
errs = _ota_downgrade_protection_errors(None, signed_ota_enabled=True)
assert len(errs) == 1
assert "version" in str(errs[0])
def test_downgrade_protection_rejects_non_numeric_version() -> None:
errs = _ota_downgrade_protection_errors("1.0-beta", signed_ota_enabled=True)
assert len(errs) == 1
assert "dotted-numeric" in str(errs[0])
def test_downgrade_protection_requires_signed_ota() -> None:
errs = _ota_downgrade_protection_errors("1.2.3", signed_ota_enabled=False)
assert len(errs) == 1
assert "signed_ota_verification" in str(errs[0])
def test_downgrade_protection_reports_all_unmet_requirements() -> None:
# No project version and no signing -> two distinct errors.
errs = _ota_downgrade_protection_errors(None, signed_ota_enabled=False)
assert len(errs) == 2
@@ -0,0 +1,26 @@
esphome:
name: test-not-paused
esp32:
board: lolin_c3_mini
spi:
mosi_pin:
number: GPIO2
ignore_strapping_warning: true
clk_pin: GPIO1
display:
- platform: mipi_spi
data_rate: 20MHz
model: st7735
cs_pin:
number: GPIO8
ignore_strapping_warning: true
dc_pin:
number: GPIO3
lvgl:
widgets:
- obj:
id: root_obj
@@ -0,0 +1,27 @@
esphome:
name: test-paused
esp32:
board: lolin_c3_mini
spi:
mosi_pin:
number: GPIO2
ignore_strapping_warning: true
clk_pin: GPIO1
display:
- platform: mipi_spi
data_rate: 20MHz
model: st7735
cs_pin:
number: GPIO8
ignore_strapping_warning: true
dc_pin:
number: GPIO3
lvgl:
paused: true
widgets:
- obj:
id: root_obj
+35
View File
@@ -0,0 +1,35 @@
"""Tests for the LVGL ``paused`` option code generation."""
from __future__ import annotations
import re
_SET_PAUSED_RE = re.compile(r"->set_paused\((.+?)\);")
def _extract_set_paused(main_cpp: str) -> list[str]:
"""Return the normalised argument text of every set_paused() call found.
Whitespace within and around the arguments is collapsed so unrelated
code-generation formatting changes don't break these tests.
"""
return [" ".join(m.group(1).split()) for m in _SET_PAUSED_RE.finditer(main_cpp)]
class TestPausedCodeGeneration:
"""Verify that the ``paused`` option drives the set_paused() call."""
def test_paused_true_generates_set_paused(
self, generate_main, component_config_path
):
"""``paused: true`` emits a set_paused(true, false) call."""
main_cpp = generate_main(component_config_path("paused.yaml"))
calls = _extract_set_paused(main_cpp)
assert calls == ["true, false"]
def test_paused_default_omits_set_paused(
self, generate_main, component_config_path
):
"""Without ``paused`` (default false) no set_paused call is generated."""
main_cpp = generate_main(component_config_path("not_paused.yaml"))
assert _extract_set_paused(main_cpp) == []
@@ -0,0 +1,9 @@
esp32:
variant: esp32s3
framework:
type: esp-idf
advanced:
nvs_encryption:
key_id: 0
<<: !include common.yaml
@@ -0,0 +1,22 @@
esphome:
project:
name: esphome.downgrade_test
version: "1.2.3"
esp32:
variant: esp32s3
framework:
type: esp-idf
advanced:
enable_ota_downgrade_protection: true
signed_ota_verification:
signing_key: ../../components/esp32/dummy_signing_key.pem
signing_scheme: rsa3072
# wifi + ota so the IDF OTA backend compiles with USE_OTA_DOWNGRADE_PROTECTION.
wifi:
ssid: MySSID
password: password1
ota:
- platform: esphome
+1
View File
@@ -2,6 +2,7 @@ espnow:
id: espnow_component
auto_add_peer: false
channel: 1
max_payload_size: 1470
peers:
- 11:22:33:44:55:66
on_receive:
+3 -14
View File
@@ -24,20 +24,6 @@ binary_sensor:
name: Button A checked
widget: button_a
state: checked
- platform: lvgl
id: button_checker
name: LVGL button
widget: button_button
state: checked
on_state:
then:
- lvgl.checkbox.update:
id: checkbox_id
state:
checked: !lambda |-
auto y = x; // block inlining of one line return
return y;
- platform: lvgl
id: button_presser
name: Button pressed
@@ -49,6 +35,9 @@ lvgl:
rotation: 90
log_level: debug
resume_on_input: true
paused: true
update_when_display_idle: true
refresh_interval: 30ms
on_pause:
- logger.log: LVGL is Paused
- lvgl.display.set_rotation: 90
+2 -3
View File
@@ -1,7 +1,8 @@
packages:
lvgl: !include lvgl-package.yaml
lvgl_package: !include lvgl-package.yaml
spi: !include ../../test_build_components/common/spi/esp32-idf.yaml
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
lvgl: !include common.yaml
sensor:
- platform: rotary_encoder
@@ -77,5 +78,3 @@ lvgl:
- component.update: tft_display
- delay: 60s
- lvgl.resume:
<<: !include common.yaml

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