Compare commits

..

6 Commits

Author SHA1 Message Date
Jesse Hills 014131233e [online_image] Use rp2_arduino framework key after rp2040->rp2 rename
dev renamed the rp2040 platform to rp2 (framework key rp2040_arduino ->
rp2_arduino). Propagate that into the online_image file platform, whose
framework-version guard was relocated here during the image platform
refactor and so was not covered by the rename on dev.
2026-07-07 09:36:36 +12:00
Jesse Hills cb0c428d6a Merge remote-tracking branch 'origin/dev' into jesserockz-2026-405
# Conflicts:
#	esphome/components/online_image/__init__.py
2026-07-07 09:35:37 +12:00
Jesse Hills 1d49daa90c [image] Drop obsolete language-schema image fix
`image` is now a platform component, so build_language_schema.py no
longer needs the fix_image() hook that imported the now-removed
IMAGE_SCHEMA (it crashed the schema build). The builder emits image with
its file/animation/online_image platforms generically, like other
platform components.
2026-07-06 20:38:25 +12:00
Jesse Hills 0b1e3283d5 [image] Only migrate recognised legacy image configs
Guard the legacy `image:` migration so it only rewrites shapes the
pre-platform schema actually accepted. A list with a non-dict or
already-platform-tagged entry, or a dict with no recognised image keys
(e.g. `image: ["bad"]`, `image: {foo: 1}`), now returns None from the
migration hook, so normal platform validation surfaces a proper error
instead of the migration silently dropping the input.
2026-07-06 20:24:55 +12:00
Jesse Hills d1938a87aa Merge remote-tracking branch 'origin/dev' into jesserockz-2026-405
# Conflicts:
#	tests/unit_tests/test_config_normalization.py
2026-07-06 20:19:30 +12:00
Jesse Hills dc5e6a20af [image] Restructure into a platform component
Make `image` a platform component with `file`, `animation` and
`online_image` as platforms. Static-file image handling moves into the
new `file` platform; `animation` and `online_image` become thin
platforms whose real schema/codegen live in `image.py`, leaving only
deletable legacy top-level shims in `__init__.py`.

The deprecated top-level `image:`, `animation:` and `online_image:`
forms keep working through 2027.1.0 and now emit a one-shot deprecation
warning that prints a pasteable, migrated `image:` block.
2026-07-06 16:31:37 +12:00
148 changed files with 2160 additions and 2578 deletions
+2
View File
@@ -1,3 +1,5 @@
# Normalize line endings to LF in the repository
* text eol=lf
*.png binary
*.gif binary
*.apng binary
+1
View File
@@ -187,6 +187,7 @@ esphome/components/ezo_pmp/* @carlos-sarmiento
esphome/components/factory_reset/* @anatoly-savchenkov
esphome/components/fastled_base/* @OttoWinter
esphome/components/feedback/* @ianchi
esphome/components/file/* @esphome/core
esphome/components/fingerprint_grow/* @alexborro @loongyh @OnFreund
esphome/components/font/* @clydebarrow @esphome/core
esphome/components/fs3000/* @kahrendt
+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.2.0
RUN uv pip install --no-cache-dir esphome-device-builder==1.1.0
RUN \
platformio settings set enable_telemetry No \
+10 -34
View File
@@ -8,7 +8,7 @@ memory-constrained platforms like ESP8266.
from __future__ import annotations
from collections import defaultdict
from dataclasses import dataclass, field
from dataclasses import dataclass
import logging
from pathlib import Path
import re
@@ -65,7 +65,6 @@ 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:
@@ -236,11 +235,6 @@ 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:
@@ -259,18 +253,6 @@ 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:
@@ -278,15 +260,15 @@ class RamStringsAnalyzer:
section = self.sections[section_name]
if section.address <= addr < section.address + section.size:
symbol = RamSymbol(
name=name,
sym_type=sym_type,
address=addr,
size=size,
section=section_name,
self.ram_symbols.append(
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:
@@ -454,13 +436,7 @@ class RamStringsAnalyzer:
for symbol in largest_symbols:
# Use demangled name if available, otherwise raw name
display_name = symbol.demangled or symbol.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
name_display = display_name[:49] if len(display_name) > 49 else display_name
lines.append(
f"{name_display:<50} {symbol.sym_type:<6} {symbol.size:>8} B {symbol.section}"
)
+20 -98
View File
@@ -1,114 +1,36 @@
import logging
# ---------------------------------------------------------------------------
# Legacy top-level `animation:` deprecation shim -- REMOVE this whole file after
# 2027.1.0.
#
# Animations are now a platform of the `image:` component (`platform:
# animation`); the real schema, actions and codegen live in `image.py`. This
# module only keeps the deprecated top-level `animation:` key working during the
# deprecation window: it reuses that schema/codegen and adds a one-shot
# deprecation warning (with a pasteable migrated `image:` block) at validation
# time. Deleting this file drops the top-level form entirely.
# ---------------------------------------------------------------------------
from esphome import automation
import esphome.codegen as cg
from esphome.components.const import CONF_LOOP
import esphome.components.image as espImage
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_REPEAT
_LOGGER = logging.getLogger(__name__)
from .image import ANIMATION_CONFIG_SCHEMA, setup_animation
AUTO_LOAD = ["image"]
AUTO_LOAD = ["image", "file"]
CODEOWNERS = ["@syndlex"]
DEPENDENCIES = ["display"]
MULTI_CONF = True
MULTI_CONF_NO_DEFAULT = True
CONF_START_FRAME = "start_frame"
CONF_END_FRAME = "end_frame"
CONF_FRAME = "frame"
DOMAIN = "animation"
animation_ns = cg.esphome_ns.namespace("animation")
LEGACY_REMOVAL_VERSION = "2027.1.0"
Animation_ = animation_ns.class_("Animation", espImage.Image_)
# Actions
NextFrameAction = animation_ns.class_(
"AnimationNextFrameAction", automation.Action, cg.Parented.template(Animation_)
)
PrevFrameAction = animation_ns.class_(
"AnimationPrevFrameAction", automation.Action, cg.Parented.template(Animation_)
)
SetFrameAction = animation_ns.class_(
"AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_)
_capture_legacy_entry, _warn_legacy_animation = (
espImage.legacy_platform_migration_warning(DOMAIN, DOMAIN, LEGACY_REMOVAL_VERSION)
)
CONFIG_SCHEMA = cv.All(
espImage.IMAGE_SCHEMA.extend(
{
cv.Required(CONF_ID): cv.declare_id(Animation_),
cv.Optional(CONF_LOOP): cv.All(
{
cv.Optional(CONF_START_FRAME, default=0): cv.positive_int,
cv.Optional(CONF_END_FRAME): cv.positive_int,
cv.Optional(CONF_REPEAT): cv.positive_int,
}
),
},
),
espImage.validate_settings,
)
CONFIG_SCHEMA = cv.All(_capture_legacy_entry, ANIMATION_CONFIG_SCHEMA)
FINAL_VALIDATE_SCHEMA = _warn_legacy_animation
NEXT_FRAME_SCHEMA = automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(Animation_),
}
)
PREV_FRAME_SCHEMA = automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(Animation_),
}
)
SET_FRAME_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.use_id(Animation_),
cv.Required(CONF_FRAME): cv.uint16_t,
}
)
@automation.register_action(
"animation.next_frame", NextFrameAction, NEXT_FRAME_SCHEMA, synchronous=True
)
@automation.register_action(
"animation.prev_frame", PrevFrameAction, PREV_FRAME_SCHEMA, synchronous=True
)
@automation.register_action(
"animation.set_frame", SetFrameAction, SET_FRAME_SCHEMA, synchronous=True
)
async def animation_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
if (frame := config.get(CONF_FRAME)) is not None:
template_ = await cg.templatable(frame, args, cg.uint16)
cg.add(var.set_frame(template_))
return var
async def to_code(config):
(
prog_arr,
width,
height,
image_type,
trans_value,
frame_count,
) = await espImage.write_image(config, all_frames=True)
var = cg.new_Pvariable(
config[CONF_ID],
prog_arr,
width,
height,
frame_count,
image_type,
trans_value,
)
if loop_config := config.get(CONF_LOOP):
start = loop_config[CONF_START_FRAME]
end = loop_config.get(CONF_END_FRAME, frame_count)
count = loop_config.get(CONF_REPEAT, -1)
cg.add(var.set_loop(start, end, count))
to_code = setup_animation
+115
View File
@@ -0,0 +1,115 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components.const import CONF_LOOP
from esphome.components.file.image import image_schema, write_image
from esphome.components.image import Image_, validate_settings
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_REPEAT
from esphome.types import ConfigType
CODEOWNERS = ["@syndlex"]
AUTO_LOAD = ["file"]
DEPENDENCIES = ["display"]
CONF_START_FRAME = "start_frame"
CONF_END_FRAME = "end_frame"
CONF_FRAME = "frame"
animation_ns = cg.esphome_ns.namespace("animation")
Animation_ = animation_ns.class_("Animation", Image_)
# Actions
NextFrameAction = animation_ns.class_(
"AnimationNextFrameAction", automation.Action, cg.Parented.template(Animation_)
)
PrevFrameAction = animation_ns.class_(
"AnimationPrevFrameAction", automation.Action, cg.Parented.template(Animation_)
)
SetFrameAction = animation_ns.class_(
"AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_)
)
ANIMATION_SCHEMA = image_schema(Animation_).extend(
{
cv.Optional(CONF_LOOP): cv.All(
{
cv.Optional(CONF_START_FRAME, default=0): cv.positive_int,
cv.Optional(CONF_END_FRAME): cv.positive_int,
cv.Optional(CONF_REPEAT): cv.positive_int,
}
),
},
)
# Shared schema used by both the (deprecated) top-level `animation:` key and the
# `image:` `platform: animation` entry.
ANIMATION_CONFIG_SCHEMA = cv.All(ANIMATION_SCHEMA, validate_settings)
NEXT_FRAME_SCHEMA = automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(Animation_),
}
)
PREV_FRAME_SCHEMA = automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(Animation_),
}
)
SET_FRAME_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.use_id(Animation_),
cv.Required(CONF_FRAME): cv.uint16_t,
}
)
@automation.register_action(
"animation.next_frame", NextFrameAction, NEXT_FRAME_SCHEMA, synchronous=True
)
@automation.register_action(
"animation.prev_frame", PrevFrameAction, PREV_FRAME_SCHEMA, synchronous=True
)
@automation.register_action(
"animation.set_frame", SetFrameAction, SET_FRAME_SCHEMA, synchronous=True
)
async def animation_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
if (frame := config.get(CONF_FRAME)) is not None:
template_ = await cg.templatable(frame, args, cg.uint16)
cg.add(var.set_frame(template_))
return var
async def setup_animation(config: ConfigType) -> None:
(
prog_arr,
width,
height,
image_type,
trans_value,
frame_count,
) = await write_image(config, all_frames=True)
var = cg.new_Pvariable(
config[CONF_ID],
prog_arr,
width,
height,
frame_count,
image_type,
trans_value,
)
if loop_config := config.get(CONF_LOOP):
start = loop_config[CONF_START_FRAME]
end = loop_config.get(CONF_END_FRAME, frame_count)
count = loop_config.get(CONF_REPEAT, -1)
cg.add(var.set_loop(start, end, count))
CONFIG_SCHEMA = ANIMATION_CONFIG_SCHEMA
to_code = setup_animation
+1 -2
View File
@@ -240,13 +240,12 @@ 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_to(addr_buf), this->port_, this->listen_backlog_, MAX_API_CONNECTIONS);
network::get_use_address(), 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()) {
+1 -3
View File
@@ -113,9 +113,7 @@ 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 ("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.
elif file_type in ("mp3", "mpeg", "mpga"):
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,7 +16,6 @@ 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,7 +11,6 @@ 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,
@@ -30,7 +29,6 @@ from esphome.const import (
CONF_PATH,
CONF_PLATFORM_VERSION,
CONF_PLATFORMIO_OPTIONS,
CONF_PROJECT,
CONF_REF,
CONF_SAFE_MODE,
CONF_SIZE,
@@ -109,9 +107,7 @@ 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"
@@ -169,20 +165,6 @@ 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",
@@ -1116,50 +1098,6 @@ 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
@@ -1365,37 +1303,6 @@ 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)
@@ -1633,9 +1540,6 @@ 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(
{
@@ -1648,15 +1552,6 @@ 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,
@@ -2463,16 +2358,6 @@ 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)
@@ -2499,20 +2384,6 @@ 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(
+5 -47
View File
@@ -9,8 +9,6 @@
#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>
@@ -35,19 +33,6 @@ 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: \
@@ -179,9 +164,6 @@ 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
@@ -210,35 +192,15 @@ bool ESP32BLE::ble_setup_() {
esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT);
#else
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);
esp_hosted_connect_to_slave(); // NOLINT
if (esp_hosted_bt_controller_init() != ESP_OK) {
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);
ESP_LOGW(TAG, "esp_hosted_bt_controller_init failed");
return false;
}
if (esp_hosted_bt_controller_enable() != ESP_OK) {
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);
ESP_LOGW(TAG, "esp_hosted_bt_controller_enable failed");
return false;
}
@@ -370,10 +332,6 @@ 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
@@ -419,12 +377,12 @@ bool ESP32BLE::ble_dismantle_() {
}
#else
if (esp_hosted_bt_controller_disable() != ESP_OK) {
ESP_LOGE(TAG, "esp_hosted_bt_controller_disable failed");
ESP_LOGW(TAG, "esp_hosted_bt_controller_disable failed");
return false;
}
if (esp_hosted_bt_controller_deinit(false) != ESP_OK) {
ESP_LOGE(TAG, "esp_hosted_bt_controller_deinit failed");
ESP_LOGW(TAG, "esp_hosted_bt_controller_deinit failed");
return false;
}
@@ -18,8 +18,6 @@ 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,12 +332,6 @@ 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.
@@ -1,35 +0,0 @@
/*
* 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,12 +94,11 @@ 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_to(addr_buf), this->port_, USE_OTA_VERSION);
network::get_use_address(), this->port_, USE_OTA_VERSION);
#ifdef USE_OTA_PASSWORD
if (!this->password_.empty()) {
ESP_LOGCONFIG(TAG, " Password configured");
+3 -29
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.uint16,
cg.uint8,
)
OnUnknownPeerTrigger = espnow_ns.class_(
@@ -56,20 +56,6 @@ 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"
@@ -77,15 +63,7 @@ CONF_ON_BROADCAST = "on_broadcast"
CONF_CONTINUE_ON_ERROR = "continue_on_error"
CONF_WAIT_FOR_SENT = "wait_for_sent"
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
MAX_ESPNOW_PACKET_SIZE = 250 # Maximum size of the payload in bytes
def validate_channel(value):
@@ -100,9 +78,6 @@ 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(
@@ -138,7 +113,7 @@ async def _trigger_to_code(config):
[
(ESPNowRecvInfoConstRef, "info"),
(cg.uint8.operator("const").operator("ptr"), "data"),
(cg.uint16, "size"),
(cg.uint8, "size"),
],
config,
)
@@ -150,7 +125,6 @@ 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 *, uint16_t>,
class OnReceiveTrigger final : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint8_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, uint16_t size) override {
bool on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint8_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 *, uint16_t>,
class OnUnknownPeerTrigger final : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint8_t>,
public ESPNowUnknownPeerHandler {
public:
bool on_unknown_peer(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) override {
bool on_unknown_peer(const ESPNowRecvInfo &info, const uint8_t *data, uint8_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 *, uint16_t>,
class OnBroadcastTrigger final : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint8_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, uint16_t size) override {
bool on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override {
bool match = !this->has_address_ || (memcmp(this->address_, info.src_addr, ESP_NOW_ETH_ALEN) == 0);
if (!match)
return false;
@@ -4,7 +4,6 @@
#include "espnow_err.h"
#include <algorithm>
#include <cinttypes>
#include "esphome/core/application.h"
@@ -97,9 +96,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 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) {
// 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) {
global_esp_now->receive_packet_queue_.increment_dropped_count();
return;
}
@@ -286,14 +285,11 @@ 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,
std::min<uint16_t>(packet->packet_.receive.size, ESP_NOW_MAX_DATA_LEN)));
format_hex_pretty_to(hex_buf, packet->packet_.receive.data, packet->packet_.receive.size));
#endif
if (memcmp(info.des_addr, ESPNOW_BROADCAST_ADDR, ESP_NOW_ETH_ALEN) == 0) {
for (auto *handler : this->broadcast_handlers_) {
@@ -366,7 +362,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 > ESPNOW_MAX_DATA_LEN) {
} else if (size > ESP_NOW_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, uint16_t size) = 0;
virtual bool on_unknown_peer(const ESPNowRecvInfo &info, const uint8_t *data, uint8_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, uint16_t size) = 0;
virtual bool on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint8_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, uint16_t size) = 0;
virtual bool on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0;
};
class ESPNowComponent final : public Component {
+9 -26
View File
@@ -19,23 +19,6 @@ 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
@@ -95,10 +78,10 @@ class ESPNowPacket {
union {
// NOLINTNEXTLINE(readability-identifier-naming)
struct received_data {
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
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
} receive;
// NOLINTNEXTLINE(readability-identifier-naming)
@@ -161,15 +144,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_[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
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
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 > ESPNOW_MAX_DATA_LEN) {
if (size > ESP_NOW_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() > ESPNOW_MAX_DATA_LEN) {
ESP_LOGE(TAG, "Packet too large: %zu bytes (max %u)", buf.size(), (unsigned) ESPNOW_MAX_DATA_LEN);
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);
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, uint16_t size) {
ESP_LOGV(TAG, "Received packet of size %u from %02X:%02X:%02X:%02X:%02X:%02X", (unsigned) size, info.src_addr[0],
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],
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, 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]);
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]);
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, uint16_t size) override;
bool on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint16_t size) override;
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;
protected:
void send_packet(const std::vector<uint8_t> &buf) const override;
size_t get_max_packet_size() override { return ESPNOW_MAX_DATA_LEN; }
size_t get_max_packet_size() override { return ESP_NOW_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 add_use_address, ip_address_literal
from esphome.components.network import 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]]))
add_use_address(var, config[CONF_USE_ADDRESS])
cg.add(var.set_use_address(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,8 +145,6 @@ 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);
@@ -348,7 +346,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_{nullptr};
const char *use_address_{""};
};
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
+1
View File
@@ -0,0 +1 @@
CODEOWNERS = ["@esphome/core"]
+315
View File
@@ -0,0 +1,315 @@
from __future__ import annotations
import contextlib
import hashlib
import io
import logging
from pathlib import Path
import re
from PIL import Image, UnidentifiedImageError
from esphome import core, external_files
import esphome.codegen as cg
from esphome.components.const import CONF_BYTE_ORDER
from esphome.components.image import (
CONF_INVERT_ALPHA,
CONF_OPAQUE,
CONF_TRANSPARENCY,
DOMAIN,
IMAGE_TYPE,
Image_,
ImageEncoder,
add_metadata,
get_image_type_enum,
get_transparency_enum,
is_svg_file,
validate_settings,
validate_transparency,
validate_type,
)
import esphome.config_validation as cv
from esphome.const import (
CONF_DITHER,
CONF_FILE,
CONF_ICON,
CONF_ID,
CONF_PATH,
CONF_RAW_DATA_ID,
CONF_RESIZE,
CONF_SOURCE,
CONF_TYPE,
CONF_URL,
)
from esphome.core import CORE, HexInt
from esphome.cpp_generator import MockObj, MockObjClass
from esphome.types import ConfigType
CODEOWNERS = ["@esphome/core"]
_LOGGER = logging.getLogger(__name__)
# If the MDI file cannot be downloaded within this time, abort.
IMAGE_DOWNLOAD_TIMEOUT = 30 # seconds
SOURCE_LOCAL = "local"
SOURCE_WEB = "web"
SOURCE_MDI = "mdi"
SOURCE_MDIL = "mdil"
SOURCE_MEMORY = "memory"
MDI_SOURCES = {
SOURCE_MDI: "https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/",
SOURCE_MDIL: "https://raw.githubusercontent.com/Pictogrammers/MaterialDesignLight/refs/heads/master/svg/",
SOURCE_MEMORY: "https://raw.githubusercontent.com/Pictogrammers/Memory/refs/heads/main/src/svg/",
}
def compute_local_image_path(value) -> Path:
url = value[CONF_URL] if isinstance(value, dict) else value
h = hashlib.new("sha256")
h.update(url.encode())
key = h.hexdigest()[:8]
# Downloaded files are cached under the shared `image` domain directory so
# the cache location is unaffected by which platform requested the file.
base_dir = external_files.compute_local_file_dir(DOMAIN)
return base_dir / key
def local_path(value):
value = value[CONF_PATH] if isinstance(value, dict) else value
return str(CORE.relative_config_path(value))
def download_file(url, path):
external_files.download_content(url, path, IMAGE_DOWNLOAD_TIMEOUT)
return str(path)
def download_gh_svg(value, source):
mdi_id = value[CONF_ICON] if isinstance(value, dict) else value
base_dir = external_files.compute_local_file_dir(DOMAIN) / source
path = base_dir / f"{mdi_id}.svg"
url = MDI_SOURCES[source] + mdi_id + ".svg"
return download_file(url, path)
def download_image(value):
value = value[CONF_URL] if isinstance(value, dict) else value
return download_file(value, compute_local_image_path(value))
def validate_file_shorthand(value):
value = cv.string_strict(value)
parts = value.strip().split(":")
if len(parts) == 2 and parts[0] in MDI_SOURCES:
match = re.match(r"^[a-zA-Z0-9\-]+$", parts[1])
if match is None:
raise cv.Invalid(f"Could not parse mdi icon name from '{value}'.")
return download_gh_svg(parts[1], parts[0])
if value.startswith(("http://", "https://")):
return download_image(value)
value = cv.file_(value)
return local_path(value)
LOCAL_SCHEMA = cv.All(
{
cv.Required(CONF_PATH): cv.file_,
},
local_path,
)
def mdi_schema(source):
def validate_mdi(value):
return download_gh_svg(value, source)
return cv.All(
cv.Schema(
{
cv.Required(CONF_ICON): cv.string,
}
),
validate_mdi,
)
WEB_SCHEMA = cv.All(
{
cv.Required(CONF_URL): cv.string,
},
download_image,
)
TYPED_FILE_SCHEMA = cv.typed_schema(
{
SOURCE_LOCAL: LOCAL_SCHEMA,
SOURCE_WEB: WEB_SCHEMA,
}
| {source: mdi_schema(source) for source in MDI_SOURCES},
key=CONF_SOURCE,
)
OPTIONS_SCHEMA = {
cv.Optional(CONF_RESIZE): cv.dimensions,
cv.Optional(CONF_DITHER, default="NONE"): cv.one_of(
"NONE", "FLOYDSTEINBERG", upper=True
),
cv.Optional(CONF_INVERT_ALPHA, default=False): cv.boolean,
cv.Optional(CONF_BYTE_ORDER): cv.one_of("BIG_ENDIAN", "LITTLE_ENDIAN", upper=True),
cv.Optional(CONF_TRANSPARENCY, default=CONF_OPAQUE): validate_transparency(),
}
def image_schema(class_: MockObjClass = Image_) -> cv.Schema:
"""Build the validation schema for a single file-backed image entry.
Shared by the built-in ``file`` image platform and the ``animation``
platform (which extends it). Platforms that source their pixels elsewhere
(e.g. ``online_image``) provide their own schema instead.
:param class_: The declared C++ class for the generated image instance.
"""
return cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(class_),
cv.Required(CONF_FILE): cv.Any(validate_file_shorthand, TYPED_FILE_SCHEMA),
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
**OPTIONS_SCHEMA,
cv.Required(CONF_TYPE): validate_type(IMAGE_TYPE),
}
)
def validate_image_final(config: ConfigType) -> ConfigType:
"""Per-entry final validation, shared by file-backed image platforms.
For LVGL 9 the default byte order for RGB565 images is little-endian, so
fill in that default when the user did not specify a byte order and warn
when big-endian was explicitly requested.
"""
if byte_order := config.get(CONF_BYTE_ORDER):
if byte_order == "BIG_ENDIAN":
_LOGGER.warning(
"The image '%s' is configured with big-endian byte order, little-endian is expected",
config.get(CONF_FILE),
)
else:
config[CONF_BYTE_ORDER] = "LITTLE_ENDIAN"
return config
async def new_image(config: ConfigType) -> MockObj:
"""Generate a single file-backed ``image::Image`` instance.
Used by the built-in ``file`` platform; encodes the image data, registers
the C++ variable and records its metadata for other components to consume.
"""
prog_arr, width, height, image_type, trans_value, _ = await write_image(config)
var = cg.new_Pvariable(
config[CONF_ID], prog_arr, width, height, image_type, trans_value
)
add_metadata(
config[CONF_ID], width, height, config[CONF_TYPE], config[CONF_TRANSPARENCY]
)
return var
async def write_image(config, all_frames=False):
path = Path(config[CONF_FILE])
if not path.is_file():
raise core.EsphomeError(f"Could not load image file {path}")
resize = config.get(CONF_RESIZE)
try:
if is_svg_file(path):
import resvg_py
resize = resize or (None, None)
image_data = resvg_py.svg_to_bytes(
svg_path=str(path), width=resize[0], height=resize[1], dpi=100
)
# Convert bytes to Pillow Image
image = Image.open(io.BytesIO(image_data))
width, height = image.size
else:
image = Image.open(path)
width, height = image.size
if resize:
# Preserve aspect ratio
new_width_max = min(width, resize[0])
new_height_max = min(height, resize[1])
ratio = min(new_width_max / width, new_height_max / height)
width, height = int(width * ratio), int(height * ratio)
except (OSError, UnidentifiedImageError, ValueError) as exc:
raise core.EsphomeError(f"Could not read image file {path}: {exc}") from exc
if not resize and (width > 500 or height > 500):
_LOGGER.warning(
'The image "%s" you requested is very big. Please consider'
" using the resize parameter.",
path,
)
dither = (
Image.Dither.NONE
if config[CONF_DITHER] == "NONE"
else Image.Dither.FLOYDSTEINBERG
)
type = config[CONF_TYPE]
transparency = config.get(CONF_TRANSPARENCY, CONF_OPAQUE)
invert_alpha = config[CONF_INVERT_ALPHA]
frame_count = 1
if all_frames:
with contextlib.suppress(AttributeError):
frame_count = image.n_frames
if frame_count <= 1:
_LOGGER.warning("Image file %s has no animation frames", path)
# Encode each frame with its own encoder and concatenate. This keeps every
# frame self-contained on disk (e.g. RGB565+alpha emits [RGB plane | alpha plane]
# per frame) so animation frame stepping in image.cpp / animation.cpp stays
# correct without needing to know the total frame count.
byte_order = config.get(CONF_BYTE_ORDER)
combined_data: list[int] = []
encoder: ImageEncoder | None = None
for frame_index in range(frame_count):
image.seek(frame_index)
encoder = IMAGE_TYPE[type](width, height, transparency, dither, invert_alpha)
if byte_order is not None:
# Check for valid type has already been done in validate_settings
encoder.set_big_endian(byte_order == "BIG_ENDIAN")
pixels = encoder.convert(image.resize((width, height)), path).getdata()
for row in range(height):
for col in range(width):
encoder.encode(pixels[row * width + col])
encoder.end_row()
encoder.end_image()
combined_data.extend(encoder.data)
rhs = [HexInt(x) for x in combined_data]
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
image_type = get_image_type_enum(type)
trans_value = get_transparency_enum(encoder.transparency)
return prog_arr, width, height, image_type, trans_value, frame_count
# The built-in static-image platform: pixels embedded at compile time from a
# local file, a downloaded web image, or a Material Design Icon.
CONFIG_SCHEMA = cv.All(image_schema(Image_), validate_settings)
FINAL_VALIDATE_SCHEMA = validate_image_final
async def to_code(config: ConfigType) -> None:
await new_image(config)
+206 -410
View File
@@ -1,38 +1,27 @@
from __future__ import annotations
import contextlib
from collections.abc import Callable
from dataclasses import dataclass
import hashlib
import io
import logging
from pathlib import Path
import re
from PIL import Image, UnidentifiedImageError
from esphome import core, external_files
import esphome.codegen as cg
from esphome.components.const import CONF_BYTE_ORDER, KEY_METADATA
import esphome.config_validation as cv
from esphome.const import (
CONF_DEFAULTS,
CONF_DITHER,
CONF_FILE,
CONF_ICON,
CONF_ID,
CONF_PATH,
CONF_RAW_DATA_ID,
CONF_RESIZE,
CONF_SOURCE,
CONF_TYPE,
CONF_URL,
)
from esphome.core import CORE, HexInt
from esphome.const import CONF_DEFAULTS, CONF_FILE, CONF_ID, CONF_PLATFORM, CONF_TYPE
from esphome.core import CORE
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
DOMAIN = "image"
DEPENDENCIES = ["display"]
IS_PLATFORM_COMPONENT = True
# Name of the built-in static-image platform (local file / web / MDI sources).
PLATFORM_FILE = "file"
image_ns = cg.esphome_ns.namespace("image")
@@ -135,17 +124,6 @@ class ImageEncoder:
"""
return False
@classmethod
def get_options(cls) -> list[str]:
"""
Get the available options for this image encoder
"""
options = [*OPTIONS]
if not cls.is_endian():
options.remove(CONF_BYTE_ORDER)
options.append(CONF_RAW_DATA_ID)
return options
def is_alpha_only(image: Image):
"""
@@ -338,60 +316,11 @@ TransparencyType = image_ns.enum("TransparencyType")
CONF_TRANSPARENCY = "transparency"
# If the MDI file cannot be downloaded within this time, abort.
IMAGE_DOWNLOAD_TIMEOUT = 30 # seconds
SOURCE_LOCAL = "local"
SOURCE_WEB = "web"
SOURCE_MDI = "mdi"
SOURCE_MDIL = "mdil"
SOURCE_MEMORY = "memory"
MDI_SOURCES = {
SOURCE_MDI: "https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/",
SOURCE_MDIL: "https://raw.githubusercontent.com/Pictogrammers/MaterialDesignLight/refs/heads/master/svg/",
SOURCE_MEMORY: "https://raw.githubusercontent.com/Pictogrammers/Memory/refs/heads/main/src/svg/",
}
Image_ = image_ns.class_("Image")
INSTANCE_TYPE = Image_
def compute_local_image_path(value) -> Path:
url = value[CONF_URL] if isinstance(value, dict) else value
h = hashlib.new("sha256")
h.update(url.encode())
key = h.hexdigest()[:8]
base_dir = external_files.compute_local_file_dir(DOMAIN)
return base_dir / key
def local_path(value):
value = value[CONF_PATH] if isinstance(value, dict) else value
return str(CORE.relative_config_path(value))
def download_file(url, path):
external_files.download_content(url, path, IMAGE_DOWNLOAD_TIMEOUT)
return str(path)
def download_gh_svg(value, source):
mdi_id = value[CONF_ICON] if isinstance(value, dict) else value
base_dir = external_files.compute_local_file_dir(DOMAIN) / source
path = base_dir / f"{mdi_id}.svg"
url = MDI_SOURCES[source] + mdi_id + ".svg"
return download_file(url, path)
def download_image(value):
value = value[CONF_URL] if isinstance(value, dict) else value
return download_file(value, compute_local_image_path(value))
def is_svg_file(file):
if not file:
return False
@@ -399,62 +328,6 @@ def is_svg_file(file):
return "<svg" in str(f.read(1024))
def validate_file_shorthand(value):
value = cv.string_strict(value)
parts = value.strip().split(":")
if len(parts) == 2 and parts[0] in MDI_SOURCES:
match = re.match(r"^[a-zA-Z0-9\-]+$", parts[1])
if match is None:
raise cv.Invalid(f"Could not parse mdi icon name from '{value}'.")
return download_gh_svg(parts[1], parts[0])
if value.startswith(("http://", "https://")):
return download_image(value)
value = cv.file_(value)
return local_path(value)
LOCAL_SCHEMA = cv.All(
{
cv.Required(CONF_PATH): cv.file_,
},
local_path,
)
def mdi_schema(source):
def validate_mdi(value):
return download_gh_svg(value, source)
return cv.All(
cv.Schema(
{
cv.Required(CONF_ICON): cv.string,
}
),
validate_mdi,
)
WEB_SCHEMA = cv.All(
{
cv.Required(CONF_URL): cv.string,
},
download_image,
)
TYPED_FILE_SCHEMA = cv.typed_schema(
{
SOURCE_LOCAL: LOCAL_SCHEMA,
SOURCE_WEB: WEB_SCHEMA,
}
| {source: mdi_schema(source) for source in MDI_SOURCES},
key=CONF_SOURCE,
)
def validate_transparency(choices=TRANSPARENCY_TYPES):
def validate(value):
if isinstance(value, bool):
@@ -508,271 +381,6 @@ def validate_settings(value, path=()):
return value
IMAGE_ID_SCHEMA = {
cv.Required(CONF_ID): cv.declare_id(Image_),
cv.Required(CONF_FILE): cv.Any(validate_file_shorthand, TYPED_FILE_SCHEMA),
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
}
OPTIONS_SCHEMA = {
cv.Optional(CONF_RESIZE): cv.dimensions,
cv.Optional(CONF_DITHER, default="NONE"): cv.one_of(
"NONE", "FLOYDSTEINBERG", upper=True
),
cv.Optional(CONF_INVERT_ALPHA, default=False): cv.boolean,
cv.Optional(CONF_BYTE_ORDER): cv.one_of("BIG_ENDIAN", "LITTLE_ENDIAN", upper=True),
cv.Optional(CONF_TRANSPARENCY, default=CONF_OPAQUE): validate_transparency(),
}
DEFAULTS_SCHEMA = {
**OPTIONS_SCHEMA,
cv.Optional(CONF_TYPE): validate_type(IMAGE_TYPE),
}
OPTIONS = [key.schema for key in OPTIONS_SCHEMA]
# image schema with no defaults, used with `CONF_IMAGES` in the config
IMAGE_SCHEMA_NO_DEFAULTS = {
**IMAGE_ID_SCHEMA,
**{cv.Optional(key): OPTIONS_SCHEMA[key] for key in OPTIONS},
}
IMAGE_SCHEMA = cv.Schema(
{
**IMAGE_ID_SCHEMA,
**OPTIONS_SCHEMA,
cv.Required(CONF_TYPE): validate_type(IMAGE_TYPE),
}
)
def apply_defaults(image, defaults, path):
"""
Apply defaults to an image configuration
"""
type = image.get(CONF_TYPE, defaults.get(CONF_TYPE))
if type is None:
raise cv.Invalid(
"Type is required either in the image config or in the defaults", path=path
)
type_class = IMAGE_TYPE[type]
config = {
**{key: image.get(key, defaults.get(key)) for key in type_class.get_options()},
**{key.schema: image[key.schema] for key in IMAGE_ID_SCHEMA},
CONF_TYPE: image.get(CONF_TYPE, defaults.get(CONF_TYPE)),
}
validate_settings(config, path)
return config
def validate_defaults(value):
"""
Apply defaults to the images in the configuration and flatten to a single list.
"""
defaults = value[CONF_DEFAULTS]
result = []
# Apply defaults to the images: list and add the list entries to the result
for index, image in enumerate(value.get(CONF_IMAGES, [])):
result.append(apply_defaults(image, defaults, [CONF_IMAGES, index]))
# Apply defaults to images under the type keys and add them to the result
for image_type, type_config in value.items():
type_upper = image_type.upper()
if type_upper not in IMAGE_TYPE:
continue
type_class = IMAGE_TYPE[type_upper]
if isinstance(type_config, list):
# If the type is a list, apply defaults to each entry
for index, image in enumerate(type_config):
result.append(apply_defaults(image, defaults, [image_type, index]))
else:
# Handle transparency options for the type
for trans_type in set(type_class.allow_config).intersection(type_config):
for index, image in enumerate(type_config[trans_type]):
result.append(
apply_defaults(image, defaults, [image_type, trans_type, index])
)
return result
def typed_image_schema(image_type):
"""
Construct a schema for a specific image type, allowing transparency options
"""
return cv.Any(
cv.Schema(
{
cv.Optional(t.lower()): cv.ensure_list(
{
**IMAGE_ID_SCHEMA,
**{
cv.Optional(key): OPTIONS_SCHEMA[key]
for key in OPTIONS
if key != CONF_TRANSPARENCY
},
cv.Optional(
CONF_TRANSPARENCY, default=t
): validate_transparency((t,)),
cv.Optional(CONF_TYPE, default=image_type): validate_type(
(image_type,)
),
}
)
for t in IMAGE_TYPE[image_type].allow_config.intersection(
TRANSPARENCY_TYPES
)
}
),
# Allow a default configuration with no transparency preselected
cv.ensure_list(
{
**IMAGE_SCHEMA_NO_DEFAULTS,
cv.Optional(CONF_TYPE, default=image_type): validate_type(
(image_type,)
),
}
),
)
# The config schema can be a (possibly empty) single list of images,
# or a dictionary with optional keys `defaults:`, `images:` and the image types
def _config_schema(value):
if isinstance(value, list) or (
isinstance(value, dict) and (CONF_ID in value or CONF_FILE in value)
):
return cv.ensure_list(cv.All(IMAGE_SCHEMA, validate_settings))(value)
if not isinstance(value, dict):
raise cv.Invalid(
"Badly formed image configuration, expected a list or a dictionary",
)
return cv.All(
cv.Schema(
{
cv.Optional(CONF_DEFAULTS, default={}): DEFAULTS_SCHEMA,
cv.Optional(CONF_IMAGES, default=[]): cv.ensure_list(
{
**IMAGE_SCHEMA_NO_DEFAULTS,
cv.Optional(CONF_TYPE): validate_type(IMAGE_TYPE),
}
),
**{cv.Optional(t.lower()): typed_image_schema(t) for t in IMAGE_TYPE},
}
),
validate_defaults,
)(value)
CONFIG_SCHEMA = _config_schema
def _final_validate(config):
"""
For LVGL 9 the default byte order for RGB565 images is little-endian
:param config:
:return:
"""
config = config.copy()
for c in config:
if byte_order := c.get(CONF_BYTE_ORDER):
if byte_order == "BIG_ENDIAN":
_LOGGER.warning(
"The image '%s' is configured with big-endian byte order, little-endian is expected",
c.get(CONF_FILE),
)
else:
c[CONF_BYTE_ORDER] = "LITTLE_ENDIAN"
return config
FINAL_VALIDATE_SCHEMA = _final_validate
async def write_image(config, all_frames=False):
path = Path(config[CONF_FILE])
if not path.is_file():
raise core.EsphomeError(f"Could not load image file {path}")
resize = config.get(CONF_RESIZE)
try:
if is_svg_file(path):
import resvg_py
resize = resize or (None, None)
image_data = resvg_py.svg_to_bytes(
svg_path=str(path), width=resize[0], height=resize[1], dpi=100
)
# Convert bytes to Pillow Image
image = Image.open(io.BytesIO(image_data))
width, height = image.size
else:
image = Image.open(path)
width, height = image.size
if resize:
# Preserve aspect ratio
new_width_max = min(width, resize[0])
new_height_max = min(height, resize[1])
ratio = min(new_width_max / width, new_height_max / height)
width, height = int(width * ratio), int(height * ratio)
except (OSError, UnidentifiedImageError, ValueError) as exc:
raise core.EsphomeError(f"Could not read image file {path}: {exc}") from exc
if not resize and (width > 500 or height > 500):
_LOGGER.warning(
'The image "%s" you requested is very big. Please consider'
" using the resize parameter.",
path,
)
dither = (
Image.Dither.NONE
if config[CONF_DITHER] == "NONE"
else Image.Dither.FLOYDSTEINBERG
)
type = config[CONF_TYPE]
transparency = config.get(CONF_TRANSPARENCY, CONF_OPAQUE)
invert_alpha = config[CONF_INVERT_ALPHA]
frame_count = 1
if all_frames:
with contextlib.suppress(AttributeError):
frame_count = image.n_frames
if frame_count <= 1:
_LOGGER.warning("Image file %s has no animation frames", path)
# Encode each frame with its own encoder and concatenate. This keeps every
# frame self-contained on disk (e.g. RGB565+alpha emits [RGB plane | alpha plane]
# per frame) so animation frame stepping in image.cpp / animation.cpp stays
# correct without needing to know the total frame count.
byte_order = config.get(CONF_BYTE_ORDER)
combined_data: list[int] = []
encoder: ImageEncoder | None = None
for frame_index in range(frame_count):
image.seek(frame_index)
encoder = IMAGE_TYPE[type](width, height, transparency, dither, invert_alpha)
if byte_order is not None:
# Check for valid type has already been done in validate_settings
encoder.set_big_endian(byte_order == "BIG_ENDIAN")
pixels = encoder.convert(image.resize((width, height)), path).getdata()
for row in range(height):
for col in range(width):
encoder.encode(pixels[row * width + col])
encoder.end_row()
encoder.end_image()
combined_data.extend(encoder.data)
rhs = [HexInt(x) for x in combined_data]
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
image_type = get_image_type_enum(type)
trans_value = get_transparency_enum(encoder.transparency)
return prog_arr, width, height, image_type, trans_value, frame_count
def add_metadata(id: str, width: int, height: int, image_type: str, transparency):
all_metadata = CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_METADATA, {})
all_metadata[str(id)] = ImageMetaData(
@@ -780,17 +388,10 @@ def add_metadata(id: str, width: int, height: int, image_type: str, transparency
)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
# Base platform-component codegen: each entry is generated by its platform's
# own ``to_code``; here we only need the feature define to be present.
cg.add_define("USE_IMAGE")
# By now the config will be a simple list.
for entry in config:
prog_arr, width, height, image_type, trans_value, _ = await write_image(entry)
cg.new_Pvariable(
entry[CONF_ID], prog_arr, width, height, image_type, trans_value
)
add_metadata(
entry[CONF_ID], width, height, entry[CONF_TYPE], entry[CONF_TRANSPARENCY]
)
def get_all_image_metadata() -> dict[str, ImageMetaData]:
@@ -801,3 +402,198 @@ def get_all_image_metadata() -> dict[str, ImageMetaData]:
def get_image_metadata(image_id: str) -> ImageMetaData | None:
"""Get image metadata by ID for use by other components."""
return get_all_image_metadata().get(image_id)
# ---------------------------------------------------------------------------
# Legacy top-level component -> `image:` platform deprecation helpers
# -- REMOVE after 2027.1.0 together with the `animation:`/`online_image:` shims.
#
# `animation:` and `online_image:` used to be standalone top-level components and
# are now platforms of `image:`. Their deprecated top-level shims use this helper
# to (1) record each raw entry as it is validated and (2) print a single,
# pasteable migrated `image:` block once every entry has been seen. The block is
# emitted from FINAL_VALIDATE_SCHEMA, which always runs after every per-entry
# CONFIG_SCHEMA step, so all entries are captured before it fires.
# ---------------------------------------------------------------------------
def legacy_platform_migration_warning(
domain: str, platform: str, removal_version: str
) -> tuple[
Callable[[ConfigType], ConfigType],
Callable[[ConfigType], ConfigType],
]:
"""Build the per-entry capture and one-shot warning validators for a
deprecated top-level component that is now an ``image:`` platform.
Returns ``(capture, finalize)``:
* ``capture`` is a ``CONFIG_SCHEMA`` validator placed *before* the real
schema so it sees the raw user entry; it records a copy of each entry.
* ``finalize`` is a ``FINAL_VALIDATE_SCHEMA`` validator that warns exactly
once with the migrated, pasteable ``image:`` block.
"""
entries_key = "legacy_entries"
shown_key = "legacy_warning_shown"
def capture(config: ConfigType) -> ConfigType:
data = CORE.data.setdefault(domain, {})
data.setdefault(entries_key, []).append(dict(config))
return config
def finalize(config: ConfigType) -> ConfigType:
data = CORE.data.setdefault(domain, {})
if not data.get(shown_key):
data[shown_key] = True
from esphome import yaml_util
migrated = [
{CONF_PLATFORM: platform, **entry}
for entry in data.get(entries_key, [])
]
_LOGGER.warning(
"The top-level '%s:' configuration is deprecated and will be "
"removed in ESPHome %s. '%s' is now a platform of the 'image' "
"component. Replace your '%s:' block with:\n\n%s",
domain,
removal_version,
domain,
domain,
yaml_util.dump({DOMAIN: migrated}),
)
return config
return capture, finalize
# ---------------------------------------------------------------------------
# Legacy `image:` config migration -- REMOVE after 2027.1.0
#
# Before `image` became a platform component, its top-level config was either a
# bare list of image dicts, a single image dict, or a dict with `defaults:`,
# `images:` and per-type group keys. This block transparently rewrites those
# forms into the new ``platform: file`` list and prints the migrated YAML.
# It is intentionally self-contained so it can be deleted in one piece together
# with the ``LEGACY_CONFIG_MIGRATE`` assignment below.
# ---------------------------------------------------------------------------
LEGACY_REMOVAL_VERSION = "2027.1.0"
def _is_new_image_format(config: object) -> bool:
"""True when the config is already the new ``platform:``-tagged list."""
return isinstance(config, list) and all(
isinstance(entry, dict) and CONF_PLATFORM in entry for entry in config
)
def _is_legacy_image_format(config: object) -> bool:
"""True when ``config`` matches a shape the pre-platform schema accepted.
Only these shapes are migrated. Anything else -- a list containing a
non-dict (or already platform-tagged) entry, or a dict with no recognised
image keys -- is left untouched so the platform validation surfaces a
proper error instead of the migration silently dropping the input.
"""
if isinstance(config, list):
# A bare list of (not-yet-platform-tagged) image dicts.
return bool(config) and all(
isinstance(entry, dict) and CONF_PLATFORM not in entry for entry in config
)
if not isinstance(config, dict):
return False
# A single image dict, or the grouped `defaults:`/`images:`/type-key form.
return (
CONF_ID in config
or CONF_FILE in config
or any(
key in (CONF_DEFAULTS, CONF_IMAGES) or key.upper() in IMAGE_TYPE
for key in config
)
)
def _flatten_legacy_image_config(config: object) -> list[dict]:
"""Structurally flatten a legacy ``image:`` config into image dicts.
No validation or file IO is performed -- the ``file`` platform schema
validates the resulting entries. Unrecognised shapes yield no entries so the
normal platform validation surfaces the error.
"""
if isinstance(config, list):
return [dict(entry) for entry in config if isinstance(entry, dict)]
if not isinstance(config, dict):
return []
if CONF_ID in config or CONF_FILE in config:
return [dict(config)]
defaults = config.get(CONF_DEFAULTS) or {}
result: list[dict] = []
def _add(entry: dict, extra: dict) -> None:
merged = {**defaults, **extra, **entry}
# The legacy `defaults:`/type-grouped forms only applied `byte_order` to
# types that support it. Replicate that so an endian default merged into
# e.g. a binary image stays valid.
type_class = IMAGE_TYPE.get(str(merged.get(CONF_TYPE, "")).upper())
if (
CONF_BYTE_ORDER in merged
and isinstance(type_class, type)
and issubclass(type_class, ImageEncoder)
and not type_class.is_endian()
):
del merged[CONF_BYTE_ORDER]
result.append(merged)
def _add_entries(entries: object, extra: dict) -> None:
# `entries` may be a single image dict or a list of them; non-dict
# members are silently skipped, mirroring the old `ensure_list` leniency.
for entry in [entries] if isinstance(entries, dict) else entries:
if isinstance(entry, dict):
_add(entry, extra)
_add_entries(config.get(CONF_IMAGES, []), {})
for key, value in config.items():
if key in (CONF_DEFAULTS, CONF_IMAGES) or key.upper() not in IMAGE_TYPE:
continue
type_extra = {CONF_TYPE: key}
if isinstance(value, dict) and (
transparency_keys := [k for k in value if k in TRANSPARENCY_TYPES]
):
for trans in transparency_keys:
_add_entries(value[trans], {**type_extra, CONF_TRANSPARENCY: trans})
elif isinstance(value, (list, dict)):
_add_entries(value, type_extra)
return result
def _migrate_legacy_image_config(config: object) -> list[dict] | None:
"""Rewrite a legacy ``image:`` config into the ``platform: file`` list.
Returns None for the already-migrated platform form and for any shape the
pre-platform schema never accepted, so normal platform validation can
surface a proper error instead of the migration silently discarding input.
"""
if _is_new_image_format(config) or not _is_legacy_image_format(config):
return None
migrated = [
{CONF_PLATFORM: PLATFORM_FILE, **entry}
for entry in _flatten_legacy_image_config(config)
]
from esphome import yaml_util
_LOGGER.warning(
"The 'image:' configuration format is deprecated and will be removed in "
"ESPHome %s. Images are now platforms of the 'image' component. Replace "
"your 'image:' block with:\n\n%s",
LEGACY_REMOVAL_VERSION,
yaml_util.dump({DOMAIN: migrated}),
)
return migrated
LEGACY_CONFIG_MIGRATE = _migrate_legacy_image_config
# --------------------------- end legacy migration --------------------------
+1 -1
View File
@@ -21,7 +21,6 @@ from esphome.const import (
UNIT_EMPTY,
UNIT_KELVIN,
UNIT_KILOWATT,
UNIT_LITRE_PER_HOUR,
)
CODEOWNERS = ["@cfeenstra1024"]
@@ -38,6 +37,7 @@ 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 = "libretiny.gpio";
static const char *const TAG = "lt.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 = "libretiny";
static const char *const TAG = "lt.component";
void LTComponent::dump_config() {
ESP_LOGCONFIG(TAG,
+8 -10
View File
@@ -213,19 +213,17 @@ 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_;
// 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()) {
// 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) {
this->state_ = false;
this->set_flag_(FLAG_HAS_STATE);
}
// 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) {
if (color_mode & ColorCapability::BRIGHTNESS) {
// Reset brightness so the light has nonzero brightness when turned back on.
this->brightness_ = 1.0f;
this->set_flag_(FLAG_HAS_BRIGHTNESS);
} 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);
}
}
-6
View File
@@ -414,10 +414,6 @@ 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)
@@ -602,7 +598,6 @@ 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,
@@ -647,7 +642,6 @@ 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,14 +758,12 @@ 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"
+21 -37
View File
@@ -401,10 +401,7 @@ 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) {
// 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_) {
if (!this->is_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,
@@ -623,20 +620,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::displays_busy_() const {
if (!this->update_when_display_idle_)
return false;
for (auto *disp : this->displays_) {
if (!disp->is_idle())
return true;
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;
}
}
return false;
}
@@ -780,8 +777,6 @@ 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, ')');
@@ -807,32 +802,21 @@ void LvglComponent::update() {
}
void LvglComponent::loop() {
if (this->paused_) {
if (this->show_snow_)
if (this->is_paused()) {
if (this->paused_ && this->show_snow_)
this->write_random_();
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_);
} 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
}
lv_timer_handler();
}
#ifdef USE_LVGL_ANIMIMG
+2 -18
View File
@@ -214,14 +214,9 @@ 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 has been explicitly paused via set_paused().
bool is_paused() const { return this->paused_; }
// Returns true if the display is explicitly paused, or a blocking display update is in progress.
bool is_paused() const;
// 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_) {
@@ -304,9 +299,6 @@ 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);
@@ -324,14 +316,6 @@ 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,6 +10,7 @@ from esphome.components.mipi import (
GMCTR,
GMCTRN1,
GMCTRP1,
IDMOFF,
IFCTR,
IFMODE,
INVCTR,
@@ -22,6 +23,7 @@ from esphome.components.mipi import (
PWCTR5,
PWSET,
PWSETN,
SETEXTC,
VMCTR,
VMCTR1,
VMCTR2,
@@ -30,6 +32,60 @@ 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,
@@ -118,6 +174,22 @@ 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,
@@ -1,71 +0,0 @@
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,
)
+12 -13
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.data()[0];
uint8_t expected_address = wfr.frame.data.get()[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.data()[0];
uint8_t expected_function_code = wfr.frame.data.data()[1];
uint8_t expected_address = wfr.frame.data.get()[0];
uint8_t expected_function_code = wfr.frame.data.get()[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.data(), frame.size());
this->write_array(frame.data.get(), frame.size);
this->flush();
this->flow_control_pin_->digital_write(false);
this->last_send_tx_offset_ = 0;
} else {
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;
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;
}
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.data(), frame.size()), now - this->last_send_,
format_hex_pretty_to(hex_buf, frame.data.get(), frame.size), now - this->last_send_,
now - this->last_modbus_byte_);
this->last_send_ = now;
return true;
@@ -590,13 +590,12 @@ 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.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[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.data()[0] == address) {
if (this->waiting_for_response_.value().frame.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;
+10 -18
View File
@@ -18,27 +18,19 @@ namespace esphome::modbus {
static constexpr uint16_t MODBUS_TX_BUFFER_SIZE = 15;
static constexpr uint16_t MODBUS_TX_MAX_DELAY_MS = 5;
// 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;
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
// 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
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;
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;
}
uint16_t size() const { return static_cast<uint16_t>(this->data.size()); }
};
class Modbus : public uart::UARTDevice, public Component {
-13
View File
@@ -59,19 +59,6 @@ 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,7 +1,5 @@
#include "util.h"
#include "esphome/core/application.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#ifdef USE_NETWORK
namespace esphome::network {
@@ -22,27 +20,6 @@ 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)
+24 -7
View File
@@ -1,7 +1,6 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_NETWORK
#include <span>
#include <string>
#include "esphome/core/helpers.h"
#include "ip_address.h"
@@ -54,12 +53,30 @@ ESPHOME_ALWAYS_INLINE inline bool is_connected() {
/// Return whether the network is disabled (only wifi for now)
bool is_disabled();
/// 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);
/// 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
}
IPAddresses get_ip_addresses();
} // namespace esphome::network
+21 -136
View File
@@ -1,150 +1,35 @@
import logging
# ---------------------------------------------------------------------------
# Legacy top-level `online_image:` deprecation shim -- REMOVE this whole file
# after 2027.1.0.
#
# Online images are now a platform of the `image:` component (`platform:
# online_image`); the real schema, actions and codegen live in `image.py`. This
# module only keeps the deprecated top-level `online_image:` key working during
# the deprecation window: it reuses that schema/codegen and adds a one-shot
# deprecation warning (with a pasteable migrated `image:` block) at validation
# time. Deleting this file drops the top-level form entirely.
# ---------------------------------------------------------------------------
from esphome import automation
import esphome.codegen as cg
from esphome.components import runtime_image
from esphome.components.const import CONF_REQUEST_HEADERS
from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent
from esphome.components.image import CONF_TRANSPARENCY, add_metadata
import esphome.components.image as espImage
import esphome.config_validation as cv
from esphome.const import CONF_BUFFER_SIZE, CONF_ID, CONF_ON_ERROR, CONF_TYPE, CONF_URL
from esphome.core import Lambda
from .image import ONLINE_IMAGE_CONFIG_SCHEMA, setup_online_image
AUTO_LOAD = ["image", "runtime_image"]
DEPENDENCIES = ["display", "http_request"]
CODEOWNERS = ["@guillempages", "@clydebarrow"]
MULTI_CONF = True
CONF_ON_DOWNLOAD_FINISHED = "on_download_finished"
CONF_UPDATE = "update"
DOMAIN = "online_image"
_LOGGER = logging.getLogger(__name__)
LEGACY_REMOVAL_VERSION = "2027.1.0"
online_image_ns = cg.esphome_ns.namespace("online_image")
OnlineImage = online_image_ns.class_(
"OnlineImage", cg.PollingComponent, runtime_image.RuntimeImage
_capture_legacy_entry, _warn_legacy_online_image = (
espImage.legacy_platform_migration_warning(DOMAIN, DOMAIN, LEGACY_REMOVAL_VERSION)
)
# Actions
SetUrlAction = online_image_ns.class_(
"OnlineImageSetUrlAction", automation.Action, cg.Parented.template(OnlineImage)
)
ReleaseImageAction = online_image_ns.class_(
"OnlineImageReleaseAction", automation.Action, cg.Parented.template(OnlineImage)
)
CONFIG_SCHEMA = cv.All(_capture_legacy_entry, ONLINE_IMAGE_CONFIG_SCHEMA)
FINAL_VALIDATE_SCHEMA = _warn_legacy_online_image
ONLINE_IMAGE_SCHEMA = (
runtime_image.runtime_image_schema(OnlineImage)
.extend(
{
# Online Image specific options
cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent),
cv.Required(CONF_URL): cv.url,
cv.Optional(CONF_BUFFER_SIZE, default=65536): cv.int_range(256, 65536),
cv.Optional(CONF_REQUEST_HEADERS): cv.All(
cv.Schema({cv.string: cv.templatable(cv.string)})
),
cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation({}),
cv.Optional(CONF_ON_ERROR): automation.validate_automation({}),
}
)
.extend(cv.polling_component_schema("never"))
)
CONFIG_SCHEMA = cv.Schema(
cv.All(
ONLINE_IMAGE_SCHEMA,
cv.require_framework_version(
# esp8266 not supported yet; if enabled in the future, minimum version of 2.7.0 is needed
# esp8266_arduino=cv.Version(2, 7, 0),
esp32_arduino=cv.Version(0, 0, 0),
esp_idf=cv.Version(4, 0, 0),
rp2_arduino=cv.Version(0, 0, 0),
host=cv.Version(0, 0, 0),
),
runtime_image.validate_runtime_image_settings,
)
)
SET_URL_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.use_id(OnlineImage),
cv.Required(CONF_URL): cv.templatable(cv.url),
cv.Optional(CONF_UPDATE, default=True): cv.templatable(bool),
}
)
RELEASE_IMAGE_SCHEMA = automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(OnlineImage),
}
)
@automation.register_action(
"online_image.set_url", SetUrlAction, SET_URL_SCHEMA, synchronous=True
)
@automation.register_action(
"online_image.release",
ReleaseImageAction,
RELEASE_IMAGE_SCHEMA,
synchronous=True,
)
async def online_image_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
if CONF_URL in config:
template_ = await cg.templatable(config[CONF_URL], args, cg.std_string)
cg.add(var.set_url(template_))
if CONF_UPDATE in config:
template_ = await cg.templatable(config[CONF_UPDATE], args, cg.bool_)
cg.add(var.set_update(template_))
return var
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_DOWNLOAD_FINISHED, "add_on_finished_callback", [(bool, "cached")]
),
automation.CallbackAutomation(CONF_ON_ERROR, "add_on_error_callback"),
)
async def to_code(config):
# Use the enhanced helper function to get all runtime image parameters
settings = await runtime_image.process_runtime_image_config(config)
add_metadata(
config[CONF_ID],
settings.width,
settings.height,
config[CONF_TYPE],
config[CONF_TRANSPARENCY],
)
url = config[CONF_URL]
var = cg.new_Pvariable(
config[CONF_ID],
url,
settings.width,
settings.height,
settings.format_enum,
settings.image_type_enum,
settings.transparent,
settings.placeholder or cg.nullptr,
config[CONF_BUFFER_SIZE],
settings.byte_order_big_endian,
)
await cg.register_component(var, config)
await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID])
for key, value in config.get(CONF_REQUEST_HEADERS, {}).items():
if isinstance(value, Lambda):
template_ = await cg.templatable(value, [], cg.std_string)
cg.add(var.add_request_header(key, template_))
else:
cg.add(var.add_request_header(key, value))
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
to_code = setup_online_image
+152
View File
@@ -0,0 +1,152 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components import runtime_image
from esphome.components.const import CONF_REQUEST_HEADERS
from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent
from esphome.components.image import CONF_TRANSPARENCY, add_metadata
import esphome.config_validation as cv
from esphome.const import CONF_BUFFER_SIZE, CONF_ID, CONF_ON_ERROR, CONF_TYPE, CONF_URL
from esphome.core import Lambda
from esphome.types import ConfigType
AUTO_LOAD = ["runtime_image"]
DEPENDENCIES = ["http_request"]
CODEOWNERS = ["@guillempages", "@clydebarrow"]
CONF_ON_DOWNLOAD_FINISHED = "on_download_finished"
CONF_UPDATE = "update"
online_image_ns = cg.esphome_ns.namespace("online_image")
OnlineImage = online_image_ns.class_(
"OnlineImage", cg.PollingComponent, runtime_image.RuntimeImage
)
# Actions
SetUrlAction = online_image_ns.class_(
"OnlineImageSetUrlAction", automation.Action, cg.Parented.template(OnlineImage)
)
ReleaseImageAction = online_image_ns.class_(
"OnlineImageReleaseAction", automation.Action, cg.Parented.template(OnlineImage)
)
ONLINE_IMAGE_SCHEMA = (
runtime_image.runtime_image_schema(OnlineImage)
.extend(
{
# Online Image specific options
cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent),
cv.Required(CONF_URL): cv.url,
cv.Optional(CONF_BUFFER_SIZE, default=65536): cv.int_range(256, 65536),
cv.Optional(CONF_REQUEST_HEADERS): cv.All(
cv.Schema({cv.string: cv.templatable(cv.string)})
),
cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation({}),
cv.Optional(CONF_ON_ERROR): automation.validate_automation({}),
}
)
.extend(cv.polling_component_schema("never"))
)
# Shared schema used by both the (deprecated) top-level `online_image:` key and
# the `image:` `platform: online_image` entry.
ONLINE_IMAGE_CONFIG_SCHEMA = cv.All(
ONLINE_IMAGE_SCHEMA,
cv.require_framework_version(
# esp8266 not supported yet; if enabled in the future, minimum version of 2.7.0 is needed
# esp8266_arduino=cv.Version(2, 7, 0),
esp32_arduino=cv.Version(0, 0, 0),
esp_idf=cv.Version(4, 0, 0),
rp2_arduino=cv.Version(0, 0, 0),
host=cv.Version(0, 0, 0),
),
runtime_image.validate_runtime_image_settings,
)
SET_URL_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.use_id(OnlineImage),
cv.Required(CONF_URL): cv.templatable(cv.url),
cv.Optional(CONF_UPDATE, default=True): cv.templatable(bool),
}
)
RELEASE_IMAGE_SCHEMA = automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(OnlineImage),
}
)
@automation.register_action(
"online_image.set_url", SetUrlAction, SET_URL_SCHEMA, synchronous=True
)
@automation.register_action(
"online_image.release",
ReleaseImageAction,
RELEASE_IMAGE_SCHEMA,
synchronous=True,
)
async def online_image_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
if CONF_URL in config:
template_ = await cg.templatable(config[CONF_URL], args, cg.std_string)
cg.add(var.set_url(template_))
if CONF_UPDATE in config:
template_ = await cg.templatable(config[CONF_UPDATE], args, cg.bool_)
cg.add(var.set_update(template_))
return var
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_DOWNLOAD_FINISHED, "add_on_finished_callback", [(bool, "cached")]
),
automation.CallbackAutomation(CONF_ON_ERROR, "add_on_error_callback"),
)
async def setup_online_image(config: ConfigType) -> None:
# Use the enhanced helper function to get all runtime image parameters
settings = await runtime_image.process_runtime_image_config(config)
add_metadata(
config[CONF_ID],
settings.width,
settings.height,
config[CONF_TYPE],
config[CONF_TRANSPARENCY],
)
url = config[CONF_URL]
var = cg.new_Pvariable(
config[CONF_ID],
url,
settings.width,
settings.height,
settings.format_enum,
settings.image_type_enum,
settings.transparent,
settings.placeholder or cg.nullptr,
config[CONF_BUFFER_SIZE],
settings.byte_order_big_endian,
)
await cg.register_component(var, config)
await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID])
for key, value in config.get(CONF_REQUEST_HEADERS, {}).items():
if isinstance(value, Lambda):
template_ = await cg.templatable(value, [], cg.std_string)
cg.add(var.add_request_header(key, template_))
else:
cg.add(var.add_request_header(key, value))
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
CONFIG_SCHEMA = ONLINE_IMAGE_CONFIG_SCHEMA
to_code = setup_online_image
+1 -2
View File
@@ -14,7 +14,6 @@ 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
@@ -289,7 +288,7 @@ async def to_code(config):
enable_mdns_storage()
ot = cg.new_Pvariable(config[CONF_ID])
add_use_address(ot, config[CONF_USE_ADDRESS])
cg.add(ot.set_use_address(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))
+1 -3
View File
@@ -39,8 +39,6 @@ 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
@@ -78,7 +76,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_{nullptr};
const char *use_address_{""};
};
extern OpenThreadComponent *global_openthread_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
-28
View File
@@ -2,34 +2,6 @@
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,24 +46,9 @@ 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,9 +9,6 @@
#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 {
@@ -162,23 +159,6 @@ 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;
+2 -49
View File
@@ -5,7 +5,6 @@ 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,
@@ -17,11 +16,6 @@ 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"
@@ -33,60 +27,26 @@ 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 = cv.All(
CONFIG_SCHEMA = (
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)),
_validate_volume_range,
.extend(i2c.i2c_device_schema(0x4D))
)
@@ -136,10 +96,3 @@ 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))
+5 -99
View File
@@ -10,12 +10,6 @@ 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");
@@ -57,22 +51,7 @@ 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");
@@ -88,40 +67,15 @@ 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_, 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_);
this->bits_per_sample_, YESNO(this->is_muted_));
}
bool PCM5122::set_mute_off() {
@@ -164,11 +118,11 @@ bool PCM5122::write_mute_() {
}
bool PCM5122::write_volume_() {
// 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.
// 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.
// Use set_mute_on() for silence.
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 dvol_max_volume = 0x30; // 0 dB at full scale
const uint8_t dvol_min_volume = 0x99; // -52.5 dB at minimum
const uint8_t volume_byte =
dvol_max_volume + static_cast<uint8_t>(lroundf((1.0f - this->volume_) * (dvol_min_volume - dvol_max_volume)));
@@ -183,52 +137,4 @@ 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
+2 -47
View File
@@ -3,7 +3,6 @@
#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 {
@@ -11,13 +10,11 @@ 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
@@ -26,9 +23,6 @@ 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)
@@ -41,33 +35,12 @@ 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;
@@ -75,11 +48,6 @@ 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;
@@ -88,30 +56,17 @@ 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_();
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
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
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
@@ -1,32 +0,0 @@
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]))
@@ -1,12 +0,0 @@
#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
@@ -1,24 +0,0 @@
#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
+14 -38
View File
@@ -3,7 +3,6 @@
#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"
@@ -397,14 +396,7 @@ int USBUartTypeFT23XX::set_dtr_rts_(USBUartChannel *channel) {
}
void USBUartTypeFT23XX::start_input(USBUartChannel *channel) {
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))
if (!channel->initialised_.load() || channel->input_started_.load())
return;
const auto *ep = channel->cdc_dev_.in_ep;
@@ -416,55 +408,39 @@ 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_) {
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);
// 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);
#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 if (status.data_len >= 2) {
} else {
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);
this->start_input(channel);
if (channel->dummy_receiver_ ||
channel->input_buffer_.get_free_space() >= channel->cdc_dev_.in_ep->wMaxPacketSize) {
this->start_input(channel);
}
};
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();
channel->input_started_.store(true);
this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize);
}
void USBUartTypeFT23XX::enable_channels() {
-5
View File
@@ -228,11 +228,6 @@ 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);
+2 -7
View File
@@ -192,13 +192,9 @@ class USBUartComponent : public usb_host::USBClient {
void add_channel(USBUartChannel *channel) { this->channels_.push_back(channel); }
virtual void start_input(USBUartChannel *channel);
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_;
@@ -252,8 +248,7 @@ class USBUartTypeFT23XX : public USBUartTypeCdcAcm {
public:
USBUartTypeFT23XX(uint16_t vid, uint16_t pid) : USBUartTypeCdcAcm(vid, pid) {}
void start_input(USBUartChannel *channel) override;
void on_rx_overflow(USBUartChannel *channel) override;
void start_input(USBUartChannel *channel);
protected:
std::vector<CdcEps> parse_descriptors(usb_device_handle_t dev_hdl) override;
+12 -14
View File
@@ -257,10 +257,8 @@ 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, size_t message_len, const char *event,
uint32_t id, uint32_t reconnect) {
// ESPAsyncWebServer's send() only accepts null-terminated strings
(void) message_len;
void DeferredUpdateEventSource::try_send_nodefer(const char *message, const char *event, uint32_t id,
uint32_t reconnect) {
this->send(message, event, id, reconnect);
}
@@ -281,10 +279,10 @@ void DeferredUpdateEventSourceList::deferrable_send_state(void *source, const ch
}
}
void DeferredUpdateEventSourceList::try_send_nodefer(const char *message, size_t message_len, const char *event,
uint32_t id, uint32_t reconnect) {
void DeferredUpdateEventSourceList::try_send_nodefer(const char *message, const char *event, uint32_t id,
uint32_t reconnect) {
for (DeferredUpdateEventSource *dues : *this) {
dues->try_send_nodefer(message, message_len, event, id, reconnect);
dues->try_send_nodefer(message, event, id, reconnect);
}
}
@@ -306,7 +304,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(), message.size(), "ping", millis(), 30000);
source->try_send_nodefer(message.c_str(), "ping", millis(), 30000);
#ifdef USE_WEBSERVER_SORTING
for (auto &group : ws->sorting_groups_) {
@@ -317,7 +315,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(), group_msg.size(), "sorting_group");
source->try_send_nodefer(group_msg.c_str(), "sorting_group");
}
#endif
@@ -397,8 +395,8 @@ void WebServer::setup() {
return;
char buf[32];
auto uptime = static_cast<uint32_t>(millis_64() / 1000);
size_t len = buf_append_printf(buf, sizeof(buf), 0, "{\"uptime\":%" PRIu32 "}", uptime);
this->events_.try_send_nodefer(buf, len, "ping", millis(), 30000);
buf_append_printf(buf, sizeof(buf), 0, "{\"uptime\":%" PRIu32 "}", uptime);
this->events_.try_send_nodefer(buf, "ping", millis(), 30000);
});
}
void WebServer::loop() {
@@ -416,16 +414,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;
this->events_.try_send_nodefer(message, message_len, "log", millis());
(void) message_len;
this->events_.try_send_nodefer(message, "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_to(addr_buf), this->base_->get_port());
network::get_use_address(), this->base_->get_port());
}
float WebServer::get_setup_priority() const { return setup_priority::WIFI - 1.0f; }
+2 -4
View File
@@ -160,8 +160,7 @@ 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, size_t message_len, const char *event = nullptr, uint32_t id = 0,
uint32_t reconnect = 0);
void try_send_nodefer(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0);
};
class DeferredUpdateEventSourceList final : public std::list<DeferredUpdateEventSource *> {
@@ -174,8 +173,7 @@ 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, size_t message_len, const char *event = nullptr, uint32_t id = 0,
uint32_t reconnect = 0);
void try_send_nodefer(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0);
void add_new_client(WebServer *ws, AsyncWebServerRequest *request);
};
@@ -546,11 +546,10 @@ void AsyncEventSource::adopt_pending_sessions_main_loop_() {
}
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
void AsyncEventSource::try_send_nodefer(const char *message, size_t message_len, const char *event, uint32_t id,
uint32_t reconnect) {
void AsyncEventSource::try_send_nodefer(const char *message, 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, message_len, event, id, reconnect);
ses->try_send_nodefer(message, event, id, reconnect);
}
}
}
@@ -601,7 +600,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(), message.size(), "ping", millis(), 30000);
this->try_send_nodefer(message.c_str(), "ping", millis(), 30000);
#ifdef USE_WEBSERVER_SORTING
for (auto &group : ws->sorting_groups_) {
@@ -613,7 +612,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(), message.size(), "sorting_group");
this->try_send_nodefer(message.c_str(), "sorting_group");
}
#endif
@@ -648,7 +647,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(), message.size(), "state")) {
if (this->try_send_nodefer(message.c_str(), "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 {
@@ -719,7 +718,7 @@ void AsyncEventSourceResponse::loop() {
this->entities_iterator_.advance();
}
bool AsyncEventSourceResponse::try_send_nodefer(const char *message, size_t message_len, const char *event, uint32_t id,
bool AsyncEventSourceResponse::try_send_nodefer(const char *message, const char *event, uint32_t id,
uint32_t reconnect) {
if (this->fd_.load() == 0) {
return false;
@@ -765,18 +764,19 @@ bool AsyncEventSourceResponse::try_send_nodefer(const char *message, size_t mess
// Fast path: check if message contains any newlines at all
// Most SSE messages (JSON state updates) have no newlines
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));
const char *first_n = strchr(message, '\n');
const char *first_r = strchr(message, '\r');
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, message_len);
event_buffer_.append(message);
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;
const char *msg_end = message + message_len;
size_t msg_len = strlen(message);
const char *msg_end = message + msg_len;
// Reuse the first search results
const char *next_n = first_n;
@@ -789,7 +789,7 @@ bool AsyncEventSourceResponse::try_send_nodefer(const char *message, size_t mess
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, msg_end - line_start);
event_buffer_.append(line_start);
event_buffer_.append(CRLF_STR, CRLF_LEN);
break;
}
@@ -828,8 +828,8 @@ bool AsyncEventSourceResponse::try_send_nodefer(const char *message, size_t mess
}
// Search for next newlines only in remaining string
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));
next_n = strchr(line_start, '\n');
next_r = strchr(line_start, '\r');
}
// 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(), message.size(), "state")) {
if (!this->try_send_nodefer(message.c_str(), "state")) {
deq_push_back_with_dedup_(source, message_generator);
}
}
@@ -291,8 +291,7 @@ class AsyncEventSourceResponse {
friend class AsyncEventSource;
public:
bool try_send_nodefer(const char *message, size_t message_len, const char *event = nullptr, uint32_t id = 0,
uint32_t reconnect = 0);
bool try_send_nodefer(const char *message, 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();
@@ -344,8 +343,7 @@ 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, size_t message_len, const char *event = nullptr, uint32_t id = 0,
uint32_t reconnect = 0);
void try_send_nodefer(const char *message, 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();
+1 -2
View File
@@ -14,7 +14,6 @@ from esphome.components.esp32 import (
request_wifi,
)
from esphome.components.network import (
add_use_address,
has_high_performance_networking,
ip_address_literal,
)
@@ -586,7 +585,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])
add_use_address(var, config[CONF_USE_ADDRESS])
cg.add(var.set_use_address(config[CONF_USE_ADDRESS]))
# Track if any network uses Enterprise authentication
has_eap = False
+1 -3
View File
@@ -501,8 +501,6 @@ 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; }
@@ -998,7 +996,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_{nullptr};
const char *use_address_{""};
};
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 final : public sensor::Sensor, public uart::UARTDevice, public Component {
class WTS01Sensor : 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 final : public output::FloatOutput, public Component {
class X9cOutput : 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 final : public PollingComponent, public i2c::I2CDevice {
class XDB401Component : 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 final : public PollingComponent, public sensor::Sensor, public i2c::I2CDevice {
class XGZP68XXComponent : 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 final : public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiListener : 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 final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiCGDK2 : 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 final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiCGG1 : 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 final : public Component,
public binary_sensor::BinarySensorInitiallyOff,
public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiCGPR1 : 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 final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiGCLS002 : 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 final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiHHCCJCY01 : 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 final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiHHCCJCY10 : 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 final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiHHCCPOT002 : 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 final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiJQJCY01YM : 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 final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiLYWSD02 : 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 final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiLYWSD02MMC : 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 final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiLYWSD03MMC : 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 final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiLYWSDCGQ : 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 final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiMHOC303 : 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 final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiMHOC401 : 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 final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiMiscale : 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 final : public Component,
public binary_sensor::BinarySensorInitiallyOff,
public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiMJYD02YLA : 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 final : public Component,
public binary_sensor::BinarySensorInitiallyOff,
public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiMUE4094RT : 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 final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiRTCGQ02LM : 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 final : public Component,
public binary_sensor::BinarySensorInitiallyOff,
public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiWX08ZM : 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 final : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
class XiaomiXMWSDJ04MMC : 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 final : public Component, public i2c::I2CDevice {
class XL9535Component : 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 final : public Component, public i2c::I2CDevice {
float get_setup_priority() const override { return setup_priority::IO; }
};
class XL9535GPIOPin final : public GPIOPin {
class XL9535GPIOPin : 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 final : public Touchscreen,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_2MHZ> {
class XPT2046Component : 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 final : public climate::Climate, public Component {
class YashimaClimate : 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 final : public Component {
class CdcAcm : public Component {
public:
CdcAcm();
void setup() override;
+1 -1
View File
@@ -16,7 +16,7 @@ struct ZephyrGPIOInterrupt {
void *arg{nullptr};
};
class ZephyrGPIOPin final : public InternalGPIOPin {
class ZephyrGPIOPin : 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 final : public Component {
class BLEServer : public Component {
public:
void setup() override;
void dump_config() override;
@@ -21,7 +21,7 @@ class BLEServer final : public Component {
CallbackManager<void(uint32_t)> passkey_cb_;
};
template<typename... Ts> class BLENumericComparisonReplyAction final : public Action<Ts...> {
template<typename... Ts> class BLENumericComparisonReplyAction : public Action<Ts...> {
public:
explicit BLENumericComparisonReplyAction(BLEServer *parent) : parent_(parent) {}
+12
View File
@@ -599,6 +599,18 @@ class LoadValidationStep(ConfigValidationStep):
CORE.loaded_integrations.add(self.domain)
# For platform components, normalize conf before creating MetadataValidationStep
if component.is_platform_component:
# Legacy config migration: allow a platform component to rewrite a
# pre-platform-format top-level config (e.g. a bare list or legacy
# dict form) into the normalized list of `platform:` tagged entries.
# Removable deprecation shim hook; no-op for components that do not
# define LEGACY_CONFIG_MIGRATE.
if (
(migrate := component.legacy_config_migrate) is not None
and self.conf
and not isinstance(self.conf, core.AutoLoad)
and (migrated := migrate(self.conf)) is not None
):
result[self.domain] = self.conf = migrated
if not self.conf:
result[self.domain] = self.conf = []
elif not isinstance(self.conf, list):
-1
View File
@@ -1255,7 +1255,6 @@ 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,12 +239,9 @@
#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
+3 -8
View File
@@ -184,10 +184,8 @@ template<size_t InlineSize = 8> class SmallInlineBuffer {
SmallInlineBuffer(const SmallInlineBuffer &) = delete;
SmallInlineBuffer &operator=(const SmallInlineBuffer &) = delete;
/// 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) {
/// Set buffer contents, allocating heap if needed
void set(const uint8_t *src, 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_;
@@ -198,12 +196,9 @@ template<size_t InlineSize = 8> class SmallInlineBuffer {
this->heap_ = new uint8_t[size]; // NOLINT(cppcoreguidelines-owning-memory)
}
this->len_ = size;
return this->data();
memcpy(this->data(), src, size);
}
/// 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,7 +52,6 @@ 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
@@ -158,11 +157,6 @@ _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",
}

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