mirror of
https://github.com/esphome/esphome.git
synced 2026-07-10 17:05:36 +00:00
Merge branch 'dev' into app-loop-optimize-speed
This commit is contained in:
+1
-1
@@ -1 +1 @@
|
||||
dc8ad5472d9fb44ce1ca29a0601afd65705642799a2819704dfc8459fbaf9815
|
||||
c65f1a0804a7765462d570c50891ac719260592df2c9cdfe88233fc346ac59e9
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"--privileged",
|
||||
"-e",
|
||||
"GIT_EDITOR=code --wait"
|
||||
// uncomment and edit the path in order to pass though local USB serial to the conatiner
|
||||
// uncomment and edit the path in order to pass through local USB serial to the container
|
||||
// , "--device=/dev/ttyACM0"
|
||||
],
|
||||
"appPort": 6052,
|
||||
|
||||
@@ -339,7 +339,7 @@ jobs:
|
||||
echo "binary=$BINARY" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Run CodSpeed benchmarks
|
||||
uses: CodSpeedHQ/action@db35df748deb45fdef0960669f57d627c1956c30 # v4
|
||||
uses: CodSpeedHQ/action@658a901452bb54c799643e060733b7afe9121b8d # v4.14.0
|
||||
with:
|
||||
run: ${{ steps.build.outputs.binary }}
|
||||
mode: simulation
|
||||
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
|
||||
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
@@ -86,6 +86,6 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
|
||||
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -8,4 +8,4 @@ on:
|
||||
|
||||
jobs:
|
||||
lock:
|
||||
uses: esphome/workflows/.github/workflows/lock.yml@main
|
||||
uses: esphome/workflows/.github/workflows/lock.yml@3c4e8446aa1029f1c346a482034b3ee1489077ca # 2026.4.0
|
||||
|
||||
@@ -11,7 +11,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.15.10
|
||||
rev: v0.15.11
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
@@ -58,6 +58,7 @@ repos:
|
||||
entry: python3 script/run-in-env.py pylint
|
||||
language: system
|
||||
types: [python]
|
||||
files: ^esphome/.+\.py$
|
||||
- id: clang-tidy-hash
|
||||
name: Update clang-tidy hash
|
||||
entry: python script/clang_tidy_hash.py --update-if-changed
|
||||
|
||||
@@ -199,11 +199,10 @@ def validate_automation(extra_schema=None, extra_validators=None, single=False):
|
||||
return cv.Schema([schema])(value)
|
||||
except cv.Invalid as err2:
|
||||
if "extra keys not allowed" in str(err2) and len(err2.path) == 2:
|
||||
# pylint: disable=raise-missing-from
|
||||
raise err
|
||||
raise err from None
|
||||
if "Unable to find action" in str(err):
|
||||
raise err2
|
||||
raise cv.MultipleInvalid([err, err2])
|
||||
raise err2 from None
|
||||
raise cv.MultipleInvalid([err, err2]) from None
|
||||
elif isinstance(value, dict):
|
||||
if CONF_THEN in value:
|
||||
return [schema(value)]
|
||||
|
||||
+72
-6
@@ -151,8 +151,8 @@ class ConfigBundleCreator:
|
||||
|
||||
def __init__(self, config: dict[str, Any]) -> None:
|
||||
self._config = config
|
||||
self._config_dir = CORE.config_dir
|
||||
self._config_path = CORE.config_path
|
||||
self._config_dir = Path(CORE.config_dir).resolve()
|
||||
self._config_path = Path(CORE.config_path).resolve()
|
||||
self._files: list[BundleFile] = []
|
||||
self._seen_paths: set[Path] = set()
|
||||
self._secrets_paths: set[Path] = set()
|
||||
@@ -258,21 +258,36 @@ class ConfigBundleCreator:
|
||||
def _discover_yaml_includes(self) -> None:
|
||||
"""Discover YAML files loaded during config parsing.
|
||||
|
||||
We track files by wrapping _load_yaml_internal. The config has already
|
||||
been loaded at this point (bundle is a POST_CONFIG_ACTION), so we
|
||||
re-load just to discover the file list.
|
||||
Deliberately uses a fresh re-parse and force-loads every deferred
|
||||
``IncludeFile`` to include *all* potentially-reachable includes,
|
||||
even branches not selected by the local substitutions. Bundles are
|
||||
meant to be compiled on another system where command-line
|
||||
substitution overrides may choose a different branch — e.g.
|
||||
``!include network/${eth_model}/config.yaml`` must ship every
|
||||
candidate so the remote build can pick any one.
|
||||
|
||||
Entries with unresolved substitution variables in the filename
|
||||
path are skipped with a warning (they cannot be resolved without
|
||||
the substitution pass).
|
||||
|
||||
Secrets files are tracked separately so we can filter them to
|
||||
only include the keys this config actually references.
|
||||
"""
|
||||
# Must be a fresh parse: IncludeFile.load() caches its result in
|
||||
# _content, and we discover files by listening for loader calls. On
|
||||
# an already-parsed tree the cache is populated, .load() returns
|
||||
# without calling the loader, the listener never fires, and the
|
||||
# referenced files would be silently dropped from the bundle.
|
||||
with yaml_util.track_yaml_loads() as loaded_files:
|
||||
try:
|
||||
yaml_util.load_yaml(self._config_path)
|
||||
data = yaml_util.load_yaml(self._config_path)
|
||||
except EsphomeError:
|
||||
_LOGGER.debug(
|
||||
"Bundle: re-loading YAML for include discovery failed, "
|
||||
"proceeding with partial file list"
|
||||
)
|
||||
else:
|
||||
_force_load_include_files(data)
|
||||
|
||||
for fpath in loaded_files:
|
||||
if fpath == self._config_path.resolve():
|
||||
@@ -608,6 +623,57 @@ def _add_bytes_to_tar(tar: tarfile.TarFile, name: str, data: bytes) -> None:
|
||||
tar.addfile(info, io.BytesIO(data))
|
||||
|
||||
|
||||
def _force_load_include_files(obj: Any, _seen: set[int] | None = None) -> None:
|
||||
"""Recursively resolve any ``IncludeFile`` instances in a YAML tree.
|
||||
|
||||
Nested ``!include`` returns a deferred ``IncludeFile`` that is only
|
||||
resolved during the substitution pass. During bundle discovery we need
|
||||
the referenced files to actually load so the ``track_yaml_loads``
|
||||
listener fires for them.
|
||||
|
||||
``IncludeFile`` instances with unresolved substitution variables in the
|
||||
filename cannot be loaded — we skip and warn about those.
|
||||
"""
|
||||
if _seen is None:
|
||||
_seen = set()
|
||||
|
||||
if isinstance(obj, yaml_util.IncludeFile):
|
||||
if id(obj) in _seen:
|
||||
return
|
||||
_seen.add(id(obj))
|
||||
if obj.has_unresolved_expressions():
|
||||
_LOGGER.warning(
|
||||
"Bundle: cannot resolve !include %s (referenced from %s) "
|
||||
"with substitutions in path",
|
||||
obj.file,
|
||||
obj.parent_file,
|
||||
)
|
||||
return
|
||||
try:
|
||||
loaded = obj.load()
|
||||
except EsphomeError as err:
|
||||
_LOGGER.warning(
|
||||
"Bundle: failed to load !include %s (referenced from %s): %s",
|
||||
obj.file,
|
||||
obj.parent_file,
|
||||
err,
|
||||
)
|
||||
return
|
||||
_force_load_include_files(loaded, _seen)
|
||||
elif isinstance(obj, dict):
|
||||
if id(obj) in _seen:
|
||||
return
|
||||
_seen.add(id(obj))
|
||||
for value in obj.values():
|
||||
_force_load_include_files(value, _seen)
|
||||
elif isinstance(obj, (list, tuple)):
|
||||
if id(obj) in _seen:
|
||||
return
|
||||
_seen.add(id(obj))
|
||||
for item in obj:
|
||||
_force_load_include_files(item, _seen)
|
||||
|
||||
|
||||
def _resolve_include_path(include_path: Any) -> Path | None:
|
||||
"""Resolve an include path to absolute, skipping system includes."""
|
||||
if isinstance(include_path, str) and include_path.startswith("<"):
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/core/alloc_helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace anova {
|
||||
|
||||
@@ -105,14 +107,14 @@ void AnovaCodec::decode(const uint8_t *data, uint16_t length) {
|
||||
}
|
||||
case READ_TARGET_TEMPERATURE:
|
||||
case SET_TARGET_TEMPERATURE: {
|
||||
this->target_temp_ = parse_number<float>(str_until(buf, '\r')).value_or(0.0f);
|
||||
this->target_temp_ = parse_number<float>(str_until(buf, '\r')).value_or(0.0f); // NOLINT
|
||||
if (this->fahrenheit_)
|
||||
this->target_temp_ = ftoc(this->target_temp_);
|
||||
this->has_target_temp_ = true;
|
||||
break;
|
||||
}
|
||||
case READ_CURRENT_TEMPERATURE: {
|
||||
this->current_temp_ = parse_number<float>(str_until(buf, '\r')).value_or(0.0f);
|
||||
this->current_temp_ = parse_number<float>(str_until(buf, '\r')).value_or(0.0f); // NOLINT
|
||||
if (this->fahrenheit_)
|
||||
this->current_temp_ = ftoc(this->current_temp_);
|
||||
this->has_current_temp_ = true;
|
||||
|
||||
@@ -83,7 +83,7 @@ def angle_to_position(value, min=-360, max=360):
|
||||
value = angle(min=min, max=max)(value)
|
||||
return (RESOLUTION + round(value * ANGLE_TO_POSITION)) % RESOLUTION
|
||||
except cv.Invalid as e:
|
||||
raise cv.Invalid(f"When using angle, {e.error_message}")
|
||||
raise cv.Invalid(f"When using angle, {e.error_message}") from e
|
||||
|
||||
|
||||
def percent_to_position(value):
|
||||
@@ -164,7 +164,7 @@ def has_valid_range_config():
|
||||
except cv.Invalid as e:
|
||||
raise cv.Invalid(
|
||||
f"The range between start and end position is invalid. It was was {range} but {e.error_message}"
|
||||
)
|
||||
) from e
|
||||
|
||||
return validator
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ def read_audio_file_and_type(file_config: ConfigType) -> tuple[bytes, MockObj]:
|
||||
raise cv.Invalid(
|
||||
f"Unable to determine audio file type of '{path}'. "
|
||||
f"Try re-encoding the file into a supported format. Details: {e}"
|
||||
)
|
||||
) from e
|
||||
|
||||
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["NONE"]
|
||||
if file_type == "wav":
|
||||
|
||||
@@ -332,8 +332,9 @@ def parse_multi_click_timing_str(value):
|
||||
try:
|
||||
state = cv.boolean(parts[0])
|
||||
except cv.Invalid:
|
||||
# pylint: disable=raise-missing-from
|
||||
raise cv.Invalid(f"First word must either be ON or OFF, not {parts[0]}")
|
||||
raise cv.Invalid(
|
||||
f"First word must either be ON or OFF, not {parts[0]}"
|
||||
) from None
|
||||
|
||||
if parts[1] != "for":
|
||||
raise cv.Invalid(f"Second word must be 'for', got {parts[1]}")
|
||||
@@ -350,7 +351,9 @@ def parse_multi_click_timing_str(value):
|
||||
try:
|
||||
length = cv.positive_time_period_milliseconds(parts[4])
|
||||
except cv.Invalid as err:
|
||||
raise cv.Invalid(f"Multi Click Grammar Parsing length failed: {err}")
|
||||
raise cv.Invalid(
|
||||
f"Multi Click Grammar Parsing length failed: {err}"
|
||||
) from err
|
||||
return {CONF_STATE: state, key: str(length)}
|
||||
|
||||
if parts[3] != "to":
|
||||
@@ -359,12 +362,16 @@ def parse_multi_click_timing_str(value):
|
||||
try:
|
||||
min_length = cv.positive_time_period_milliseconds(parts[2])
|
||||
except cv.Invalid as err:
|
||||
raise cv.Invalid(f"Multi Click Grammar Parsing minimum length failed: {err}")
|
||||
raise cv.Invalid(
|
||||
f"Multi Click Grammar Parsing minimum length failed: {err}"
|
||||
) from err
|
||||
|
||||
try:
|
||||
max_length = cv.positive_time_period_milliseconds(parts[4])
|
||||
except cv.Invalid as err:
|
||||
raise cv.Invalid(f"Multi Click Grammar Parsing minimum length failed: {err}")
|
||||
raise cv.Invalid(
|
||||
f"Multi Click Grammar Parsing maximum length failed: {err}"
|
||||
) from err
|
||||
|
||||
return {
|
||||
CONF_STATE: state,
|
||||
|
||||
@@ -65,3 +65,8 @@ async def to_code(config):
|
||||
@pins.PIN_SCHEMA_REGISTRY.register("bk72xx", PIN_SCHEMA)
|
||||
async def pin_to_code(config):
|
||||
return await libretiny.gpio.component_pin_to_code(config)
|
||||
|
||||
|
||||
# Called by writer.py; delegates to the shared libretiny implementation.
|
||||
def copy_files() -> None:
|
||||
libretiny.copy_files()
|
||||
|
||||
@@ -63,7 +63,7 @@ void BM8563::read_time() {
|
||||
rtc_time.day_of_week, rtc_time.hour, rtc_time.minute, rtc_time.second);
|
||||
|
||||
rtc_time.recalc_timestamp_utc(false);
|
||||
if (!rtc_time.is_valid()) {
|
||||
if (!rtc_time.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) {
|
||||
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ from esphome.const import CONF_ID, CONF_SAMPLE_RATE, CONF_TEMPERATURE_OFFSET, Fr
|
||||
CODEOWNERS = ["@trvrnrth"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
AUTO_LOAD = ["sensor", "text_sensor"]
|
||||
CONFLICTS_WITH = ["bme68x_bsec2"]
|
||||
MULTI_CONF = True
|
||||
|
||||
CONF_BME680_BSEC_ID = "bme680_bsec_id"
|
||||
|
||||
@@ -13,6 +13,7 @@ from esphome.const import (
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@neffs", "@kbx81"]
|
||||
CONFLICTS_WITH = ["bme680_bsec"]
|
||||
|
||||
DOMAIN = "bme68x_bsec2"
|
||||
|
||||
@@ -171,7 +172,9 @@ async def to_code_base(config):
|
||||
with open(path, encoding="utf-8") as f:
|
||||
bsec2_iaq_config = f.read()
|
||||
except Exception as e:
|
||||
raise core.EsphomeError(f"Could not open binary configuration file {path}: {e}")
|
||||
raise core.EsphomeError(
|
||||
f"Could not open binary configuration file {path}: {e}"
|
||||
) from e
|
||||
|
||||
# Convert retrieved BSEC2 config to an array of ints
|
||||
rhs = [int(x) for x in bsec2_iaq_config.split(",")]
|
||||
|
||||
@@ -44,7 +44,7 @@ void DS1307Component::read_time() {
|
||||
.year = uint16_t(ds1307_.reg.year + 10u * ds1307_.reg.year_10 + 2000),
|
||||
};
|
||||
rtc_time.recalc_timestamp_utc(false);
|
||||
if (!rtc_time.is_valid()) {
|
||||
if (!rtc_time.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) {
|
||||
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
#include "epaper_spi_ssd1683.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::epaper_spi {
|
||||
static constexpr const char *const TAG = "epaper_spi.mono";
|
||||
|
||||
void EPaperSSD1683::refresh_screen(bool partial) {
|
||||
ESP_LOGV(TAG, "Refresh screen");
|
||||
this->cmd_data(0x3C, {partial ? (uint8_t) 0x80 : (uint8_t) 0x01});
|
||||
// On partial update, set red RAM to inverse to remove BW ghosting
|
||||
this->cmd_data(0x21, {partial ? (uint8_t) 0x80 : (uint8_t) 0x40, (uint8_t) 0x00});
|
||||
// Set full update to 0xD7 for fast update, 0xF7 for normal
|
||||
// Fast update flashes less and draws sooner but is in busy state for the same amount of time
|
||||
// Manufacturer recommends not using fast update all the time, TODO expose this to the user
|
||||
this->cmd_data(0x22, {partial ? (uint8_t) 0xFC : (uint8_t) 0xF7});
|
||||
this->command(0x20);
|
||||
}
|
||||
|
||||
// Puts the display into deep sleep mode 1, only way to get out is to reset the display
|
||||
// Mode 1 retains RAM while sleeping, necessary for future partial and window updates
|
||||
void EPaperSSD1683::deep_sleep() {
|
||||
if (this->is_using_partial_update_()) {
|
||||
ESP_LOGV(TAG, "Deep sleep mode 1");
|
||||
this->cmd_data(0x10, {0x01}); // deep sleep, retain RAM
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Deep sleep mode 2");
|
||||
this->cmd_data(0x10, {0x03}); // deep sleep, lose RAM
|
||||
}
|
||||
}
|
||||
|
||||
void EPaperSSD1683::set_window() {
|
||||
// if not using partial update, the display will go into deep sleep mode 2, so must rewrite entire
|
||||
// buffer since the display RAM will not retain contents
|
||||
if (!this->is_using_partial_update_()) {
|
||||
this->x_low_ = 0;
|
||||
this->x_high_ = this->width_;
|
||||
this->y_low_ = 0;
|
||||
this->y_high_ = this->height_;
|
||||
}
|
||||
|
||||
// round x-coordinates to byte boundaries
|
||||
this->x_low_ /= 8;
|
||||
this->x_high_ += 7;
|
||||
this->x_high_ /= 8;
|
||||
|
||||
this->cmd_data(0x44, {(uint8_t) this->x_low_, (uint8_t) (this->x_high_ - 1)});
|
||||
this->cmd_data(0x45, {(uint8_t) this->y_low_, (uint8_t) (this->y_low_ / 256), (uint8_t) (this->y_high_ - 1),
|
||||
(uint8_t) ((this->y_high_ - 1) / 256)});
|
||||
this->cmd_data(0x4E, {(uint8_t) this->x_low_});
|
||||
this->cmd_data(0x4F, {(uint8_t) this->y_low_, (uint8_t) (this->y_low_ / 256)});
|
||||
}
|
||||
|
||||
bool HOT EPaperSSD1683::transfer_data() {
|
||||
auto start_time = millis();
|
||||
if (this->current_data_index_ == 0) {
|
||||
if (this->send_red_) {
|
||||
// round to byte boundaries
|
||||
this->set_window();
|
||||
}
|
||||
// for monochrome, we need to send red on every refresh to prevent dirty pixels
|
||||
// when doing a partial refresh
|
||||
this->command(this->send_red_ ? 0x26 : 0x24);
|
||||
this->current_data_index_ = this->y_low_; // actually current line
|
||||
}
|
||||
size_t row_length = this->x_high_ - this->x_low_;
|
||||
FixedVector<uint8_t> bytes_to_send{};
|
||||
bytes_to_send.init(row_length);
|
||||
ESP_LOGV(TAG, "Writing %u bytes at line %zu at %ums", row_length, this->current_data_index_, (unsigned) millis());
|
||||
this->start_data_();
|
||||
while (this->current_data_index_ != this->y_high_) {
|
||||
size_t data_idx = this->current_data_index_ * this->row_width_ + this->x_low_;
|
||||
for (size_t i = 0; i != row_length; i++) {
|
||||
bytes_to_send[i] = this->buffer_[data_idx++];
|
||||
}
|
||||
++this->current_data_index_;
|
||||
this->write_array(&bytes_to_send.front(), row_length); // NOLINT
|
||||
if (millis() - start_time > MAX_TRANSFER_TIME) {
|
||||
// Let the main loop run and come back next loop
|
||||
this->disable();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
this->disable();
|
||||
this->current_data_index_ = 0;
|
||||
if (this->send_red_) {
|
||||
this->send_red_ = false;
|
||||
return false;
|
||||
}
|
||||
this->send_red_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace esphome::epaper_spi
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include "epaper_spi_mono.h"
|
||||
|
||||
namespace esphome::epaper_spi {
|
||||
/**
|
||||
* A class for Solomon SSD1683 epaper displays.
|
||||
*/
|
||||
class EPaperSSD1683 : public EPaperMono {
|
||||
public:
|
||||
EPaperSSD1683(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
|
||||
size_t init_sequence_length)
|
||||
: EPaperMono(name, width, height, init_sequence, init_sequence_length) {}
|
||||
|
||||
protected:
|
||||
void refresh_screen(bool partial) override;
|
||||
void deep_sleep() override;
|
||||
void set_window() override;
|
||||
bool transfer_data() override;
|
||||
};
|
||||
|
||||
} // namespace esphome::epaper_spi
|
||||
@@ -0,0 +1,27 @@
|
||||
from esphome.const import CONF_DATA_RATE
|
||||
|
||||
from . import EpaperModel
|
||||
|
||||
|
||||
class SSD1683(EpaperModel):
|
||||
def __init__(self, name, class_name="EPaperSSD1683", data_rate="20MHz", **defaults):
|
||||
defaults[CONF_DATA_RATE] = data_rate
|
||||
super().__init__(name, class_name, **defaults)
|
||||
|
||||
# fmt: off
|
||||
def get_init_sequence(self, config: dict):
|
||||
_width, height = self.get_dimensions(config)
|
||||
return (
|
||||
(0x01, (height - 1) % 256, (height - 1) // 256, 0x00), # Set column gate limit
|
||||
(0x18, 0x80), # Select internal Temp sensor
|
||||
(0x11, 0x03), # Set transform
|
||||
)
|
||||
|
||||
|
||||
ssd1683 = SSD1683("ssd1683")
|
||||
|
||||
goodisplay_gdey042t81 = ssd1683.extend(
|
||||
"goodisplay-gdey042t81-4.2",
|
||||
width=400,
|
||||
height=300,
|
||||
)
|
||||
@@ -1222,7 +1222,7 @@ FRAMEWORK_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_IGNORE_EFUSE_CUSTOM_MAC, default=False): cv.boolean,
|
||||
cv.Optional(CONF_IGNORE_EFUSE_MAC_CRC, default=False): cv.boolean,
|
||||
cv.Optional(CONF_MINIMUM_CHIP_REVISION): cv.one_of(
|
||||
*ESP32_CHIP_REVISIONS
|
||||
*ESP32_CHIP_REVISIONS, string=True
|
||||
),
|
||||
cv.Optional(CONF_SRAM1_AS_IRAM, default=False): cv.boolean,
|
||||
# DHCP server is needed for WiFi AP mode. When WiFi component is used,
|
||||
|
||||
@@ -172,10 +172,16 @@ def validate_gpio_pin(pin):
|
||||
exc,
|
||||
)
|
||||
else:
|
||||
# Throw an exception if used for a pin that would not have resulted
|
||||
# in a validation error anyway!
|
||||
# `ignore_pin_validation_error` only suppresses an error raised by the
|
||||
# variant's pin_validation above (e.g. SPI flash/PSRAM pins, invalid pin
|
||||
# numbers). If that didn't raise, the option is a no-op -- warn so the
|
||||
# user can clean it up, but don't block the build.
|
||||
if ignore_pin_validation_warning:
|
||||
raise cv.Invalid(f"GPIO{pin[CONF_NUMBER]} is not a reserved pin")
|
||||
_LOGGER.warning(
|
||||
"GPIO%d has no validation errors to ignore; "
|
||||
"remove `ignore_pin_validation_error: true` from this pin.",
|
||||
pin[CONF_NUMBER],
|
||||
)
|
||||
|
||||
return pin
|
||||
|
||||
|
||||
@@ -221,7 +221,7 @@ class EthernetComponent final : public Component {
|
||||
int reset_pin_{-1};
|
||||
int phy_addr_spi_{-1};
|
||||
int clock_speed_;
|
||||
spi_host_device_t interface_{SPI3_HOST};
|
||||
spi_host_device_t interface_{SPI2_HOST};
|
||||
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
|
||||
uint32_t polling_interval_{0};
|
||||
#endif
|
||||
|
||||
@@ -325,7 +325,7 @@ def download_gfont(value):
|
||||
raise cv.Invalid(
|
||||
f"Could not download font at {url}, please check the fonts exists "
|
||||
f"at google fonts ({e})"
|
||||
)
|
||||
) from e
|
||||
match = re.search(r"src:\s+url\((.+)\)\s+format\('truetype'\);", req.text)
|
||||
if match is None:
|
||||
raise cv.Invalid(
|
||||
|
||||
@@ -60,20 +60,35 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = await binary_sensor.new_binary_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
def _pin_shared_only_with_deep_sleep(pin_num: int) -> bool:
|
||||
"""Check if pin is shared exclusively with deep_sleep (wakeup pin)."""
|
||||
pin_key = (CORE.target_platform, CORE.target_platform, pin_num)
|
||||
pin_users = pins.PIN_SCHEMA_REGISTRY.pins_used.get(pin_key, [])
|
||||
if len(pin_users) != 2:
|
||||
return False
|
||||
return any(path and path[0] == "deep_sleep" for path, _, _ in pin_users)
|
||||
|
||||
pin = await cg.gpio_pin_expression(config[CONF_PIN])
|
||||
cg.add(var.set_pin(pin))
|
||||
|
||||
# Check for ESP8266 GPIO16 interrupt limitation
|
||||
# GPIO16 on ESP8266 is a special pin that doesn't support interrupts through
|
||||
# the Arduino attachInterrupt() function. This is the only known GPIO pin
|
||||
# across all supported platforms that has this limitation, so we handle it
|
||||
# here instead of in the platform-specific code.
|
||||
def _final_validate(config):
|
||||
use_interrupt = config[CONF_USE_INTERRUPT]
|
||||
if use_interrupt and CORE.is_esp8266 and config[CONF_PIN][CONF_NUMBER] == 16:
|
||||
if not use_interrupt:
|
||||
return config
|
||||
|
||||
pin_num = config[CONF_PIN][CONF_NUMBER]
|
||||
|
||||
# Expander pins (e.g. PCF8574, MCP23017) don't support direct interrupt
|
||||
# attachment — only internal/native GPIO pins do.
|
||||
if pins.PIN_SCHEMA_REGISTRY.get_key(config[CONF_PIN]) != CORE.target_platform:
|
||||
_LOGGER.info(
|
||||
"GPIO binary_sensor '%s': Pin is not an internal GPIO, "
|
||||
"falling back to polling mode.",
|
||||
config.get(CONF_NAME, config[CONF_ID]),
|
||||
)
|
||||
config[CONF_USE_INTERRUPT] = False
|
||||
return config
|
||||
|
||||
# GPIO16 on ESP8266 doesn't support interrupts through attachInterrupt().
|
||||
if CORE.is_esp8266 and pin_num == 16:
|
||||
_LOGGER.warning(
|
||||
"GPIO binary_sensor '%s': GPIO16 on ESP8266 doesn't support interrupts. "
|
||||
"Falling back to polling mode (same as in ESPHome <2025.7). "
|
||||
@@ -81,22 +96,45 @@ async def to_code(config):
|
||||
"performance with interrupts.",
|
||||
config.get(CONF_NAME, config[CONF_ID]),
|
||||
)
|
||||
use_interrupt = False
|
||||
config[CONF_USE_INTERRUPT] = False
|
||||
return config
|
||||
|
||||
# Check if pin is shared with other components (allow_other_uses)
|
||||
# When a pin is shared, interrupts can interfere with other components
|
||||
# (e.g., duty_cycle sensor) that need to monitor the pin's state changes
|
||||
if use_interrupt and config[CONF_PIN].get(CONF_ALLOW_OTHER_USES, False):
|
||||
_LOGGER.info(
|
||||
"GPIO binary_sensor '%s': Disabling interrupts because pin %s is shared with other components. "
|
||||
"The sensor will use polling mode for compatibility with other pin uses.",
|
||||
config.get(CONF_NAME, config[CONF_ID]),
|
||||
config[CONF_PIN][CONF_NUMBER],
|
||||
)
|
||||
use_interrupt = False
|
||||
# (e.g., duty_cycle sensor) that need to monitor the pin's state changes.
|
||||
# Exception: deep_sleep wakeup pins are compatible with interrupts when
|
||||
# the pin is only shared between this sensor and deep_sleep (count == 2).
|
||||
if config[CONF_PIN].get(CONF_ALLOW_OTHER_USES, False):
|
||||
if not _pin_shared_only_with_deep_sleep(pin_num):
|
||||
_LOGGER.info(
|
||||
"GPIO binary_sensor '%s': Disabling interrupts because pin %s is shared "
|
||||
"with other components. The sensor will use polling mode for "
|
||||
"compatibility with other pin uses.",
|
||||
config.get(CONF_NAME, config[CONF_ID]),
|
||||
pin_num,
|
||||
)
|
||||
config[CONF_USE_INTERRUPT] = False
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"GPIO binary_sensor '%s': Pin %s is shared with deep_sleep, "
|
||||
"keeping interrupts enabled.",
|
||||
config.get(CONF_NAME, config[CONF_ID]),
|
||||
pin_num,
|
||||
)
|
||||
|
||||
if use_interrupt:
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = await binary_sensor.new_binary_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
pin = await cg.gpio_pin_expression(config[CONF_PIN])
|
||||
cg.add(var.set_pin(pin))
|
||||
|
||||
if config[CONF_USE_INTERRUPT]:
|
||||
cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE]))
|
||||
else:
|
||||
# Only generate call when disabling interrupts (default is true)
|
||||
cg.add(var.set_use_interrupt(use_interrupt))
|
||||
cg.add(var.set_use_interrupt(False))
|
||||
|
||||
@@ -46,11 +46,6 @@ void GPIOBinarySensorStore::setup(InternalGPIOPin *pin, Component *component) {
|
||||
}
|
||||
|
||||
void GPIOBinarySensor::setup() {
|
||||
if (this->store_.use_interrupt_ && !this->pin_->is_internal()) {
|
||||
ESP_LOGD(TAG, "GPIO is not internal, falling back to polling mode");
|
||||
this->store_.use_interrupt_ = false;
|
||||
}
|
||||
|
||||
if (this->store_.use_interrupt_) {
|
||||
auto *internal_pin = static_cast<InternalGPIOPin *>(this->pin_);
|
||||
this->store_.setup(internal_pin, this);
|
||||
|
||||
@@ -22,7 +22,7 @@ void HttpRequestComponent::dump_config() {
|
||||
}
|
||||
|
||||
std::string HttpContainer::get_response_header(const std::string &header_name) {
|
||||
auto lower = str_lower_case(header_name);
|
||||
auto lower = str_lower_case(header_name); // NOLINT
|
||||
for (const auto &entry : this->response_headers_) {
|
||||
if (entry.name == lower) {
|
||||
ESP_LOGD(TAG, "Header with name %s found with value %s", lower.c_str(), entry.value.c_str());
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/alloc_helpers.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
@@ -400,7 +401,7 @@ class HttpRequestComponent : public Component {
|
||||
std::vector<std::string> lower;
|
||||
lower.reserve(collect_headers.size());
|
||||
for (const auto &h : collect_headers) {
|
||||
lower.push_back(str_lower_case(h));
|
||||
lower.push_back(str_lower_case(h)); // NOLINT
|
||||
}
|
||||
return this->perform(url, method, body, request_headers, lower);
|
||||
}
|
||||
@@ -415,7 +416,7 @@ class HttpRequestComponent : public Component {
|
||||
std::vector<std::string> lower;
|
||||
lower.reserve(collect_headers.size());
|
||||
for (const auto &h : collect_headers) {
|
||||
lower.push_back(str_lower_case(h));
|
||||
lower.push_back(str_lower_case(h)); // NOLINT
|
||||
}
|
||||
return this->perform(url, method, body, std::vector<Header>(request_headers.begin(), request_headers.end()), lower);
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
|
||||
container->response_headers_.clear();
|
||||
auto header_count = container->client_.headers();
|
||||
for (int i = 0; i < header_count; i++) {
|
||||
const std::string header_name = str_lower_case(container->client_.headerName(i).c_str());
|
||||
const std::string header_name = str_lower_case(container->client_.headerName(i).c_str()); // NOLINT
|
||||
if (should_collect_header(lower_case_collect_headers, header_name)) {
|
||||
std::string header_value = container->client_.header(i).c_str();
|
||||
ESP_LOGD(TAG, "Received response header, name: %s, value: %s", header_name.c_str(), header_value.c_str());
|
||||
|
||||
@@ -115,7 +115,7 @@ std::shared_ptr<HttpContainer> HttpRequestHost::perform(const std::string &url,
|
||||
container->content_length = container->response_body_.size();
|
||||
for (auto header : response.headers) {
|
||||
ESP_LOGD(TAG, "Header: %s: %s", header.first.c_str(), header.second.c_str());
|
||||
auto lower_name = str_lower_case(header.first);
|
||||
auto lower_name = str_lower_case(header.first); // NOLINT
|
||||
if (should_collect_header(lower_case_collect_headers, lower_name)) {
|
||||
container->response_headers_.push_back({lower_name, header.second});
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) {
|
||||
|
||||
switch (evt->event_id) {
|
||||
case HTTP_EVENT_ON_HEADER: {
|
||||
const std::string header_name = str_lower_case(evt->header_key);
|
||||
const std::string header_name = str_lower_case(evt->header_key); // NOLINT
|
||||
if (should_collect_header(user_data->lower_case_collect_headers, header_name)) {
|
||||
const std::string header_value = evt->header_value;
|
||||
ESP_LOGD(TAG, "Received response header, name: %s, value: %s", header_name.c_str(), header_value.c_str());
|
||||
|
||||
@@ -283,7 +283,7 @@ async def to_code(config):
|
||||
try:
|
||||
return Image.open(path)
|
||||
except Exception as e:
|
||||
raise core.EsphomeError(f"Could not load image file {path}: {e}")
|
||||
raise core.EsphomeError(f"Could not load image file {path}: {e}") from e
|
||||
|
||||
# make a wide horizontal combined image.
|
||||
images = [load_image(x) for x in config[CONF_COLOR_PALETTE_IMAGES]]
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome {
|
||||
namespace ili9xxx {
|
||||
|
||||
|
||||
@@ -229,6 +229,10 @@ void ILI9XXXDisplay::update() {
|
||||
}
|
||||
|
||||
void ILI9XXXDisplay::display_() {
|
||||
// buffer may be null if allocation failed
|
||||
if (this->buffer_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
// check if something was displayed
|
||||
if ((this->x_high_ < this->x_low_) || (this->y_high_ < this->y_low_)) {
|
||||
return;
|
||||
|
||||
@@ -28,7 +28,6 @@ from esphome.const import (
|
||||
CONF_URL,
|
||||
)
|
||||
from esphome.core import CORE, HexInt
|
||||
from esphome.final_validate import full_config
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -676,12 +675,16 @@ def _final_validate(config):
|
||||
:param config:
|
||||
:return:
|
||||
"""
|
||||
fv = full_config.get()
|
||||
if "lvgl" in fv and not all(CONF_BYTE_ORDER in x for x in config):
|
||||
config = config.copy()
|
||||
for c in config:
|
||||
if not c.get(CONF_BYTE_ORDER):
|
||||
c[CONF_BYTE_ORDER] = "LITTLE_ENDIAN"
|
||||
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
|
||||
|
||||
|
||||
@@ -753,7 +756,7 @@ async def write_image(config, all_frames=False):
|
||||
for col in range(width):
|
||||
encoder.encode(pixels[row * width + col])
|
||||
encoder.end_row()
|
||||
encoder.end_image()
|
||||
encoder.end_image()
|
||||
|
||||
rhs = [HexInt(x) for x in encoder.data]
|
||||
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
|
||||
|
||||
@@ -189,7 +189,7 @@ Color Image::get_rgb_pixel_(int x, int y) const {
|
||||
}
|
||||
Color Image::get_rgb565_pixel_(int x, int y) const {
|
||||
const uint8_t *pos = this->data_start_ + (x + y * this->width_) * this->bpp_ / 8;
|
||||
uint16_t rgb565 = encode_uint16(progmem_read_byte(pos), progmem_read_byte(pos + 1));
|
||||
uint16_t rgb565 = encode_uint16(progmem_read_byte(pos + 1), progmem_read_byte(pos));
|
||||
auto r = (rgb565 & 0xF800) >> 11;
|
||||
auto g = (rgb565 & 0x07E0) >> 5;
|
||||
auto b = rgb565 & 0x001F;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
@@ -24,6 +25,7 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.core.config import BOARD_MAX_LENGTH
|
||||
from esphome.helpers import copy_file_if_changed
|
||||
from esphome.storage_json import StorageJSON
|
||||
|
||||
from . import gpio # noqa
|
||||
@@ -465,6 +467,11 @@ async def component_to_code(config):
|
||||
# it for project source files only. GCC uses the last -O flag.
|
||||
build_src_flags += " -Os"
|
||||
cg.add_platformio_option("build_src_flags", build_src_flags)
|
||||
# IRAM_ATTR is a no-op on BK72xx (SDK masks FIQ+IRQ around flash ops).
|
||||
# On other families, patch_linker.py routes .sram.text into the right
|
||||
# RAM-executable output section and prints a post-link placement summary.
|
||||
if FAMILY_COMPONENT[config[CONF_FAMILY]] != COMPONENT_BK72XX:
|
||||
cg.add_platformio_option("extra_scripts", ["pre:patch_linker.py"])
|
||||
# dummy version code
|
||||
cg.add_define("USE_ARDUINO_VERSION_CODE", cg.RawExpression("VERSION_CODE(0, 0, 0)"))
|
||||
# decrease web server stack size (16k words -> 4k words)
|
||||
@@ -549,3 +556,13 @@ async def component_to_code(config):
|
||||
_configure_lwip(config)
|
||||
|
||||
await cg.register_component(var, config)
|
||||
|
||||
|
||||
# Called by writer.py
|
||||
def copy_files() -> None:
|
||||
script_dir = Path(__file__).parent
|
||||
patch_linker_file = script_dir / "patch_linker.py.script"
|
||||
copy_file_if_changed(
|
||||
patch_linker_file,
|
||||
CORE.relative_build_path("patch_linker.py"),
|
||||
)
|
||||
|
||||
@@ -79,6 +79,11 @@ async def to_code(config):
|
||||
@pins.PIN_SCHEMA_REGISTRY.register("{COMPONENT_LOWER}", PIN_SCHEMA)
|
||||
async def pin_to_code(config):
|
||||
return await libretiny.gpio.component_pin_to_code(config)
|
||||
|
||||
|
||||
# Called by writer.py; delegates to the shared libretiny implementation.
|
||||
def copy_files() -> None:
|
||||
libretiny.copy_files()
|
||||
'''
|
||||
|
||||
BASE_CODE_BOARDS = '''
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
# pylint: disable=E0602
|
||||
Import("env") # noqa
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
# ESPHome marks ISR code IRAM_ATTR, which on LibreTiny maps to a per-family
|
||||
# section routed into RAM-executable memory (see esphome/core/hal.h).
|
||||
#
|
||||
# This script is NOT loaded on BK72xx (IRAM_ATTR is a no-op there; the SDK
|
||||
# masks FIQ+IRQ around flash writes). On the remaining families:
|
||||
# - RTL8710B: hal.h uses section(".image2.ram.text"); stock linker consumes it.
|
||||
# - RTL8720C: hal.h uses section(".sram.text"); stock linker consumes it.
|
||||
# - LN882H: stock linker has no glob for ".sram.text", so we inject
|
||||
# KEEP(*(.sram.text*)) into ".flash_copysection" (> RAM0 AT> FLASH).
|
||||
#
|
||||
# All families also get a post-link summary showing where IRAM_ATTR landed.
|
||||
|
||||
|
||||
_MARKER = "/* esphome .sram.text */"
|
||||
# Strong assignments (not PROVIDE) so the symbols are always emitted in the
|
||||
# ELF; PROVIDE symbols with no references can be garbage-collected.
|
||||
_KEEP_LINE = (
|
||||
" __esphome_sram_text_start = .; "
|
||||
"KEEP(*(.sram.text*)) "
|
||||
"__esphome_sram_text_end = .; "
|
||||
+ _MARKER + "\n"
|
||||
)
|
||||
_LN_COPY = re.compile(r"(\.flash_copysection\s*:\s*\{\s*\n)")
|
||||
|
||||
|
||||
def _detect(env):
|
||||
prefix = "USE_LIBRETINY_VARIANT_"
|
||||
# CPPDEFINES may hold strings or (name, value) tuples; BUILD_FLAGS holds
|
||||
# the raw "-DNAME" strings. PlatformIO populates both, but the exact order
|
||||
# vs. extra_scripts varies, so check both to be robust.
|
||||
for token in env.get("CPPDEFINES", []):
|
||||
if isinstance(token, (list, tuple)):
|
||||
token = token[0]
|
||||
if isinstance(token, str) and token.startswith(prefix):
|
||||
return token[len(prefix):]
|
||||
for flag in env.get("BUILD_FLAGS", []):
|
||||
if isinstance(flag, str) and "-D" + prefix in flag:
|
||||
name = flag.split("-D", 1)[1].split("=", 1)[0].strip()
|
||||
if name.startswith(prefix):
|
||||
return name[len(prefix):]
|
||||
return None
|
||||
|
||||
|
||||
KNOWN_VARIANTS = frozenset({
|
||||
"LN882H",
|
||||
"RTL8710B",
|
||||
"RTL8720C",
|
||||
})
|
||||
|
||||
|
||||
def _inject_keep(host_section):
|
||||
"""Return a patcher that injects _KEEP_LINE at the top of `host_section`."""
|
||||
def patch(content):
|
||||
if _MARKER in content:
|
||||
return content
|
||||
return host_section.sub(r"\1" + _KEEP_LINE, content, count=1)
|
||||
return patch
|
||||
|
||||
|
||||
# Variants not listed here intentionally have no .ld patcher:
|
||||
# - RTL8710B: hal.h uses section(".image2.ram.text") which the stock linker
|
||||
# already routes into .ram_image2.text (> BD_RAM).
|
||||
# - RTL8720C: stock linker already consumes *(.sram.text*).
|
||||
# - BK72xx (all): SDK masks FIQ+IRQ around flash writes, IRAM_ATTR is no-op.
|
||||
_PATCHERS_BY_VARIANT = {
|
||||
"LN882H": (_inject_keep(_LN_COPY),),
|
||||
}
|
||||
|
||||
|
||||
def _patchers_for(variant):
|
||||
return _PATCHERS_BY_VARIANT.get(variant, ())
|
||||
|
||||
|
||||
def _pre_link(target, source, env):
|
||||
build_dir = env.subst("$BUILD_DIR")
|
||||
ld_files = [f for f in os.listdir(build_dir) if f.endswith(".ld")]
|
||||
patched = 0
|
||||
for name in ld_files:
|
||||
path = os.path.join(build_dir, name)
|
||||
with open(path, "r", encoding="utf-8") as fh:
|
||||
original = fh.read()
|
||||
if _MARKER in original:
|
||||
patched += 1
|
||||
continue
|
||||
content = original
|
||||
for fn in _patchers:
|
||||
content = fn(content)
|
||||
if content != original:
|
||||
with open(path, "w", encoding="utf-8") as fh:
|
||||
fh.write(content)
|
||||
print("ESPHome: patched {} for IRAM_ATTR placement".format(name))
|
||||
patched += 1
|
||||
if not patched:
|
||||
raise RuntimeError(
|
||||
"ESPHome: no .ld in {} was patched for IRAM_ATTR. Update the "
|
||||
"regex in patch_linker.py.script (_PATCHERS_BY_VARIANT).".format(
|
||||
build_dir
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Substrings matched against demangled names as a fallback on RTL8720C,
|
||||
# where we cannot inject __esphome_sram_text_start/end markers.
|
||||
_FALLBACK_SUBSTRINGS = ("wake_loop_any_context", "wake_loop_isrsafe",
|
||||
"enable_loop_soon_any_context")
|
||||
|
||||
|
||||
def _post_link(target, source, env):
|
||||
"""Print where IRAM_ATTR ended up so users can confirm at a glance."""
|
||||
elf = env.subst("$BUILD_DIR/${PROGNAME}.elf")
|
||||
if not os.path.isfile(elf):
|
||||
return
|
||||
nm = env.subst("$NM")
|
||||
try:
|
||||
out = subprocess.check_output(
|
||||
[nm, "--defined-only", "--demangle", elf], text=True
|
||||
)
|
||||
except (OSError, subprocess.CalledProcessError) as exc:
|
||||
print("ESPHome: IRAM_ATTR summary unavailable (nm failed: {})".format(exc))
|
||||
return
|
||||
start = end = None
|
||||
fallback = []
|
||||
for line in out.splitlines():
|
||||
parts = line.split(maxsplit=2)
|
||||
if len(parts) != 3:
|
||||
continue
|
||||
addr_str, _kind, name = parts
|
||||
if name == "__esphome_sram_text_start":
|
||||
start = int(addr_str, 16)
|
||||
elif name == "__esphome_sram_text_end":
|
||||
end = int(addr_str, 16)
|
||||
elif "veneer" not in name and any(s in name for s in _FALLBACK_SUBSTRINGS):
|
||||
fallback.append(int(addr_str, 16))
|
||||
print("ESPHome: IRAM_ATTR placement summary ({}):".format(_variant))
|
||||
if start is not None and end is not None:
|
||||
print(" .sram.text: {} bytes at 0x{:08x} - 0x{:08x}".format(end - start, start, end))
|
||||
elif fallback:
|
||||
lo, hi = min(fallback), max(fallback)
|
||||
print(" IRAM symbols at 0x{:08x} - 0x{:08x} (approx {} bytes)".format(lo, hi, hi - lo))
|
||||
else:
|
||||
print(" no IRAM_ATTR symbols found")
|
||||
|
||||
|
||||
if (_variant := _detect(env)) is None:
|
||||
raise RuntimeError(
|
||||
"ESPHome: could not determine LibreTiny variant from build flags. "
|
||||
"patch_linker.py needs USE_LIBRETINY_VARIANT_* to route IRAM_ATTR "
|
||||
"into SRAM; without it, ISR handlers would silently end up in flash."
|
||||
)
|
||||
if _variant not in KNOWN_VARIANTS:
|
||||
raise RuntimeError(
|
||||
"ESPHome: unknown LibreTiny variant {!r}; patch_linker.py does not "
|
||||
"know how to route IRAM_ATTR into SRAM for this family. Update "
|
||||
"patch_linker.py.script before shipping firmware.".format(_variant)
|
||||
)
|
||||
|
||||
if _patchers := _patchers_for(_variant):
|
||||
# LibreTiny writes the processed .ld templates into $BUILD_DIR during its
|
||||
# own builder setup, which may run after this script. Register the patch
|
||||
# as a pre-link action so it executes once the linker scripts exist.
|
||||
env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", _pre_link)
|
||||
|
||||
# Post-link summary for every family that reaches this script.
|
||||
env.AddPostAction("$BUILD_DIR/${PROGNAME}.elf", _post_link)
|
||||
@@ -222,7 +222,7 @@ class LightCall {
|
||||
inline bool get_save_() { return (this->flags_ & FLAG_SAVE) != 0; }
|
||||
|
||||
// Helper to set flag - defaults to true for common case
|
||||
void set_flag_(FieldFlags flag, bool value = true) {
|
||||
void set_flag_(FieldFlags flag, bool value = true) ESPHOME_ALWAYS_INLINE {
|
||||
if (value) {
|
||||
this->flags_ |= flag;
|
||||
} else {
|
||||
@@ -231,7 +231,7 @@ class LightCall {
|
||||
}
|
||||
|
||||
// Helper to clear flag - reduces code size for common case
|
||||
void clear_flag_(FieldFlags flag) { this->flags_ &= ~flag; }
|
||||
void clear_flag_(FieldFlags flag) ESPHOME_ALWAYS_INLINE { this->flags_ &= ~flag; }
|
||||
|
||||
// Helper to log unsupported feature and clear flag - reduces code duplication
|
||||
void log_and_clear_unsupported_(FieldFlags flag, const LogString *feature, bool use_color_mode_log);
|
||||
|
||||
@@ -65,3 +65,8 @@ async def to_code(config):
|
||||
@pins.PIN_SCHEMA_REGISTRY.register("ln882x", PIN_SCHEMA)
|
||||
async def pin_to_code(config):
|
||||
return await libretiny.gpio.component_pin_to_code(config)
|
||||
|
||||
|
||||
# Called by writer.py; delegates to the shared libretiny implementation.
|
||||
def copy_files() -> None:
|
||||
libretiny.copy_files()
|
||||
|
||||
@@ -45,6 +45,7 @@ optional<uint32_t> LTR390Component::read_sensor_data_(LTR390MODE mode) {
|
||||
uint8_t buffer[num_bytes];
|
||||
|
||||
// Wait until data available
|
||||
constexpr uint32_t max_wait_ms = 25;
|
||||
const uint32_t now = millis();
|
||||
while (true) {
|
||||
std::bitset<8> status = this->reg(LTR390_MAIN_STATUS).get();
|
||||
@@ -52,12 +53,12 @@ optional<uint32_t> LTR390Component::read_sensor_data_(LTR390MODE mode) {
|
||||
if (available)
|
||||
break;
|
||||
|
||||
if (millis() - now > 100) {
|
||||
if (millis() - now > max_wait_ms) {
|
||||
ESP_LOGW(TAG, "Sensor didn't return any data, aborting");
|
||||
return {};
|
||||
}
|
||||
ESP_LOGD(TAG, "Waiting for data");
|
||||
delay(2);
|
||||
ESP_LOGV(TAG, "Waiting for data");
|
||||
delay(1);
|
||||
}
|
||||
|
||||
if (!this->read_bytes(MODEADDRESSES[mode], buffer, num_bytes)) {
|
||||
|
||||
@@ -44,6 +44,7 @@ from esphome.core import CORE, ID, Lambda
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.final_validate import full_config
|
||||
from esphome.helpers import write_file_if_changed
|
||||
from esphome.writer import clean_build
|
||||
from esphome.yaml_util import load_yaml
|
||||
|
||||
from . import defines as df, helpers, lv_validation as lvalid, widgets
|
||||
@@ -451,7 +452,8 @@ async def to_code(configs):
|
||||
df.add_define(f"LV_DRAW_SW_SUPPORT_{fmt}", "1")
|
||||
|
||||
lv_conf_h_file = CORE.relative_src_path(LV_CONF_FILENAME)
|
||||
write_file_if_changed(lv_conf_h_file, generate_lv_conf_h())
|
||||
if write_file_if_changed(lv_conf_h_file, generate_lv_conf_h()):
|
||||
clean_build(clear_pio_cache=False)
|
||||
cg.add_build_flag("-DLV_CONF_H=1")
|
||||
# handle windows paths in a way that doesn't break the generated C++
|
||||
lv_conf_h_path = Path(lv_conf_h_file).as_posix()
|
||||
|
||||
@@ -89,10 +89,12 @@
|
||||
id: hello_world_label_
|
||||
text: "Hello World!"
|
||||
align: center
|
||||
- obj:
|
||||
- container:
|
||||
id: hello_world_qrcode_
|
||||
outline_width: 0
|
||||
border_width: 0
|
||||
height: 100
|
||||
width: 100
|
||||
hidden: !lambda |-
|
||||
return lv_obj_get_width(lv_screen_active()) < 300 && lv_obj_get_height(lv_screen_active()) < 400;
|
||||
widgets:
|
||||
|
||||
@@ -642,26 +642,28 @@ void LvglComponent::write_random_() {
|
||||
int iterations = 6 - lv_display_get_inactive_time(this->disp_) / 60000;
|
||||
if (iterations <= 0)
|
||||
iterations = 1;
|
||||
int16_t width = lv_display_get_horizontal_resolution(this->disp_);
|
||||
int16_t height = lv_display_get_vertical_resolution(this->disp_);
|
||||
while (iterations-- != 0) {
|
||||
int32_t col = random_uint32() % this->width_;
|
||||
int32_t col = random_uint32() % width;
|
||||
col = col / this->draw_rounding * this->draw_rounding;
|
||||
int32_t row = random_uint32() % this->height_;
|
||||
int32_t row = random_uint32() % height;
|
||||
row = row / this->draw_rounding * this->draw_rounding;
|
||||
// size will be between 8 and 32, and a multiple of draw_rounding
|
||||
int32_t size = (random_uint32() % 25 + 8) / this->draw_rounding * this->draw_rounding;
|
||||
lv_area_t area{col, row, col + size - 1, row + size - 1};
|
||||
lv_area_t area{.x1 = col, .y1 = row, .x2 = col + size - 1, .y2 = row + size - 1};
|
||||
// clip to display bounds just in case
|
||||
if (area.x2 >= this->width_)
|
||||
area.x2 = this->width_ - 1;
|
||||
if (area.y2 >= this->height_)
|
||||
area.y2 = this->height_ - 1;
|
||||
if (area.x2 >= width)
|
||||
area.x2 = width - 1;
|
||||
if (area.y2 >= height)
|
||||
area.y2 = height - 1;
|
||||
|
||||
// line_len can't exceed 1024, and minimum buffer size is 2048, so this won't overflow the buffer
|
||||
size_t line_len = lv_area_get_width(&area) * lv_area_get_height(&area) / 2;
|
||||
for (size_t i = 0; i != line_len; i++) {
|
||||
((uint32_t *) (this->draw_buf_))[i] = random_uint32();
|
||||
reinterpret_cast<uint32_t *>(this->draw_buf_)[i] = random_uint32();
|
||||
}
|
||||
this->draw_buffer_(&area, (lv_color_data *) this->draw_buf_);
|
||||
this->draw_buffer_(&area, reinterpret_cast<lv_color_data *>(this->draw_buf_));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -76,16 +76,23 @@ inline void lv_style_set_text_font(lv_style_t *style, const font::Font *font) {
|
||||
}
|
||||
#endif
|
||||
#if defined(USE_LVGL_IMAGE) && defined(USE_IMAGE)
|
||||
// Shortcut / overload, so that the source of an image can easily be updated
|
||||
// from within a lambda.
|
||||
inline void lv_image_set_src(lv_obj_t *obj, image::Image *image) { lv_image_set_src(obj, image->get_lv_image_dsc()); }
|
||||
#if LV_USE_IMAGE
|
||||
// Shortcut / overload, so that the source of an image widget can easily be updated from within a lambda.
|
||||
inline void lv_image_set_src(lv_obj_t *obj, image::Image *image) { ::lv_image_set_src(obj, image->get_lv_image_dsc()); }
|
||||
#endif // LV_USE_IMAGE
|
||||
|
||||
inline void lv_obj_set_style_bitmap_mask_src(lv_obj_t *obj, image::Image *image, lv_style_selector_t selector) {
|
||||
lv_obj_set_style_bitmap_mask_src(obj, image->get_lv_image_dsc(), selector);
|
||||
::lv_obj_set_style_bitmap_mask_src(obj, image->get_lv_image_dsc(), selector);
|
||||
}
|
||||
|
||||
inline void lv_obj_set_style_bg_image_src(lv_obj_t *obj, image::Image *image, lv_style_selector_t selector) {
|
||||
lv_obj_set_style_bg_image_src(obj, image->get_lv_image_dsc(), selector);
|
||||
::lv_obj_set_style_bg_image_src(obj, image->get_lv_image_dsc(), selector);
|
||||
}
|
||||
inline void lv_style_set_bg_image_src(lv_style_t *style, image::Image *image) {
|
||||
::lv_style_set_bg_image_src(style, image->get_lv_image_dsc());
|
||||
}
|
||||
inline void lv_style_set_bitmap_mask_src(lv_style_t *style, image::Image *image) {
|
||||
::lv_style_set_bitmap_mask_src(style, image->get_lv_image_dsc());
|
||||
}
|
||||
#endif // USE_LVGL_IMAGE
|
||||
#ifdef USE_LVGL_ANIMIMG
|
||||
|
||||
@@ -77,8 +77,11 @@ class ArcType(NumberType):
|
||||
# start_angle and end_angle are mapped to bg_start_angle and bg_end_angle
|
||||
prop = str(prop)
|
||||
if prop.endswith("_angle"):
|
||||
prop = "bg_" + prop
|
||||
await w.set_property(prop, config, processor=validator)
|
||||
await w.set_property(
|
||||
"bg_" + prop, await validator.process(config.get(prop))
|
||||
)
|
||||
else:
|
||||
await w.set_property(prop, config, processor=validator)
|
||||
if CONF_ADJUSTABLE in config:
|
||||
if not config[CONF_ADJUSTABLE]:
|
||||
lv_obj.remove_style(w.obj, nullptr, LV_PART.KNOB)
|
||||
|
||||
@@ -52,19 +52,23 @@ class KeyboardType(WidgetType):
|
||||
if mode := config.get(CONF_MODE):
|
||||
await w.set_property(CONF_MODE, await KEYBOARD_MODES.process(mode))
|
||||
if textarea := config.get(CONF_TEXTAREA):
|
||||
# If a textarea is configured, it must be generated before the keyboard can attach it.
|
||||
# If not yet configured, defer the attachment code.
|
||||
if not is_widget_completed(textarea):
|
||||
# Can only happen for an initial config, where the keyboard is configured before the
|
||||
# textarea, so it's ok to always emit into the global context
|
||||
async def add_textarea():
|
||||
async with LvContext():
|
||||
await w.set_property(
|
||||
CONF_TEXTAREA,
|
||||
(await get_widgets(config, CONF_TEXTAREA))[0].obj,
|
||||
)
|
||||
|
||||
async def add_textarea():
|
||||
async with LvContext():
|
||||
await w.set_property(
|
||||
CONF_TEXTAREA, (await get_widgets(config, CONF_TEXTAREA))[0].obj
|
||||
)
|
||||
|
||||
if is_widget_completed(textarea):
|
||||
await add_textarea()
|
||||
else:
|
||||
CORE.add_job(add_textarea)
|
||||
else:
|
||||
# Handles updates in automations, and properly ordered initial config. Code is generated
|
||||
# into the enclosing context (main or lambda)
|
||||
await w.set_property(
|
||||
CONF_TEXTAREA, (await get_widgets(config, CONF_TEXTAREA))[0].obj
|
||||
)
|
||||
|
||||
|
||||
keyboard_spec = KeyboardType()
|
||||
|
||||
@@ -454,12 +454,12 @@ async def to_code(config):
|
||||
# Pin esp-nn for stable future builds (esp-tflite-micro depends on esp-nn)
|
||||
esp32.add_idf_component(name="espressif/esp-nn", ref="1.1.2")
|
||||
|
||||
esp32.add_idf_component(name="esphome/esp-micro-speech-features", ref="1.2.3")
|
||||
|
||||
cg.add_build_flag("-DTF_LITE_STATIC_MEMORY")
|
||||
cg.add_build_flag("-DTF_LITE_DISABLE_X86_NEON")
|
||||
cg.add_build_flag("-DESP_NN")
|
||||
|
||||
cg.add_library("kahrendt/ESPMicroSpeechFeatures", "1.1.0")
|
||||
|
||||
if vad_model := config.get(CONF_VAD):
|
||||
cg.add_define("USE_MICRO_WAKE_WORD_VAD")
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
from esphome.components.mipi import DriverChip
|
||||
import esphome.config_validation as cv
|
||||
|
||||
# Standalone display
|
||||
# Product page: https://www.seeedstudio.com/reTerminal-D1001-p-6729.html
|
||||
DriverChip(
|
||||
"SEEED-RETERMINAL-D1001",
|
||||
height=1280,
|
||||
width=800,
|
||||
hsync_back_porch=20,
|
||||
hsync_pulse_width=20,
|
||||
hsync_front_porch=40,
|
||||
vsync_back_porch=12,
|
||||
vsync_pulse_width=4,
|
||||
vsync_front_porch=30,
|
||||
pclk_frequency="80MHz",
|
||||
lane_bit_rate="1.5Gbps",
|
||||
swap_xy=cv.UNDEFINED,
|
||||
color_order="RGB",
|
||||
enable_pin=[{"xl9535": None, "number": 0}, {"xl9535": None, "number": 7}],
|
||||
reset_pin={"xl9535": None, "number": 2},
|
||||
initsequence=(
|
||||
(0xE0, 0x00),
|
||||
(0xE1, 0x93),
|
||||
(0xE2, 0x65),
|
||||
(0xE3, 0xF8),
|
||||
(0x80, 0x01),
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,51 @@
|
||||
from esphome.components.mipi import DriverChip
|
||||
from esphome.config_validation import UNDEFINED
|
||||
|
||||
# fmt: off
|
||||
sunton = DriverChip(
|
||||
"ESP32-8048S070",
|
||||
swap_xy=UNDEFINED,
|
||||
initsequence=(),
|
||||
width=800,
|
||||
height=480,
|
||||
pclk_frequency="12.5MHz",
|
||||
de_pin=41,
|
||||
hsync_pin=39,
|
||||
vsync_pin=40,
|
||||
pclk_pin=42,
|
||||
hsync_pulse_width=30,
|
||||
hsync_back_porch=16,
|
||||
hsync_front_porch=210,
|
||||
vsync_pulse_width=13,
|
||||
vsync_back_porch=10,
|
||||
vsync_front_porch=22,
|
||||
data_pins={
|
||||
"red": [14, 21, 47, 48, 45],
|
||||
"green": [9, 46, 3, 8, 16, 1],
|
||||
"blue": [15, 7, 6, 5, 4],
|
||||
},
|
||||
)
|
||||
|
||||
sunton.extend(
|
||||
"ESP32-8048S050",
|
||||
swap_xy=UNDEFINED,
|
||||
initsequence=(),
|
||||
width=800,
|
||||
height=480,
|
||||
pclk_frequency="16MHz",
|
||||
de_pin=40,
|
||||
hsync_pin=39,
|
||||
vsync_pin=41,
|
||||
pclk_pin=42,
|
||||
hsync_back_porch=8,
|
||||
hsync_front_porch=8,
|
||||
hsync_pulse_width=4,
|
||||
vsync_back_porch=8,
|
||||
vsync_front_porch=8,
|
||||
vsync_pulse_width=4,
|
||||
data_pins={
|
||||
"red": [45, 48, 47, 21, 14],
|
||||
"green": [5, 6, 7, 15, 16, 4],
|
||||
"blue": [8, 3, 46, 9, 1],
|
||||
},
|
||||
)
|
||||
@@ -195,7 +195,7 @@ def model_schema(config):
|
||||
"big_endian", "little_endian", lower=True
|
||||
),
|
||||
model.option(CONF_COLOR_DEPTH, 16): cv.one_of(*color_depth, lower=True),
|
||||
model.option(CONF_DRAW_ROUNDING, 2): power_of_two,
|
||||
model.option(CONF_DRAW_ROUNDING, 1): power_of_two,
|
||||
model.option(CONF_PIXEL_MODE, DISPLAY_16BIT): cv.one_of(
|
||||
*pixel_modes, lower=True
|
||||
),
|
||||
@@ -297,9 +297,9 @@ def _final_validate(config):
|
||||
|
||||
buffer_size = color_depth // 8 * width * height // frac
|
||||
# Target a buffer size of 20kB, except for large displays, which shouldn't end up here
|
||||
fraction = min(20000.0, buffer_size // 16) / buffer_size
|
||||
fraction = min(20000.0, buffer_size // 4) / buffer_size
|
||||
config[CONF_BUFFER_SIZE] = 1.0 / next(
|
||||
x for x in range(2, 17) if fraction >= 1 / x
|
||||
(x for x in range(2, 8) if fraction >= 1 / x), 8
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -546,13 +546,12 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
|
||||
}
|
||||
// for updates with a small buffer, we repeatedly call the writer_ function, clipping the height to a fraction of
|
||||
// the display height,
|
||||
for (this->start_line_ = 0; this->start_line_ < this->get_height_internal();
|
||||
this->start_line_ += this->get_height_internal() / FRACTION) {
|
||||
auto increment = (this->get_height_internal() / FRACTION / ROUNDING) * ROUNDING;
|
||||
for (this->start_line_ = 0; this->start_line_ < this->get_height_internal(); this->start_line_ = this->end_line_) {
|
||||
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
|
||||
auto lap = millis();
|
||||
#endif
|
||||
this->end_line_ =
|
||||
clamp_at_most(this->start_line_ + this->get_height_internal() / FRACTION, this->get_height_internal());
|
||||
this->end_line_ = clamp_at_most(this->start_line_ + increment, this->get_height_internal());
|
||||
if (this->auto_clear_enabled_) {
|
||||
this->clear();
|
||||
}
|
||||
@@ -574,12 +573,13 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
|
||||
// Some chips require that the drawing window be aligned on certain boundaries
|
||||
this->x_low_ = this->x_low_ / ROUNDING * ROUNDING;
|
||||
this->y_low_ = this->y_low_ / ROUNDING * ROUNDING;
|
||||
this->x_high_ = (this->x_high_ + ROUNDING) / ROUNDING * ROUNDING - 1;
|
||||
this->y_high_ = (this->y_high_ + ROUNDING) / ROUNDING * ROUNDING - 1;
|
||||
this->x_high_ = round_buffer(this->x_high_ + 1) - 1;
|
||||
this->y_high_ = clamp_at_most(round_buffer(this->y_high_ + 1) - 1, this->end_line_ - 1);
|
||||
int w = this->x_high_ - this->x_low_ + 1;
|
||||
int h = this->y_high_ - this->y_low_ + 1;
|
||||
this->write_to_display_(this->x_low_, this->y_low_, w, h, this->buffer_, this->x_low_,
|
||||
this->y_low_ - this->start_line_, round_buffer(this->get_width_internal()) - w);
|
||||
this->y_low_ - this->start_line_,
|
||||
round_buffer(this->get_width_internal()) - w - this->x_low_);
|
||||
// invalidate watermarks
|
||||
this->x_low_ = this->get_width_internal();
|
||||
this->y_low_ = this->get_height_internal();
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from .ili import ILI9341, ILI9342, ST7789V
|
||||
from esphome.const import CONF_IGNORE_STRAPPING_WARNING, CONF_NUMBER
|
||||
|
||||
from .ili import GC9A01A, ILI9341, ILI9342, ST7789V
|
||||
|
||||
ILI9341.extend(
|
||||
# ESP32-2432S028 CYD board with Micro USB, has ILI9341 controller
|
||||
@@ -43,3 +45,10 @@ ILI9342.extend(
|
||||
(0xE1, 0x00, 0x0B, 0x11, 0x05, 0x13, 0x09, 0x33, 0x67, 0x48, 0x07, 0x0E, 0x0B, 0x23, 0x33, 0x0F), # Negative Gamma Correction
|
||||
)
|
||||
)
|
||||
|
||||
GC9A01A.extend(
|
||||
"ESP32-2424S012",
|
||||
invert_colors=True,
|
||||
cs_pin=10,
|
||||
dc_pin={CONF_NUMBER: 2, CONF_IGNORE_STRAPPING_WARNING: True},
|
||||
)
|
||||
|
||||
@@ -555,7 +555,7 @@ ST7789V = DriverChip(
|
||||
),
|
||||
),
|
||||
)
|
||||
DriverChip(
|
||||
GC9A01A = DriverChip(
|
||||
"GC9A01A",
|
||||
mirror_x=True,
|
||||
width=240,
|
||||
|
||||
@@ -15,7 +15,7 @@ from esphome.components.mipi import (
|
||||
import esphome.config_validation as cv
|
||||
|
||||
from .amoled import CO5300
|
||||
from .ili import ILI9488_A
|
||||
from .ili import ILI9488_A, ST7789V
|
||||
from .jc import AXS15231
|
||||
|
||||
DriverChip(
|
||||
@@ -243,3 +243,15 @@ ST7789P.extend(
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
ST7789V.extend(
|
||||
"WAVESHARE-ESP32-C6-LCD-1.47",
|
||||
width=172,
|
||||
height=320,
|
||||
offset_width=34,
|
||||
invert_colors=True,
|
||||
data_rate="40MHz",
|
||||
reset_pin=21,
|
||||
cs_pin=14,
|
||||
dc_pin={"number": 15, "ignore_strapping_warning": True},
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace esphome::mitsubishi_cn105 {
|
||||
static const char *const TAG = "mitsubishi_cn105.climate";
|
||||
|
||||
static constexpr std::array MODE_MAP{
|
||||
std::pair{MitsubishiCN105::Mode::AUTO, climate::CLIMATE_MODE_AUTO},
|
||||
std::pair{MitsubishiCN105::Mode::AUTO, climate::CLIMATE_MODE_HEAT_COOL},
|
||||
std::pair{MitsubishiCN105::Mode::HEAT, climate::CLIMATE_MODE_HEAT},
|
||||
std::pair{MitsubishiCN105::Mode::DRY, climate::CLIMATE_MODE_DRY},
|
||||
std::pair{MitsubishiCN105::Mode::COOL, climate::CLIMATE_MODE_COOL},
|
||||
@@ -76,23 +76,13 @@ void MitsubishiCN105Climate::loop() {
|
||||
climate::ClimateTraits MitsubishiCN105Climate::traits() {
|
||||
climate::ClimateTraits traits;
|
||||
|
||||
traits.set_supported_modes({
|
||||
climate::CLIMATE_MODE_OFF,
|
||||
climate::CLIMATE_MODE_COOL,
|
||||
climate::CLIMATE_MODE_HEAT,
|
||||
climate::CLIMATE_MODE_DRY,
|
||||
climate::CLIMATE_MODE_FAN_ONLY,
|
||||
climate::CLIMATE_MODE_AUTO,
|
||||
});
|
||||
for (const auto &p : MODE_MAP) {
|
||||
traits.add_supported_mode(p.second);
|
||||
}
|
||||
|
||||
traits.set_supported_fan_modes({
|
||||
climate::CLIMATE_FAN_AUTO,
|
||||
climate::CLIMATE_FAN_QUIET,
|
||||
climate::CLIMATE_FAN_LOW,
|
||||
climate::CLIMATE_FAN_MEDIUM,
|
||||
climate::CLIMATE_FAN_MIDDLE,
|
||||
climate::CLIMATE_FAN_HIGH,
|
||||
});
|
||||
for (const auto &p : FAN_MODE_MAP) {
|
||||
traits.add_supported_fan_mode(p.second);
|
||||
}
|
||||
|
||||
traits.set_visual_min_temperature(16.0f);
|
||||
traits.set_visual_max_temperature(31.0f);
|
||||
|
||||
@@ -5,6 +5,29 @@ namespace esphome::modbus::helpers {
|
||||
|
||||
static const char *const TAG = "modbus_helpers";
|
||||
|
||||
static size_t required_payload_size(SensorValueType sensor_value_type) {
|
||||
switch (sensor_value_type) {
|
||||
case SensorValueType::U_WORD:
|
||||
case SensorValueType::S_WORD:
|
||||
return 2;
|
||||
case SensorValueType::U_DWORD:
|
||||
case SensorValueType::FP32:
|
||||
case SensorValueType::U_DWORD_R:
|
||||
case SensorValueType::FP32_R:
|
||||
case SensorValueType::S_DWORD:
|
||||
case SensorValueType::S_DWORD_R:
|
||||
return 4;
|
||||
case SensorValueType::U_QWORD:
|
||||
case SensorValueType::S_QWORD:
|
||||
case SensorValueType::U_QWORD_R:
|
||||
case SensorValueType::S_QWORD_R:
|
||||
return 8;
|
||||
case SensorValueType::RAW:
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
void number_to_payload(std::vector<uint16_t> &data, int64_t value, SensorValueType value_type) {
|
||||
switch (value_type) {
|
||||
case SensorValueType::U_WORD:
|
||||
@@ -47,93 +70,70 @@ int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sens
|
||||
uint32_t bitmask) {
|
||||
int64_t value = 0; // int64_t because it can hold signed and unsigned 32 bits
|
||||
|
||||
if (offset > data.size()) {
|
||||
ESP_LOGE(TAG, "not enough data for value");
|
||||
// Validate offset against the buffer for all types, including RAW/unsupported, so
|
||||
// a malformed or misconfigured frame still produces an error log.
|
||||
if (static_cast<size_t>(offset) > data.size()) {
|
||||
ESP_LOGE(TAG, "not enough data for value type=%u offset=%u size=%zu", static_cast<unsigned int>(sensor_value_type),
|
||||
static_cast<unsigned int>(offset), data.size());
|
||||
return value;
|
||||
}
|
||||
|
||||
const size_t required_size = required_payload_size(sensor_value_type);
|
||||
if (required_size == 0) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (data.size() - offset < required_size) {
|
||||
ESP_LOGE(TAG, "not enough data for value type=%u offset=%u size=%zu required=%zu",
|
||||
static_cast<unsigned int>(sensor_value_type), static_cast<unsigned int>(offset), data.size(),
|
||||
required_size);
|
||||
return value;
|
||||
}
|
||||
|
||||
size_t size = data.size() - offset;
|
||||
bool error = false;
|
||||
switch (sensor_value_type) {
|
||||
case SensorValueType::U_WORD:
|
||||
if (size >= 2) {
|
||||
value = mask_and_shift_by_rightbit(get_data<uint16_t>(data, offset),
|
||||
bitmask); // default is 0xFFFF ;
|
||||
} else {
|
||||
error = true;
|
||||
}
|
||||
value = mask_and_shift_by_rightbit(get_data<uint16_t>(data, offset), bitmask); // default is 0xFFFF ;
|
||||
break;
|
||||
case SensorValueType::U_DWORD:
|
||||
case SensorValueType::FP32:
|
||||
if (size >= 4) {
|
||||
value = get_data<uint32_t>(data, offset);
|
||||
value = mask_and_shift_by_rightbit((uint32_t) value, bitmask);
|
||||
} else {
|
||||
error = true;
|
||||
}
|
||||
value = get_data<uint32_t>(data, offset);
|
||||
value = mask_and_shift_by_rightbit((uint32_t) value, bitmask);
|
||||
break;
|
||||
case SensorValueType::U_DWORD_R:
|
||||
case SensorValueType::FP32_R:
|
||||
if (size >= 4) {
|
||||
value = get_data<uint32_t>(data, offset);
|
||||
value = static_cast<uint32_t>(value & 0xFFFF) << 16 | (value & 0xFFFF0000) >> 16;
|
||||
value = mask_and_shift_by_rightbit((uint32_t) value, bitmask);
|
||||
} else {
|
||||
error = true;
|
||||
}
|
||||
value = get_data<uint32_t>(data, offset);
|
||||
value = static_cast<uint32_t>(value & 0xFFFF) << 16 | (value & 0xFFFF0000) >> 16;
|
||||
value = mask_and_shift_by_rightbit((uint32_t) value, bitmask);
|
||||
break;
|
||||
case SensorValueType::S_WORD:
|
||||
if (size >= 2) {
|
||||
value = mask_and_shift_by_rightbit(get_data<int16_t>(data, offset),
|
||||
bitmask); // default is 0xFFFF ;
|
||||
} else {
|
||||
error = true;
|
||||
}
|
||||
value = mask_and_shift_by_rightbit(get_data<int16_t>(data, offset), bitmask); // default is 0xFFFF ;
|
||||
break;
|
||||
case SensorValueType::S_DWORD:
|
||||
if (size >= 4) {
|
||||
value = mask_and_shift_by_rightbit(get_data<int32_t>(data, offset), bitmask);
|
||||
} else {
|
||||
error = true;
|
||||
}
|
||||
value = mask_and_shift_by_rightbit(get_data<int32_t>(data, offset), bitmask);
|
||||
break;
|
||||
case SensorValueType::S_DWORD_R: {
|
||||
if (size >= 4) {
|
||||
value = get_data<uint32_t>(data, offset);
|
||||
// Currently the high word is at the low position
|
||||
// the sign bit is therefore at low before the switch
|
||||
uint32_t sign_bit = (value & 0x8000) << 16;
|
||||
value = mask_and_shift_by_rightbit(
|
||||
static_cast<int32_t>(((value & 0x7FFF) << 16 | (value & 0xFFFF0000) >> 16) | sign_bit), bitmask);
|
||||
} else {
|
||||
error = true;
|
||||
}
|
||||
value = get_data<uint32_t>(data, offset);
|
||||
// Currently the high word is at the low position
|
||||
// the sign bit is therefore at low before the switch
|
||||
uint32_t sign_bit = (value & 0x8000) << 16;
|
||||
value = mask_and_shift_by_rightbit(
|
||||
static_cast<int32_t>(((value & 0x7FFF) << 16 | (value & 0xFFFF0000) >> 16) | sign_bit), bitmask);
|
||||
} break;
|
||||
case SensorValueType::U_QWORD:
|
||||
case SensorValueType::S_QWORD:
|
||||
// Ignore bitmask for QWORD
|
||||
if (size >= 8) {
|
||||
value = get_data<uint64_t>(data, offset);
|
||||
} else {
|
||||
error = true;
|
||||
}
|
||||
value = get_data<uint64_t>(data, offset);
|
||||
break;
|
||||
case SensorValueType::U_QWORD_R:
|
||||
case SensorValueType::S_QWORD_R: {
|
||||
// Ignore bitmask for QWORD
|
||||
if (size >= 8) {
|
||||
uint64_t tmp = get_data<uint64_t>(data, offset);
|
||||
value = (tmp << 48) | (tmp >> 48) | ((tmp & 0xFFFF0000) << 16) | ((tmp >> 16) & 0xFFFF0000);
|
||||
} else {
|
||||
error = true;
|
||||
}
|
||||
uint64_t tmp = get_data<uint64_t>(data, offset);
|
||||
value = (tmp << 48) | (tmp >> 48) | ((tmp & 0xFFFF0000) << 16) | ((tmp >> 16) & 0xFFFF0000);
|
||||
} break;
|
||||
case SensorValueType::RAW:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (error)
|
||||
ESP_LOGE(TAG, "not enough data for value");
|
||||
return value;
|
||||
}
|
||||
} // namespace esphome::modbus::helpers
|
||||
|
||||
@@ -8,8 +8,11 @@ from typing import Any
|
||||
from esphome import git, yaml_util
|
||||
from esphome.components.substitutions import (
|
||||
ContextVars,
|
||||
ErrList,
|
||||
push_context,
|
||||
raise_first_undefined,
|
||||
resolve_include,
|
||||
resolve_substitutions_block,
|
||||
substitute,
|
||||
)
|
||||
from esphome.components.substitutions.jinja import has_jinja
|
||||
@@ -359,12 +362,19 @@ def _substitute_package_definition(
|
||||
if isinstance(package_config, str) or (
|
||||
isinstance(package_config, dict) and is_remote_package(package_config)
|
||||
):
|
||||
# Collect undefined-variable errors (rather than raising strict) so the
|
||||
# path walked through a remote-package dict is preserved and the user
|
||||
# sees which field (url / path / ref / ...) referenced the undefined
|
||||
# variable.
|
||||
errors: ErrList = []
|
||||
package_config = substitute(
|
||||
item=package_config,
|
||||
path=[],
|
||||
parent_context=context_vars or ContextVars(),
|
||||
strict_undefined=False,
|
||||
errors=errors,
|
||||
)
|
||||
raise_first_undefined(errors, package_config, "package definition")
|
||||
return package_config
|
||||
|
||||
|
||||
@@ -516,7 +526,12 @@ def do_packages_pass(
|
||||
if CONF_PACKAGES not in config:
|
||||
return config
|
||||
|
||||
substitutions = UserDict(config.pop(CONF_SUBSTITUTIONS, {}))
|
||||
with cv.prepend_path(CONF_SUBSTITUTIONS):
|
||||
substitutions = UserDict(
|
||||
resolve_substitutions_block(
|
||||
config.pop(CONF_SUBSTITUTIONS, {}), command_line_substitutions
|
||||
)
|
||||
)
|
||||
processor = _PackageProcessor(
|
||||
substitutions, command_line_substitutions, skip_update
|
||||
)
|
||||
|
||||
@@ -44,7 +44,7 @@ void PCF85063Component::read_time() {
|
||||
.year = uint16_t(pcf85063_.reg.year + 10u * pcf85063_.reg.year_10 + 2000),
|
||||
};
|
||||
rtc_time.recalc_timestamp_utc(false);
|
||||
if (!rtc_time.is_valid()) {
|
||||
if (!rtc_time.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) {
|
||||
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ void PCF8563Component::read_time() {
|
||||
.year = uint16_t(pcf8563_.reg.year + 10u * pcf8563_.reg.year_10 + 2000),
|
||||
};
|
||||
rtc_time.recalc_timestamp_utc(false);
|
||||
if (!rtc_time.is_valid()) {
|
||||
if (!rtc_time.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) {
|
||||
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ void QMC5883LComponent::update() {
|
||||
// ROL_PNT in setup and reading 7 bytes starting at the status register.
|
||||
// If status and all three axes are desired, using ROL_PNT saves you 3 bytes.
|
||||
// But simply not reading status saves you 4 bytes always and is much simpler.
|
||||
if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG) {
|
||||
if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) {
|
||||
err = this->read_register(QMC5883L_REGISTER_STATUS, &status, 1);
|
||||
if (err != i2c::ERROR_OK) {
|
||||
char buf[32];
|
||||
@@ -165,7 +165,7 @@ void QMC5883LComponent::update() {
|
||||
temp = int16_t(raw_temp) * 0.01f;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Got x=%0.02fµT y=%0.02fµT z=%0.02fµT heading=%0.01f° temperature=%0.01f°C status=%u", x, y, z, heading,
|
||||
ESP_LOGV(TAG, "Got x=%0.02fµT y=%0.02fµT z=%0.02fµT heading=%0.01f° temperature=%0.01f°C status=%u", x, y, z, heading,
|
||||
temp, status);
|
||||
|
||||
if (this->x_sensor_ != nullptr)
|
||||
|
||||
@@ -65,3 +65,8 @@ async def to_code(config):
|
||||
@pins.PIN_SCHEMA_REGISTRY.register("rtl87xx", PIN_SCHEMA)
|
||||
async def pin_to_code(config):
|
||||
return await libretiny.gpio.component_pin_to_code(config)
|
||||
|
||||
|
||||
# Called by writer.py; delegates to the shared libretiny implementation.
|
||||
def copy_files() -> None:
|
||||
libretiny.copy_files()
|
||||
|
||||
@@ -294,57 +294,59 @@ void Rtttl::play(std::string rtttl) {
|
||||
}
|
||||
ESP_LOGD(TAG, "Playing song %.*s", (int) this->position_, this->rtttl_.c_str());
|
||||
|
||||
// Get default duration
|
||||
this->position_ = this->rtttl_.find("d=", this->position_);
|
||||
if (this->position_ == std::string::npos) {
|
||||
ESP_LOGE(TAG, "Missing 'd='");
|
||||
return;
|
||||
}
|
||||
this->position_ += 2;
|
||||
num = this->get_integer_();
|
||||
if (num == 1 || num == 2 || num == 4 || num == 8 || num == 16 || num == 32) {
|
||||
this->default_note_denominator_ = num;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Invalid default duration: %d", num);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get default octave
|
||||
this->position_ = this->rtttl_.find("o=", this->position_);
|
||||
if (this->position_ == std::string::npos) {
|
||||
ESP_LOGE(TAG, "Missing 'o=");
|
||||
return;
|
||||
}
|
||||
this->position_ += 2;
|
||||
num = this->get_integer_();
|
||||
if (num >= MIN_OCTAVE && num <= MAX_OCTAVE) {
|
||||
this->default_octave_ = num;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Invalid default octave: %d", num);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get BPM
|
||||
this->position_ = this->rtttl_.find("b=", this->position_);
|
||||
if (this->position_ == std::string::npos) {
|
||||
ESP_LOGE(TAG, "Missing b=");
|
||||
return;
|
||||
}
|
||||
this->position_ += 2;
|
||||
num = this->get_integer_();
|
||||
if (num >= 4) { // Below 4 is not realistic and would cause a integer overflow
|
||||
bpm = num;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Invalid BPM: %d", num);
|
||||
return;
|
||||
}
|
||||
|
||||
this->position_ = this->rtttl_.find(':', this->position_);
|
||||
if (this->position_ == std::string::npos) {
|
||||
size_t name_end_position = this->position_;
|
||||
size_t control_end = this->rtttl_.find(':', name_end_position + 1);
|
||||
if (control_end == std::string::npos) {
|
||||
ESP_LOGE(TAG, "Missing second ':'");
|
||||
return;
|
||||
}
|
||||
this->position_++;
|
||||
|
||||
// Get default duration
|
||||
size_t pos = this->rtttl_.find("d=", name_end_position);
|
||||
if (pos == std::string::npos || pos >= control_end) {
|
||||
ESP_LOGW(TAG, "Missing 'd='; use default duration %d", this->default_note_denominator_);
|
||||
} else {
|
||||
this->position_ = pos + 2;
|
||||
num = this->get_integer_();
|
||||
if (num == 1 || num == 2 || num == 4 || num == 8 || num == 16 || num == 32) {
|
||||
this->default_note_denominator_ = num;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Invalid default duration: %d", num);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Get default octave
|
||||
pos = this->rtttl_.find("o=", name_end_position);
|
||||
if (pos == std::string::npos || pos >= control_end) {
|
||||
ESP_LOGW(TAG, "Missing 'o='; use default octave %d", this->default_octave_);
|
||||
} else {
|
||||
this->position_ = pos + 2;
|
||||
num = this->get_integer_();
|
||||
if (num >= MIN_OCTAVE && num <= MAX_OCTAVE) {
|
||||
this->default_octave_ = num;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Invalid default octave: %d", num);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Get BPM
|
||||
pos = this->rtttl_.find("b=", name_end_position);
|
||||
if (pos == std::string::npos || pos >= control_end) {
|
||||
ESP_LOGW(TAG, "Missing 'b='; use default BPM %d", bpm);
|
||||
} else {
|
||||
this->position_ = pos + 2;
|
||||
num = this->get_integer_();
|
||||
if (num >= 4) { // Below 4 is not realistic and would cause a integer overflow
|
||||
bpm = num;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Invalid BPM: %d", num);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this->position_ = control_end + 1;
|
||||
|
||||
// BPM usually expresses the number of quarter notes per minute
|
||||
this->wholenote_duration_ = 60 * 1000L * 4 / bpm; // This is the time for whole note (in milliseconds)
|
||||
|
||||
@@ -127,9 +127,9 @@ void RuntimeImage::draw_pixel(int x, int y, const Color &color) {
|
||||
uint32_t pos = this->get_position_(x, y);
|
||||
Color mapped_color = color;
|
||||
this->map_chroma_key(mapped_color);
|
||||
this->buffer_[pos + 0] = mapped_color.r;
|
||||
this->buffer_[pos + 0] = mapped_color.b;
|
||||
this->buffer_[pos + 1] = mapped_color.g;
|
||||
this->buffer_[pos + 2] = mapped_color.b;
|
||||
this->buffer_[pos + 2] = mapped_color.r;
|
||||
if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
|
||||
this->buffer_[pos + 3] = color.w;
|
||||
}
|
||||
|
||||
@@ -32,40 +32,101 @@ void RuntimeStatsCollector::log_stats_() {
|
||||
" Period stats (last %" PRIu32 "ms): %zu active components",
|
||||
this->log_interval_, count);
|
||||
|
||||
if (count == 0) {
|
||||
return;
|
||||
// Sum component time so we can derive main-loop overhead
|
||||
// (active loop time minus time attributable to component loop()s).
|
||||
// Period sum iterates the active-in-period subset; total sum must iterate
|
||||
// all components since total_active_time_us_ includes iterations where
|
||||
// currently-idle components previously ran.
|
||||
uint64_t period_component_sum_us = 0;
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
period_component_sum_us += sorted[i]->runtime_stats_.period_time_us;
|
||||
}
|
||||
uint64_t total_component_sum_us = 0;
|
||||
for (auto *component : components) {
|
||||
total_component_sum_us += component->runtime_stats_.total_time_us;
|
||||
}
|
||||
|
||||
// Sort by period runtime (descending)
|
||||
std::sort(sorted, sorted + count, compare_period_time);
|
||||
if (count > 0) {
|
||||
// Sort by period runtime (descending)
|
||||
std::sort(sorted, sorted + count, compare_period_time);
|
||||
|
||||
// Log top components by period runtime
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
const auto &stats = sorted[i]->runtime_stats_;
|
||||
ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms",
|
||||
LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.period_count,
|
||||
stats.period_count > 0 ? stats.period_time_us / (float) stats.period_count / 1000.0f : 0.0f,
|
||||
stats.period_max_time_us / 1000.0f, stats.period_time_us / 1000.0f);
|
||||
// Log top components by period runtime
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
const auto &stats = sorted[i]->runtime_stats_;
|
||||
ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms",
|
||||
LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.period_count,
|
||||
stats.period_count > 0 ? stats.period_time_us / (float) stats.period_count / 1000.0f : 0.0f,
|
||||
stats.period_max_time_us / 1000.0f, stats.period_time_us / 1000.0f);
|
||||
}
|
||||
}
|
||||
|
||||
// Main-loop overhead for the period: active wall time minus component time.
|
||||
// active = sum of per-iteration loop time excluding yield/sleep.
|
||||
if (this->period_active_count_ > 0) {
|
||||
uint64_t active = this->period_active_time_us_;
|
||||
uint64_t overhead = active > period_component_sum_us ? active - period_component_sum_us : 0;
|
||||
// Use double for µs→ms conversion so multi-day uptimes (where total
|
||||
// microsecond counters exceed float's ~7-digit mantissa) keep resolution.
|
||||
ESP_LOGI(TAG,
|
||||
" main_loop: iters=%" PRIu64 ", active_avg=%.3fms, active_max=%.2fms, active_total=%.1fms, "
|
||||
"overhead_total=%.1fms",
|
||||
this->period_active_count_,
|
||||
static_cast<double>(active) / static_cast<double>(this->period_active_count_) / 1000.0,
|
||||
static_cast<double>(this->period_active_max_us_) / 1000.0, static_cast<double>(active) / 1000.0,
|
||||
static_cast<double>(overhead) / 1000.0);
|
||||
uint64_t before = this->period_before_time_us_;
|
||||
uint64_t tail = this->period_tail_time_us_;
|
||||
uint64_t accounted = before + tail;
|
||||
uint64_t inter = overhead > accounted ? overhead - accounted : 0;
|
||||
ESP_LOGI(TAG, " main_loop_overhead_section: before=%.1fms, tail=%.1fms, inter_component=%.1fms",
|
||||
static_cast<double>(before) / 1000.0, static_cast<double>(tail) / 1000.0,
|
||||
static_cast<double>(inter) / 1000.0);
|
||||
}
|
||||
|
||||
// Log total stats since boot (only for active components - idle ones haven't changed)
|
||||
ESP_LOGI(TAG, " Total stats (since boot): %zu active components", count);
|
||||
|
||||
// Re-sort by total runtime for all-time stats
|
||||
std::sort(sorted, sorted + count, compare_total_time);
|
||||
if (count > 0) {
|
||||
// Re-sort by total runtime for all-time stats
|
||||
std::sort(sorted, sorted + count, compare_total_time);
|
||||
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
const auto &stats = sorted[i]->runtime_stats_;
|
||||
ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms",
|
||||
LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.total_count,
|
||||
stats.total_count > 0 ? stats.total_time_us / (float) stats.total_count / 1000.0f : 0.0f,
|
||||
stats.total_max_time_us / 1000.0f, stats.total_time_us / 1000.0);
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
const auto &stats = sorted[i]->runtime_stats_;
|
||||
ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.3fms, max=%.2fms, total=%.1fms",
|
||||
LOG_STR_ARG(sorted[i]->get_component_log_str()), stats.total_count,
|
||||
stats.total_count > 0 ? stats.total_time_us / (float) stats.total_count / 1000.0f : 0.0f,
|
||||
stats.total_max_time_us / 1000.0f, stats.total_time_us / 1000.0);
|
||||
}
|
||||
}
|
||||
|
||||
if (this->total_active_count_ > 0) {
|
||||
uint64_t active = this->total_active_time_us_;
|
||||
uint64_t overhead = active > total_component_sum_us ? active - total_component_sum_us : 0;
|
||||
ESP_LOGI(TAG,
|
||||
" main_loop: iters=%" PRIu64 ", active_avg=%.3fms, active_max=%.2fms, active_total=%.1fms, "
|
||||
"overhead_total=%.1fms",
|
||||
this->total_active_count_,
|
||||
static_cast<double>(active) / static_cast<double>(this->total_active_count_) / 1000.0,
|
||||
static_cast<double>(this->total_active_max_us_) / 1000.0, static_cast<double>(active) / 1000.0,
|
||||
static_cast<double>(overhead) / 1000.0);
|
||||
uint64_t before = this->total_before_time_us_;
|
||||
uint64_t tail = this->total_tail_time_us_;
|
||||
uint64_t accounted = before + tail;
|
||||
uint64_t inter = overhead > accounted ? overhead - accounted : 0;
|
||||
ESP_LOGI(TAG, " main_loop_overhead_section: before=%.1fms, tail=%.1fms, inter_component=%.1fms",
|
||||
static_cast<double>(before) / 1000.0, static_cast<double>(tail) / 1000.0,
|
||||
static_cast<double>(inter) / 1000.0);
|
||||
}
|
||||
|
||||
// Reset period stats
|
||||
for (auto *component : components) {
|
||||
component->runtime_stats_.reset_period();
|
||||
}
|
||||
this->period_active_count_ = 0;
|
||||
this->period_active_time_us_ = 0;
|
||||
this->period_active_max_us_ = 0;
|
||||
this->period_before_time_us_ = 0;
|
||||
this->period_tail_time_us_ = 0;
|
||||
}
|
||||
|
||||
bool RuntimeStatsCollector::compare_period_time(Component *a, Component *b) {
|
||||
|
||||
@@ -29,6 +29,31 @@ class RuntimeStatsCollector {
|
||||
// Process any pending stats printing (should be called after component loop)
|
||||
void process_pending_stats(uint32_t current_time);
|
||||
|
||||
// Record the wall time of one main loop iteration excluding the yield/sleep.
|
||||
// Called once per loop from Application::loop().
|
||||
// active_us = total time between loop start and just before yield.
|
||||
// before_us = time spent in before_loop_tasks_ (scheduler + ISR enable_loop).
|
||||
// tail_us = time spent in after_loop_tasks_ + the trailing record/stats prefix.
|
||||
// Residual overhead at log time = active − Σ(component) − before − tail,
|
||||
// which captures per-iteration inter-component bookkeeping (set_current_component,
|
||||
// WarnIfComponentBlockingGuard construction/destruction, feed_wdt_with_time calls,
|
||||
// the for-loop itself).
|
||||
void record_loop_active(uint32_t active_us, uint32_t before_us, uint32_t tail_us) {
|
||||
this->period_active_count_++;
|
||||
this->period_active_time_us_ += active_us;
|
||||
if (active_us > this->period_active_max_us_)
|
||||
this->period_active_max_us_ = active_us;
|
||||
this->total_active_count_++;
|
||||
this->total_active_time_us_ += active_us;
|
||||
if (active_us > this->total_active_max_us_)
|
||||
this->total_active_max_us_ = active_us;
|
||||
|
||||
this->period_before_time_us_ += before_us;
|
||||
this->total_before_time_us_ += before_us;
|
||||
this->period_tail_time_us_ += tail_us;
|
||||
this->total_tail_time_us_ += tail_us;
|
||||
}
|
||||
|
||||
protected:
|
||||
void log_stats_();
|
||||
// Static comparators — member functions have friend access, lambdas do not
|
||||
@@ -37,6 +62,22 @@ class RuntimeStatsCollector {
|
||||
|
||||
uint32_t log_interval_;
|
||||
uint32_t next_log_time_{0};
|
||||
|
||||
// Main loop active-time stats (wall time per iteration, excluding yield/sleep).
|
||||
// Counters are uint64_t — at sub-millisecond loop times a uint32_t can wrap in
|
||||
// a few weeks of uptime, which is well within ESPHome device lifetimes.
|
||||
uint64_t period_active_count_{0};
|
||||
uint64_t period_active_time_us_{0};
|
||||
uint32_t period_active_max_us_{0};
|
||||
uint64_t total_active_count_{0};
|
||||
uint64_t total_active_time_us_{0};
|
||||
uint32_t total_active_max_us_{0};
|
||||
|
||||
// Split of overhead sections — accumulated per iteration.
|
||||
uint64_t period_before_time_us_{0};
|
||||
uint64_t total_before_time_us_{0};
|
||||
uint64_t period_tail_time_us_{0};
|
||||
uint64_t total_tail_time_us_{0};
|
||||
};
|
||||
|
||||
} // namespace runtime_stats
|
||||
|
||||
@@ -81,7 +81,7 @@ void RX8130Component::read_time() {
|
||||
.year = static_cast<uint16_t>(bcd2dec(date[6]) + 2000),
|
||||
};
|
||||
rtc_time.recalc_timestamp_utc(false);
|
||||
if (!rtc_time.is_valid()) {
|
||||
if (!rtc_time.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) {
|
||||
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -275,6 +275,9 @@ ThrottleFilter = sensor_ns.class_("ThrottleFilter", Filter)
|
||||
ThrottleWithPriorityFilter = sensor_ns.class_(
|
||||
"ThrottleWithPriorityFilter", ValueListFilter
|
||||
)
|
||||
ThrottleWithPriorityNanFilter = sensor_ns.class_(
|
||||
"ThrottleWithPriorityNanFilter", Filter
|
||||
)
|
||||
TimeoutFilterBase = sensor_ns.class_("TimeoutFilterBase", Filter, cg.Component)
|
||||
TimeoutFilterLast = sensor_ns.class_("TimeoutFilterLast", TimeoutFilterBase)
|
||||
TimeoutFilterConfigured = sensor_ns.class_("TimeoutFilterConfigured", TimeoutFilterBase)
|
||||
@@ -656,9 +659,18 @@ THROTTLE_WITH_PRIORITY_SCHEMA = cv.maybe_simple_value(
|
||||
THROTTLE_WITH_PRIORITY_SCHEMA,
|
||||
)
|
||||
async def throttle_with_priority_filter_to_code(config, filter_id):
|
||||
if not isinstance(config[CONF_VALUE], list):
|
||||
config[CONF_VALUE] = [config[CONF_VALUE]]
|
||||
template_ = [await cg.templatable(x, [], cg.float_) for x in config[CONF_VALUE]]
|
||||
values = config[CONF_VALUE]
|
||||
if not isinstance(values, list):
|
||||
values = [values]
|
||||
# Specialize the common "NaN-only" case (the schema default when the user
|
||||
# omits `value:`) to avoid the TemplatableFn<float> array + NaN lambda the
|
||||
# generic ValueListFilter path requires. Behavior is identical: NaN sensor
|
||||
# readings always bypass the throttle.
|
||||
if values and all(isinstance(v, float) and math.isnan(v) for v in values):
|
||||
filter_id = filter_id.copy()
|
||||
filter_id.type = ThrottleWithPriorityNanFilter
|
||||
return cg.new_Pvariable(filter_id, config[CONF_TIMEOUT])
|
||||
template_ = [await cg.templatable(x, [], cg.float_) for x in values]
|
||||
return cg.new_Pvariable(
|
||||
filter_id, cg.TemplateArguments(len(template_)), config[CONF_TIMEOUT], template_
|
||||
)
|
||||
|
||||
@@ -269,6 +269,18 @@ optional<float> throttle_with_priority_new_value(Sensor *parent, float value, co
|
||||
return {};
|
||||
}
|
||||
|
||||
// ThrottleWithPriorityNanFilter
|
||||
ThrottleWithPriorityNanFilter::ThrottleWithPriorityNanFilter(uint32_t min_time_between_inputs)
|
||||
: min_time_between_inputs_(min_time_between_inputs) {}
|
||||
optional<float> ThrottleWithPriorityNanFilter::new_value(float value) {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
if (this->last_input_ == 0 || now - this->last_input_ >= this->min_time_between_inputs_ || std::isnan(value)) {
|
||||
this->last_input_ = now;
|
||||
return value;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
// DeltaFilter
|
||||
DeltaFilter::DeltaFilter(float min_a0, float min_a1, float max_a0, float max_a1)
|
||||
: min_a0_(min_a0), min_a1_(min_a1), max_a0_(max_a0), max_a1_(max_a1) {}
|
||||
|
||||
@@ -399,6 +399,19 @@ template<size_t N> class ThrottleWithPriorityFilter : public ValueListFilter<N>
|
||||
uint32_t min_time_between_inputs_;
|
||||
};
|
||||
|
||||
/// Specialization of ThrottleWithPriorityFilter for the common "prioritize NaN"
|
||||
/// case: skips the TemplatableFn<float> array + lambda and inlines the check.
|
||||
class ThrottleWithPriorityNanFilter : public Filter {
|
||||
public:
|
||||
explicit ThrottleWithPriorityNanFilter(uint32_t min_time_between_inputs);
|
||||
|
||||
optional<float> new_value(float value) override;
|
||||
|
||||
protected:
|
||||
uint32_t last_input_{0};
|
||||
uint32_t min_time_between_inputs_;
|
||||
};
|
||||
|
||||
// Base class for timeout filters - contains common loop logic
|
||||
class TimeoutFilterBase : public Filter, public Component {
|
||||
public:
|
||||
|
||||
@@ -84,7 +84,7 @@ def get_firmware(value):
|
||||
req = requests.get(url, timeout=30)
|
||||
req.raise_for_status()
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise cv.Invalid(f"Could not download firmware file ({url}): {e}")
|
||||
raise cv.Invalid(f"Could not download firmware file ({url}): {e}") from e
|
||||
|
||||
h = hashlib.new("sha256")
|
||||
h.update(req.content)
|
||||
|
||||
@@ -14,38 +14,34 @@ BSDSocketImpl::BSDSocketImpl(int fd, bool monitor_loop) {
|
||||
if (!monitor_loop || this->fd_ < 0)
|
||||
return;
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
// Cache lwip_sock pointer and register for monitoring (hooks callback internally)
|
||||
this->cached_sock_ = esphome_lwip_get_sock(this->fd_);
|
||||
this->loop_monitored_ = App.register_socket(this->cached_sock_);
|
||||
this->cached_sock_ = hook_fd_for_fast_select(this->fd_);
|
||||
#else
|
||||
this->loop_monitored_ = App.register_socket_fd(this->fd_);
|
||||
#endif
|
||||
}
|
||||
|
||||
BSDSocketImpl::~BSDSocketImpl() {
|
||||
if (!this->closed_) {
|
||||
this->close();
|
||||
}
|
||||
}
|
||||
BSDSocketImpl::~BSDSocketImpl() { this->close(); }
|
||||
|
||||
int BSDSocketImpl::close() {
|
||||
if (!this->closed_) {
|
||||
// Unregister before closing to avoid dangling pointer in monitored set
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
if (this->loop_monitored_) {
|
||||
App.unregister_socket(this->cached_sock_);
|
||||
this->cached_sock_ = nullptr;
|
||||
}
|
||||
#else
|
||||
if (this->loop_monitored_) {
|
||||
App.unregister_socket_fd(this->fd_);
|
||||
}
|
||||
#endif
|
||||
int ret = ::close(this->fd_);
|
||||
this->closed_ = true;
|
||||
return ret;
|
||||
if (this->fd_ < 0) {
|
||||
// Already closed, or never opened.
|
||||
return 0;
|
||||
}
|
||||
return 0;
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
// Null the cached lwip_sock pointer before closing. The underlying lwip slot can be
|
||||
// recycled for a new connection as soon as ::close() returns, so anything that might
|
||||
// dereference cached_sock_ post-close (e.g. setsockopt(TCP_NODELAY)) would otherwise
|
||||
// touch an unrelated socket's pcb. No per-socket callback unhook is needed —
|
||||
// all LwIP sockets share the same static event_callback.
|
||||
this->cached_sock_ = nullptr;
|
||||
#else
|
||||
if (this->loop_monitored_) {
|
||||
App.unregister_socket_fd(this->fd_);
|
||||
}
|
||||
#endif
|
||||
int ret = ::close(this->fd_);
|
||||
this->fd_ = -1; // Sentinel for "closed" — prevents double-close and makes use-after-close visible.
|
||||
return ret;
|
||||
}
|
||||
|
||||
int BSDSocketImpl::setblocking(bool blocking) {
|
||||
|
||||
@@ -119,12 +119,21 @@ class BSDSocketImpl {
|
||||
int get_fd() const { return this->fd_; }
|
||||
|
||||
protected:
|
||||
// fd_ < 0 means "not open" — used both pre-open (initial state) and post-close. This
|
||||
// replaces a separate closed_ flag: close() sets fd_ = -1 after ::close(), and the
|
||||
// destructor / double-close path just check fd_ < 0.
|
||||
int fd_{-1};
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
struct lwip_sock *cached_sock_{nullptr}; // Cached for direct rcvevent read in ready()
|
||||
#endif
|
||||
bool closed_{false};
|
||||
// Cached lwip_sock pointer used for direct rcvevent reads in ready() on the
|
||||
// fast-select path. Replaces loop_monitored_: null means this socket is not being
|
||||
// monitored for read events — either monitoring was not requested, the fd was
|
||||
// invalid, or esphome_lwip_get_sock() failed. Non-null means the netconn event
|
||||
// callback was hooked and notifications are flowing. close() nulls this to prevent
|
||||
// use-after-free via a recycled lwip slot.
|
||||
struct lwip_sock *cached_sock_{nullptr};
|
||||
#else
|
||||
bool loop_monitored_{false};
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace esphome::socket
|
||||
|
||||
@@ -14,38 +14,34 @@ LwIPSocketImpl::LwIPSocketImpl(int fd, bool monitor_loop) {
|
||||
if (!monitor_loop || this->fd_ < 0)
|
||||
return;
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
// Cache lwip_sock pointer and register for monitoring (hooks callback internally)
|
||||
this->cached_sock_ = esphome_lwip_get_sock(this->fd_);
|
||||
this->loop_monitored_ = App.register_socket(this->cached_sock_);
|
||||
this->cached_sock_ = hook_fd_for_fast_select(this->fd_);
|
||||
#else
|
||||
this->loop_monitored_ = App.register_socket_fd(this->fd_);
|
||||
#endif
|
||||
}
|
||||
|
||||
LwIPSocketImpl::~LwIPSocketImpl() {
|
||||
if (!this->closed_) {
|
||||
this->close();
|
||||
}
|
||||
}
|
||||
LwIPSocketImpl::~LwIPSocketImpl() { this->close(); }
|
||||
|
||||
int LwIPSocketImpl::close() {
|
||||
if (!this->closed_) {
|
||||
// Unregister before closing to avoid dangling pointer in monitored set
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
if (this->loop_monitored_) {
|
||||
App.unregister_socket(this->cached_sock_);
|
||||
this->cached_sock_ = nullptr;
|
||||
}
|
||||
#else
|
||||
if (this->loop_monitored_) {
|
||||
App.unregister_socket_fd(this->fd_);
|
||||
}
|
||||
#endif
|
||||
int ret = lwip_close(this->fd_);
|
||||
this->closed_ = true;
|
||||
return ret;
|
||||
if (this->fd_ < 0) {
|
||||
// Already closed, or never opened.
|
||||
return 0;
|
||||
}
|
||||
return 0;
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
// Null the cached lwip_sock pointer before closing. The underlying lwip slot can be
|
||||
// recycled for a new connection as soon as lwip_close() returns, so anything that
|
||||
// might dereference cached_sock_ post-close (e.g. setsockopt(TCP_NODELAY)) would
|
||||
// otherwise touch an unrelated socket's pcb. No per-socket callback unhook is needed —
|
||||
// all LwIP sockets share the same static event_callback.
|
||||
this->cached_sock_ = nullptr;
|
||||
#else
|
||||
if (this->loop_monitored_) {
|
||||
App.unregister_socket_fd(this->fd_);
|
||||
}
|
||||
#endif
|
||||
int ret = lwip_close(this->fd_);
|
||||
this->fd_ = -1; // Sentinel for "closed" — prevents double-close and makes use-after-close visible.
|
||||
return ret;
|
||||
}
|
||||
|
||||
int LwIPSocketImpl::setblocking(bool blocking) {
|
||||
|
||||
@@ -85,12 +85,21 @@ class LwIPSocketImpl {
|
||||
int get_fd() const { return this->fd_; }
|
||||
|
||||
protected:
|
||||
// fd_ < 0 means "not open" — used both pre-open (initial state) and post-close. This
|
||||
// replaces a separate closed_ flag: close() sets fd_ = -1 after lwip_close(), and the
|
||||
// destructor / double-close path just check fd_ < 0.
|
||||
int fd_{-1};
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
struct lwip_sock *cached_sock_{nullptr}; // Cached for direct rcvevent read in ready()
|
||||
#endif
|
||||
bool closed_{false};
|
||||
// Cached lwip_sock pointer used for direct rcvevent reads in ready() on the
|
||||
// fast-select path. Replaces loop_monitored_: null means this socket is not being
|
||||
// monitored for read events — either monitoring was not requested, the fd was
|
||||
// invalid, or esphome_lwip_get_sock() failed. Non-null means the netconn event
|
||||
// callback was hooked and notifications are flowing. close() nulls this to prevent
|
||||
// use-after-free via a recycled lwip slot.
|
||||
struct lwip_sock *cached_sock_{nullptr};
|
||||
#else
|
||||
bool loop_monitored_{false};
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace esphome::socket
|
||||
|
||||
@@ -42,8 +42,23 @@ using ListenSocket = LWIPRawListenImpl;
|
||||
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
/// Shared ready() helper using cached lwip_sock pointer for direct rcvevent read.
|
||||
inline bool socket_ready(struct lwip_sock *cached_sock, bool loop_monitored) {
|
||||
return !loop_monitored || (cached_sock != nullptr && esphome_lwip_socket_has_data(cached_sock));
|
||||
/// cached_sock == nullptr means the socket is not monitored (monitor_loop was false, fd
|
||||
/// was invalid, or esphome_lwip_get_sock() failed) — in that case return true so the
|
||||
/// caller attempts the read and handles blocking itself.
|
||||
inline bool socket_ready(struct lwip_sock *cached_sock) {
|
||||
return cached_sock == nullptr || esphome_lwip_socket_has_data(cached_sock);
|
||||
}
|
||||
|
||||
/// Resolve an fd to its lwip_sock and install the netconn event-callback hook so the
|
||||
/// main loop is woken by FreeRTOS task notifications when data arrives. Shared between
|
||||
/// BSD and LwIP socket impls on the fast-select path. Returns the cached lwip_sock
|
||||
/// pointer (or nullptr if the fd does not map to a valid lwip_sock).
|
||||
inline struct lwip_sock *hook_fd_for_fast_select(int fd) {
|
||||
struct lwip_sock *sock = esphome_lwip_get_sock(fd);
|
||||
if (sock != nullptr) {
|
||||
esphome_lwip_hook_socket(sock);
|
||||
}
|
||||
return sock;
|
||||
}
|
||||
#elif defined(USE_HOST)
|
||||
/// Shared ready() helper for fd-based socket implementations.
|
||||
@@ -69,7 +84,7 @@ bool socket_ready_fd(int fd, bool loop_monitored);
|
||||
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
||||
inline bool Socket::ready() const {
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
return socket_ready(this->cached_sock_, this->loop_monitored_);
|
||||
return socket_ready(this->cached_sock_);
|
||||
#else
|
||||
return socket_ready_fd(this->fd_, this->loop_monitored_);
|
||||
#endif
|
||||
|
||||
@@ -173,7 +173,7 @@ def _read_audio_file_and_type(file_config):
|
||||
raise cv.Invalid(
|
||||
f"Unable to determine audio file type of '{path}'. "
|
||||
f"Try re-encoding the file into a supported format. Details: {e}"
|
||||
)
|
||||
) from e
|
||||
|
||||
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["NONE"]
|
||||
if file_type in ("wav"):
|
||||
|
||||
@@ -45,8 +45,8 @@ MODELS = {
|
||||
presets={
|
||||
CONF_HEIGHT: 240,
|
||||
CONF_WIDTH: 135,
|
||||
CONF_OFFSET_HEIGHT: 52,
|
||||
CONF_OFFSET_WIDTH: 40,
|
||||
CONF_OFFSET_HEIGHT: 40,
|
||||
CONF_OFFSET_WIDTH: 52,
|
||||
CONF_CS_PIN: "GPIO5",
|
||||
CONF_DC_PIN: "GPIO16",
|
||||
CONF_RESET_PIN: "GPIO23",
|
||||
@@ -68,8 +68,8 @@ MODELS = {
|
||||
presets={
|
||||
CONF_HEIGHT: 280,
|
||||
CONF_WIDTH: 240,
|
||||
CONF_OFFSET_HEIGHT: 0,
|
||||
CONF_OFFSET_WIDTH: 20,
|
||||
CONF_OFFSET_HEIGHT: 20,
|
||||
CONF_OFFSET_WIDTH: 0,
|
||||
}
|
||||
),
|
||||
"ADAFRUIT_S2_TFT_FEATHER_240X135": model_spec(
|
||||
@@ -77,8 +77,8 @@ MODELS = {
|
||||
presets={
|
||||
CONF_HEIGHT: 240,
|
||||
CONF_WIDTH: 135,
|
||||
CONF_OFFSET_HEIGHT: 52,
|
||||
CONF_OFFSET_WIDTH: 40,
|
||||
CONF_OFFSET_HEIGHT: 40,
|
||||
CONF_OFFSET_WIDTH: 52,
|
||||
CONF_CS_PIN: "GPIO7",
|
||||
CONF_DC_PIN: "GPIO39",
|
||||
CONF_RESET_PIN: "GPIO40",
|
||||
@@ -89,8 +89,8 @@ MODELS = {
|
||||
presets={
|
||||
CONF_HEIGHT: 320,
|
||||
CONF_WIDTH: 170,
|
||||
CONF_OFFSET_HEIGHT: 35,
|
||||
CONF_OFFSET_WIDTH: 0,
|
||||
CONF_OFFSET_HEIGHT: 0,
|
||||
CONF_OFFSET_WIDTH: 35,
|
||||
CONF_ROTATION: 270,
|
||||
CONF_CS_PIN: "GPIO10",
|
||||
CONF_DC_PIN: "GPIO13",
|
||||
@@ -102,8 +102,8 @@ MODELS = {
|
||||
presets={
|
||||
CONF_HEIGHT: 320,
|
||||
CONF_WIDTH: 172,
|
||||
CONF_OFFSET_HEIGHT: 34,
|
||||
CONF_OFFSET_WIDTH: 0,
|
||||
CONF_OFFSET_HEIGHT: 0,
|
||||
CONF_OFFSET_WIDTH: 34,
|
||||
CONF_ROTATION: 90,
|
||||
CONF_CS_PIN: "GPIO21",
|
||||
CONF_DC_PIN: "GPIO22",
|
||||
|
||||
@@ -7,6 +7,11 @@ namespace status_led {
|
||||
|
||||
static const char *const TAG = "status_led";
|
||||
|
||||
static constexpr uint32_t ERROR_PERIOD_MS = 250;
|
||||
static constexpr uint32_t ERROR_ON_MS = 150;
|
||||
static constexpr uint32_t WARNING_PERIOD_MS = 1500;
|
||||
static constexpr uint32_t WARNING_ON_MS = 250;
|
||||
|
||||
StatusLED *global_status_led = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
StatusLED::StatusLED(GPIOPin *pin) : pin_(pin) { global_status_led = this; }
|
||||
@@ -19,12 +24,18 @@ void StatusLED::dump_config() {
|
||||
LOG_PIN(" Pin: ", this->pin_);
|
||||
}
|
||||
void StatusLED::loop() {
|
||||
if ((App.get_app_state() & STATUS_LED_ERROR) != 0u) {
|
||||
this->pin_->digital_write(millis() % 250u < 150u);
|
||||
} else if ((App.get_app_state() & STATUS_LED_WARNING) != 0u) {
|
||||
this->pin_->digital_write(millis() % 1500u < 250u);
|
||||
const uint32_t app_state = App.get_app_state();
|
||||
// Use millis() rather than App.get_loop_component_start_time() because this loop is also
|
||||
// dispatched from Application::feed_wdt() during long blocking operations, where the cached
|
||||
// per-component timestamp doesn't advance and would freeze the blink pattern.
|
||||
const uint32_t now = millis();
|
||||
if ((app_state & STATUS_LED_ERROR) != 0u) {
|
||||
this->pin_->digital_write(now % ERROR_PERIOD_MS < ERROR_ON_MS);
|
||||
} else if ((app_state & STATUS_LED_WARNING) != 0u) {
|
||||
this->pin_->digital_write(now % WARNING_PERIOD_MS < WARNING_ON_MS);
|
||||
} else {
|
||||
this->pin_->digital_write(false);
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
float StatusLED::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||
|
||||
@@ -35,8 +35,9 @@ def validate_acceleration(value):
|
||||
try:
|
||||
value = float(value)
|
||||
except ValueError:
|
||||
# pylint: disable=raise-missing-from
|
||||
raise cv.Invalid(f"Expected acceleration as floating point number, got {value}")
|
||||
raise cv.Invalid(
|
||||
f"Expected acceleration as floating point number, got {value}"
|
||||
) from None
|
||||
|
||||
if value <= 0:
|
||||
raise cv.Invalid("Acceleration must be larger than 0 steps/s^2!")
|
||||
@@ -55,8 +56,9 @@ def validate_speed(value):
|
||||
try:
|
||||
value = float(value)
|
||||
except ValueError:
|
||||
# pylint: disable=raise-missing-from
|
||||
raise cv.Invalid(f"Expected speed as floating point number, got {value}")
|
||||
raise cv.Invalid(
|
||||
f"Expected speed as floating point number, got {value}"
|
||||
) from None
|
||||
|
||||
if value <= 0:
|
||||
raise cv.Invalid("Speed must be larger than 0 steps/s!")
|
||||
|
||||
@@ -30,6 +30,56 @@ ErrList = list[tuple[UndefinedError, SubstitutionPath, Any]]
|
||||
jinja = Jinja()
|
||||
|
||||
|
||||
def raise_first_undefined(
|
||||
errors: ErrList,
|
||||
source: Any,
|
||||
context_label: str,
|
||||
) -> None:
|
||||
"""If *errors* is non-empty, raise ``cv.Invalid`` for the first undefined variable.
|
||||
|
||||
The raised error names the missing variable, the path walked into *source*
|
||||
(for nested dicts, e.g. ``url`` or ``ref``), and the YAML source location
|
||||
when *source* carries one. Only the first error is surfaced; the user will
|
||||
re-run after fixing it and any remaining undefined variables will be
|
||||
reported then.
|
||||
|
||||
``context_label`` is the noun describing where the undefined variable
|
||||
appeared (e.g. ``"package definition"``).
|
||||
"""
|
||||
if not errors:
|
||||
return
|
||||
err, err_path, err_value = errors[0]
|
||||
if len(errors) > 1:
|
||||
# Log any further undefined variables so debug-level output covers
|
||||
# the full set, even though only the first is surfaced to the user.
|
||||
extras = ", ".join(
|
||||
f"{e.message} at '{'->'.join(str(p) for p in p_path)}'"
|
||||
for e, p_path, _ in errors[1:]
|
||||
)
|
||||
_LOGGER.debug("Additional undefined variables in %s: %s", context_label, extras)
|
||||
# Prefer the location of the offending scalar (e.g. the `url:` value) over
|
||||
# the enclosing package-definition dict so the message points at the exact
|
||||
# line/column that carries the undefined variable.
|
||||
location_node = (
|
||||
err_value
|
||||
if isinstance(err_value, ESPHomeDataBase) and err_value.esp_range is not None
|
||||
else source
|
||||
)
|
||||
location = ""
|
||||
if (
|
||||
isinstance(location_node, ESPHomeDataBase)
|
||||
and location_node.esp_range is not None
|
||||
):
|
||||
mark = location_node.esp_range.start_mark
|
||||
# DocumentLocation.line/column are 0-based (from the YAML Mark). Render
|
||||
# as 1-based to match config.line_info() and editor line numbering.
|
||||
location = f" (in {mark.document} {mark.line + 1}:{mark.column + 1})"
|
||||
field = f" at '{'->'.join(str(p) for p in err_path)}'" if err_path else ""
|
||||
raise cv.Invalid(
|
||||
f"Undefined variable in {context_label}{field}: {err.message}{location}"
|
||||
)
|
||||
|
||||
|
||||
def validate_substitution_key(value: Any) -> str:
|
||||
"""Validate and normalize a substitution key, stripping a leading ``$`` if present."""
|
||||
value = cv.string(value)
|
||||
@@ -188,7 +238,7 @@ def _expand_substitutions(
|
||||
f"\nRelevant context:\n{err.context_trace_str()}"
|
||||
f"\nSee {'->'.join(str(x) for x in path)}",
|
||||
path,
|
||||
)
|
||||
) from err
|
||||
else:
|
||||
if isinstance(orig_value, ESPHomeDataBase):
|
||||
value = _restore_data_base(value, orig_value)
|
||||
@@ -414,6 +464,34 @@ def _warn_unresolved_variables(errors: ErrList) -> None:
|
||||
)
|
||||
|
||||
|
||||
def resolve_substitutions_block(
|
||||
substitutions: Any,
|
||||
command_line_substitutions: dict[str, Any] | None,
|
||||
) -> dict[str, Any]:
|
||||
"""Resolve a deferred ``substitutions: !include file.yaml`` and validate the shape.
|
||||
|
||||
The caller is responsible for wrapping the call in
|
||||
``cv.prepend_path(CONF_SUBSTITUTIONS)`` for error reporting.
|
||||
``command_line_substitutions`` seeds the filename context so
|
||||
``substitutions: !include ${var}.yaml`` can reference CLI-provided vars.
|
||||
"""
|
||||
if isinstance(substitutions, IncludeFile):
|
||||
# Single-shot resolution — matches ``_walk_packages`` for the
|
||||
# ``packages: !include`` entry point. Chained includes (an include that
|
||||
# itself loads another ``!include`` at the top level) are not supported.
|
||||
substitutions, _ = resolve_include(
|
||||
substitutions,
|
||||
[],
|
||||
ContextVars(command_line_substitutions or {}),
|
||||
strict_undefined=False,
|
||||
)
|
||||
if not isinstance(substitutions, dict):
|
||||
raise cv.Invalid(
|
||||
f"Substitutions must be a key to value mapping, got {type(substitutions)}"
|
||||
)
|
||||
return substitutions
|
||||
|
||||
|
||||
def do_substitution_pass(
|
||||
config: OrderedDict, command_line_substitutions: dict[str, Any] | None = None
|
||||
) -> OrderedDict:
|
||||
@@ -429,10 +507,9 @@ def do_substitution_pass(
|
||||
# Use merge_dicts_ordered to preserve OrderedDict type for move_to_end()
|
||||
substitutions = config.pop(CONF_SUBSTITUTIONS, {})
|
||||
with cv.prepend_path(CONF_SUBSTITUTIONS):
|
||||
if not isinstance(substitutions, dict):
|
||||
raise cv.Invalid(
|
||||
f"Substitutions must be a key to value mapping, got {type(substitutions)}"
|
||||
)
|
||||
substitutions = resolve_substitutions_block(
|
||||
substitutions, command_line_substitutions
|
||||
)
|
||||
substitutions = merge_dicts_ordered(
|
||||
substitutions, command_line_substitutions or {}
|
||||
)
|
||||
|
||||
@@ -200,11 +200,11 @@ CONFIG_SCHEMA = (
|
||||
cv.hex_int, cv.Range(min=0, max=0xFFFF)
|
||||
),
|
||||
cv.Optional(CONF_DEVIATION, default="5kHz"): cv.All(
|
||||
cv.frequency, cv.float_range(min=0, max=100000)
|
||||
cv.frequency, cv.int_range(min=0, max=100000)
|
||||
),
|
||||
cv.Required(CONF_DIO1_PIN): pins.gpio_input_pin_schema,
|
||||
cv.Required(CONF_FREQUENCY): cv.All(
|
||||
cv.frequency, cv.float_range(min=137.0e6, max=1020.0e6)
|
||||
cv.frequency, cv.int_range(min=int(137e6), max=int(1020e6))
|
||||
),
|
||||
cv.Required(CONF_HW_VERSION): cv.one_of(
|
||||
"sx1261", "sx1262", "sx1268", "llcc68", lower=True
|
||||
|
||||
@@ -197,11 +197,11 @@ CONFIG_SCHEMA = (
|
||||
cv.Optional(CONF_CODING_RATE, default="CR_4_5"): cv.enum(CODING_RATE),
|
||||
cv.Optional(CONF_CRC_ENABLE, default=False): cv.boolean,
|
||||
cv.Optional(CONF_DEVIATION, default="5kHz"): cv.All(
|
||||
cv.frequency, cv.float_range(min=0, max=100000)
|
||||
cv.frequency, cv.int_range(min=0, max=100000)
|
||||
),
|
||||
cv.Optional(CONF_DIO0_PIN): pins.internal_gpio_input_pin_schema,
|
||||
cv.Required(CONF_FREQUENCY): cv.All(
|
||||
cv.frequency, cv.float_range(min=137.0e6, max=1020.0e6)
|
||||
cv.frequency, cv.int_range(min=int(137e6), max=int(1020e6))
|
||||
),
|
||||
cv.Required(CONF_MODULATION): cv.enum(MOD),
|
||||
cv.Optional(CONF_ON_PACKET): automation.validate_automation(single=True),
|
||||
|
||||
@@ -109,8 +109,7 @@ def _parse_cron_int(value, special_mapping, message):
|
||||
try:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
# pylint: disable=raise-missing-from
|
||||
raise cv.Invalid(message.format(value))
|
||||
raise cv.Invalid(message.format(value)) from None
|
||||
|
||||
|
||||
def _parse_cron_part(part, min_value, max_value, special_mapping):
|
||||
@@ -134,10 +133,9 @@ def _parse_cron_part(part, min_value, max_value, special_mapping):
|
||||
try:
|
||||
repeat_n = int(repeat)
|
||||
except ValueError:
|
||||
# pylint: disable=raise-missing-from
|
||||
raise cv.Invalid(
|
||||
f"Repeat for '/' time expression must be an integer, got {repeat}"
|
||||
)
|
||||
) from None
|
||||
return set(range(offset_n, max_value + 1, repeat_n))
|
||||
if "-" in part:
|
||||
data = part.split("-")
|
||||
|
||||
@@ -347,6 +347,13 @@ uint8_t TM1637Display::print(uint8_t start_pos, const char *str) {
|
||||
}
|
||||
return pos - start_pos;
|
||||
}
|
||||
|
||||
void TM1637Display::set_brightness(float brightness) {
|
||||
auto intensity = clamp(brightness, 0.f, 1.f) * 7;
|
||||
this->set_on(intensity > 0);
|
||||
this->set_intensity(intensity);
|
||||
}
|
||||
|
||||
uint8_t TM1637Display::print(const char *str) { return this->print(0, str); }
|
||||
|
||||
void TM1637Display::set_buffer(const uint8_t *data, uint8_t length) {
|
||||
|
||||
@@ -50,6 +50,9 @@ class TM1637Display : public PollingComponent {
|
||||
/// Set raw buffer bytes from data array up to length bytes.
|
||||
void set_buffer(const uint8_t *data, uint8_t length);
|
||||
|
||||
/// Set the display brightness. Accepts a value between 0.0 and 1.0; 0 will turn off
|
||||
/// the display and 1.0 will set it to the maximum brightness.
|
||||
void set_brightness(float brightness);
|
||||
void set_intensity(uint8_t intensity) { this->intensity_ = intensity; }
|
||||
void set_inverted(bool inverted) { this->inverted_ = inverted; }
|
||||
void set_length(uint8_t length) { this->length_ = length; }
|
||||
|
||||
@@ -75,6 +75,9 @@ class ListEntitiesIterator final : public ComponentIterator {
|
||||
#ifdef USE_VALVE
|
||||
bool on_valve(valve::Valve *obj) override;
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
bool on_media_player(media_player::MediaPlayer *obj) override { return true; }
|
||||
#endif
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *obj) override;
|
||||
#endif
|
||||
|
||||
@@ -308,6 +308,7 @@ bool CompactString::operator==(const StringRef &other) const {
|
||||
/// │ - Roaming fail (RECONNECTING on other AP): counter preserved │
|
||||
/// └──────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_INFO
|
||||
// Use if-chain instead of switch to avoid jump table in RODATA (wastes RAM on ESP8266)
|
||||
static const LogString *retry_phase_to_log_string(WiFiRetryPhase phase) {
|
||||
if (phase == WiFiRetryPhase::INITIAL_CONNECT)
|
||||
@@ -326,6 +327,7 @@ static const LogString *retry_phase_to_log_string(WiFiRetryPhase phase) {
|
||||
return LOG_STR("RESTARTING");
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
#endif // ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_INFO
|
||||
|
||||
bool WiFiComponent::went_through_explicit_hidden_phase_() const {
|
||||
// If first configured network is marked hidden, we went through EXPLICIT_HIDDEN phase
|
||||
|
||||
@@ -67,7 +67,7 @@ def _validate_load_certificate(value):
|
||||
contents = read_relative_config_path(value)
|
||||
return wrapped_load_pem_x509_certificate(contents)
|
||||
except ValueError as err:
|
||||
raise cv.Invalid(f"Invalid certificate: {err}")
|
||||
raise cv.Invalid(f"Invalid certificate: {err}") from err
|
||||
|
||||
|
||||
def validate_certificate(value):
|
||||
@@ -86,9 +86,9 @@ def _validate_load_private_key(key, cert_pw):
|
||||
except ValueError as e:
|
||||
raise cv.Invalid(
|
||||
f"There was an error with the EAP 'password:' provided for 'key' {e}"
|
||||
)
|
||||
) from e
|
||||
except TypeError as e:
|
||||
raise cv.Invalid(f"There was an error with the EAP 'key:' provided: {e}")
|
||||
raise cv.Invalid(f"There was an error with the EAP 'key:' provided: {e}") from e
|
||||
|
||||
|
||||
def _check_private_key_cert_match(key, cert):
|
||||
|
||||
@@ -53,7 +53,7 @@ def _cidr_network(value):
|
||||
try:
|
||||
ipaddress.ip_network(value, strict=False)
|
||||
except ValueError as err:
|
||||
raise cv.Invalid(f"Invalid network in CIDR notation: {err}")
|
||||
raise cv.Invalid(f"Invalid network in CIDR notation: {err}") from err
|
||||
return value
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ async def to_code(config):
|
||||
# the '+1' modifier is relative to the device's own address that will
|
||||
# be automatically added to the provided list.
|
||||
cg.add_build_flag(f"-DCONFIG_WIREGUARD_MAX_SRC_IPS={len(allowed_ips) + 1}")
|
||||
cg.add_library("droscy/esp_wireguard", "0.4.4")
|
||||
cg.add_library("droscy/esp_wireguard", "0.4.5")
|
||||
|
||||
await cg.register_component(var, config)
|
||||
|
||||
|
||||
@@ -1,28 +1,35 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.zephyr import zephyr_add_prj_conf
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ESPHOME, CONF_ID, CONF_NAME, Framework
|
||||
import esphome.final_validate as fv
|
||||
from esphome.const import CONF_ID, Framework
|
||||
from esphome.core import CORE
|
||||
|
||||
zephyr_ble_server_ns = cg.esphome_ns.namespace("zephyr_ble_server")
|
||||
BLEServer = zephyr_ble_server_ns.class_("BLEServer", cg.Component)
|
||||
|
||||
CONF_ON_NUMERIC_COMPARISON_REQUEST = "on_numeric_comparison_request"
|
||||
CONF_ACCEPT = "accept"
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(BLEServer),
|
||||
cv.Optional(
|
||||
CONF_ON_NUMERIC_COMPARISON_REQUEST
|
||||
): automation.validate_automation({}),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
cv.only_with_framework(Framework.ZEPHYR),
|
||||
)
|
||||
|
||||
|
||||
def _final_validate(_):
|
||||
full_config = fv.full_config.get()
|
||||
zephyr_add_prj_conf("BT_DEVICE_NAME", full_config[CONF_ESPHOME][CONF_NAME])
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_NUMERIC_COMPARISON_REQUEST,
|
||||
"add_passkey_callback",
|
||||
[(cg.uint32, "passkey")],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
@@ -30,5 +37,39 @@ async def to_code(config):
|
||||
zephyr_add_prj_conf("BT", True)
|
||||
zephyr_add_prj_conf("BT_PERIPHERAL", True)
|
||||
zephyr_add_prj_conf("BT_RX_STACK_SIZE", 1536)
|
||||
# zephyr_add_prj_conf("BT_LL_SW_SPLIT", True)
|
||||
zephyr_add_prj_conf("BT_DEVICE_NAME", CORE.name)
|
||||
await cg.register_component(var, config)
|
||||
if config.get(CONF_ON_NUMERIC_COMPARISON_REQUEST):
|
||||
zephyr_add_prj_conf("BT_SMP", True)
|
||||
zephyr_add_prj_conf("BT_SETTINGS", True)
|
||||
zephyr_add_prj_conf("BT_SMP_SC_ONLY", True)
|
||||
zephyr_add_prj_conf("BT_KEYS_OVERWRITE_OLDEST", True)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
|
||||
BLENumericComparisonReplyAction = zephyr_ble_server_ns.class_(
|
||||
"BLENumericComparisonReplyAction", automation.Action
|
||||
)
|
||||
|
||||
BLE_NUMERIC_COMPARISON_REPLY_ACTION_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.use_id(BLEServer),
|
||||
cv.Required(CONF_ACCEPT): cv.templatable(cv.boolean),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"ble_server.numeric_comparison_reply",
|
||||
BLENumericComparisonReplyAction,
|
||||
BLE_NUMERIC_COMPARISON_REPLY_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def numeric_comparison_reply_to_code(config, action_id, template_arg, args):
|
||||
parent = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, parent)
|
||||
|
||||
templ = await cg.templatable(config[CONF_ACCEPT], args, cg.bool_)
|
||||
cg.add(var.set_accept(templ))
|
||||
|
||||
return var
|
||||
|
||||
@@ -3,32 +3,34 @@
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <zephyr/bluetooth/bluetooth.h>
|
||||
#include <zephyr/bluetooth/conn.h>
|
||||
#include <zephyr/settings/settings.h>
|
||||
|
||||
namespace esphome::zephyr_ble_server {
|
||||
|
||||
static const char *const TAG = "zephyr_ble_server";
|
||||
|
||||
static struct k_work advertise_work; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static k_work advertise_work; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
BLEServer *global_ble_server; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
#define DEVICE_NAME CONFIG_BT_DEVICE_NAME
|
||||
#define DEVICE_NAME_LEN (sizeof(DEVICE_NAME) - 1)
|
||||
|
||||
static const struct bt_data AD[] = {
|
||||
static const bt_data AD[] = {
|
||||
BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
|
||||
BT_DATA(BT_DATA_NAME_COMPLETE, DEVICE_NAME, DEVICE_NAME_LEN),
|
||||
};
|
||||
|
||||
static const struct bt_data SD[] = {
|
||||
static const bt_data SD[] = {
|
||||
#ifdef USE_OTA
|
||||
BT_DATA_BYTES(BT_DATA_UUID128_ALL, 0x84, 0xaa, 0x60, 0x74, 0x52, 0x8a, 0x8b, 0x86, 0xd3, 0x4c, 0xb7, 0x1d, 0x1d,
|
||||
0xdc, 0x53, 0x8d),
|
||||
#endif
|
||||
};
|
||||
|
||||
const struct bt_le_adv_param *const ADV_PARAM = BT_LE_ADV_CONN;
|
||||
const bt_le_adv_param *const ADV_PARAM = BT_LE_ADV_CONN;
|
||||
|
||||
static void advertise(struct k_work *work) {
|
||||
static void advertise(k_work *work) {
|
||||
int rc = bt_le_adv_stop();
|
||||
if (rc) {
|
||||
ESP_LOGE(TAG, "Advertising failed to stop (rc %d)", rc);
|
||||
@@ -42,57 +44,276 @@ static void advertise(struct k_work *work) {
|
||||
ESP_LOGI(TAG, "Advertising successfully started");
|
||||
}
|
||||
|
||||
static void connected(struct bt_conn *conn, uint8_t err) {
|
||||
void BLEServer::connected(bt_conn *conn, uint8_t err) {
|
||||
char addr[BT_ADDR_LE_STR_LEN];
|
||||
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Connection failed (err 0x%02x)", err);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Connected");
|
||||
ESP_LOGE(TAG, "Failed to connect to %s (%u)", addr, err);
|
||||
return;
|
||||
}
|
||||
ESP_LOGI(TAG, "Connected %s", addr);
|
||||
#ifdef CONFIG_BT_SMP
|
||||
if (bt_conn_set_security(conn, BT_SECURITY_L4)) {
|
||||
ESP_LOGE(TAG, "Failed to set security");
|
||||
}
|
||||
#endif
|
||||
conn = bt_conn_ref(conn);
|
||||
global_ble_server->defer([conn]() { global_ble_server->conn_ = conn; });
|
||||
}
|
||||
|
||||
static void disconnected(struct bt_conn *conn, uint8_t reason) {
|
||||
ESP_LOGI(TAG, "Disconnected (reason 0x%02x)", reason);
|
||||
void BLEServer::disconnected(bt_conn *conn, uint8_t reason) {
|
||||
char addr[BT_ADDR_LE_STR_LEN];
|
||||
|
||||
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
|
||||
|
||||
ESP_LOGI(TAG, "Disconnected from %s (reason 0x%02x)", addr, reason);
|
||||
global_ble_server->defer([]() {
|
||||
if (global_ble_server->conn_) {
|
||||
bt_conn_unref(global_ble_server->conn_);
|
||||
global_ble_server->conn_ = nullptr;
|
||||
}
|
||||
});
|
||||
k_work_submit(&advertise_work);
|
||||
}
|
||||
|
||||
static void bt_ready(int err) {
|
||||
if (err != 0) {
|
||||
ESP_LOGE(TAG, "Bluetooth failed to initialise: %d", err);
|
||||
#ifdef CONFIG_BT_SMP
|
||||
static void identity_resolved(bt_conn *conn, const bt_addr_le_t *rpa, const bt_addr_le_t *identity) {
|
||||
char addr_identity[BT_ADDR_LE_STR_LEN];
|
||||
char addr_rpa[BT_ADDR_LE_STR_LEN];
|
||||
|
||||
bt_addr_le_to_str(identity, addr_identity, sizeof(addr_identity));
|
||||
bt_addr_le_to_str(rpa, addr_rpa, sizeof(addr_rpa));
|
||||
|
||||
ESP_LOGD(TAG, "Identity resolved %s -> %s", addr_rpa, addr_identity);
|
||||
}
|
||||
|
||||
static void security_changed(bt_conn *conn, bt_security_t level, bt_security_err err) {
|
||||
char addr[BT_ADDR_LE_STR_LEN];
|
||||
|
||||
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
|
||||
|
||||
if (!err) {
|
||||
ESP_LOGD(TAG, "Security changed: %s level %u", addr, level);
|
||||
} else {
|
||||
k_work_submit(&advertise_work);
|
||||
ESP_LOGE(TAG, "Security failed: %s level %u err %d", addr, level, err);
|
||||
}
|
||||
}
|
||||
|
||||
BT_CONN_CB_DEFINE(conn_callbacks) = {
|
||||
.connected = connected,
|
||||
.disconnected = disconnected,
|
||||
};
|
||||
static void pairing_complete(bt_conn *conn, bool bonded) {
|
||||
char addr[BT_ADDR_LE_STR_LEN];
|
||||
|
||||
void BLEServer::setup() {
|
||||
k_work_init(&advertise_work, advertise);
|
||||
resume_();
|
||||
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
|
||||
|
||||
ESP_LOGD(TAG, "Pairing completed: %s, bonded: %d", addr, bonded);
|
||||
}
|
||||
|
||||
void BLEServer::loop() {
|
||||
if (this->suspended_) {
|
||||
resume_();
|
||||
this->suspended_ = false;
|
||||
}
|
||||
static void pairing_failed(bt_conn *conn, bt_security_err reason) {
|
||||
char addr[BT_ADDR_LE_STR_LEN];
|
||||
|
||||
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
|
||||
|
||||
ESP_LOGE(TAG, "Pairing failed conn: %s, reason %d", addr, reason);
|
||||
|
||||
bt_conn_disconnect(conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN);
|
||||
}
|
||||
|
||||
void BLEServer::resume_() {
|
||||
int rc = bt_enable(bt_ready);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(TAG, "Bluetooth enable failed: %d", rc);
|
||||
static void bond_deleted(uint8_t id, const bt_addr_le_t *peer) {
|
||||
char addr[BT_ADDR_LE_STR_LEN];
|
||||
|
||||
bt_addr_le_to_str(peer, addr, sizeof(addr));
|
||||
ESP_LOGD(TAG, "Bond deleted for %s, id %u", addr, id);
|
||||
}
|
||||
|
||||
static void auth_passkey_display(bt_conn *conn, unsigned int passkey) {
|
||||
char addr[BT_ADDR_LE_STR_LEN];
|
||||
char passkey_str[7];
|
||||
|
||||
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
|
||||
|
||||
snprintk(passkey_str, 7, "%06u", passkey);
|
||||
|
||||
ESP_LOGI(TAG, "Passkey for %s: %s", addr, passkey_str);
|
||||
}
|
||||
|
||||
static void conn_addr_str(bt_conn *conn, char *addr, size_t len) {
|
||||
struct bt_conn_info info;
|
||||
|
||||
if (bt_conn_get_info(conn, &info) < 0) {
|
||||
addr[0] = '\0';
|
||||
return;
|
||||
}
|
||||
|
||||
switch (info.type) {
|
||||
case BT_CONN_TYPE_LE:
|
||||
bt_addr_le_to_str(info.le.dst, addr, len);
|
||||
break;
|
||||
default:
|
||||
ESP_LOGE(TAG, "Not implemented");
|
||||
addr[0] = '\0';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void BLEServer::on_shutdown() {
|
||||
struct k_work_sync sync;
|
||||
k_work_cancel_sync(&advertise_work, &sync);
|
||||
bt_disable();
|
||||
this->suspended_ = true;
|
||||
static void auth_cancel(bt_conn *conn) {
|
||||
char addr[BT_ADDR_LE_STR_LEN];
|
||||
|
||||
conn_addr_str(conn, addr, sizeof(addr));
|
||||
|
||||
ESP_LOGI(TAG, "Pairing cancelled: %s", addr);
|
||||
}
|
||||
|
||||
void BLEServer::auth_passkey_confirm(bt_conn *conn, unsigned int passkey) {
|
||||
char addr[BT_ADDR_LE_STR_LEN];
|
||||
char passkey_str[7];
|
||||
|
||||
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
|
||||
|
||||
snprintk(passkey_str, 7, "%06u", passkey);
|
||||
|
||||
ESP_LOGI(TAG, "Confirm passkey for %s: %s", addr, passkey_str);
|
||||
global_ble_server->defer([passkey]() { global_ble_server->passkey_cb_(passkey); });
|
||||
}
|
||||
|
||||
static void auth_pairing_confirm(bt_conn *conn) {
|
||||
/* Automatically confirm pairing request from the device side. */
|
||||
auto err = bt_conn_auth_pairing_confirm(conn);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Can't confirm pairing (err: %d)", err);
|
||||
return;
|
||||
}
|
||||
|
||||
char addr[BT_ADDR_LE_STR_LEN];
|
||||
|
||||
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
|
||||
|
||||
ESP_LOGI(TAG, "Pairing confirmed: %s", addr);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
void BLEServer::setup() {
|
||||
global_ble_server = this;
|
||||
int err = 0;
|
||||
k_work_init(&advertise_work, advertise);
|
||||
|
||||
static bt_conn_cb conn_callbacks = {
|
||||
.connected = connected,
|
||||
.disconnected = disconnected,
|
||||
#ifdef CONFIG_BT_SMP
|
||||
.identity_resolved = identity_resolved,
|
||||
.security_changed = security_changed,
|
||||
#endif
|
||||
};
|
||||
|
||||
bt_conn_cb_register(&conn_callbacks);
|
||||
#ifdef CONFIG_BT_SMP
|
||||
static struct bt_conn_auth_info_cb conn_auth_info_callbacks = {
|
||||
.pairing_complete = pairing_complete, .pairing_failed = pairing_failed, .bond_deleted = bond_deleted};
|
||||
err = bt_conn_auth_info_cb_register(&conn_auth_info_callbacks);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to register authorization info callbacks.");
|
||||
}
|
||||
static struct bt_conn_auth_cb auth_cb = {
|
||||
.passkey_display = auth_passkey_display,
|
||||
.passkey_confirm = auth_passkey_confirm,
|
||||
.cancel = auth_cancel,
|
||||
.pairing_confirm = auth_pairing_confirm,
|
||||
};
|
||||
err = bt_conn_auth_cb_register(&auth_cb);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to set auth handlers (%d)", err);
|
||||
}
|
||||
#endif
|
||||
// callback cannot be used to start scanning due to race conditions with BT_SETTINGS
|
||||
err = bt_enable(nullptr);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Bluetooth enable failed: %d", err);
|
||||
return;
|
||||
}
|
||||
#ifdef CONFIG_BT_SETTINGS
|
||||
err = settings_load();
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Cannot load settings, err: %d", err);
|
||||
}
|
||||
#endif
|
||||
k_work_submit(&advertise_work);
|
||||
}
|
||||
|
||||
#ifdef ESPHOME_LOG_HAS_DEBUG
|
||||
static const char *role_str(uint8_t role) {
|
||||
switch (role) {
|
||||
case BT_CONN_ROLE_CENTRAL:
|
||||
return "Central";
|
||||
case BT_CONN_ROLE_PERIPHERAL:
|
||||
return "Peripheral";
|
||||
}
|
||||
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
static void connection_info(bt_conn *conn, void *user_data) {
|
||||
char addr[BT_ADDR_LE_STR_LEN];
|
||||
struct bt_conn_info info;
|
||||
|
||||
if (bt_conn_get_info(conn, &info) < 0) {
|
||||
ESP_LOGE(TAG, "Unable to get info: conn %p", conn);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (info.type) {
|
||||
case BT_CONN_TYPE_LE:
|
||||
bt_addr_le_to_str(info.le.dst, addr, sizeof(addr));
|
||||
ESP_LOGD(TAG, " %u [LE][%s] %s: Interval %u latency %u timeout %u security L%u", info.id, role_str(info.role),
|
||||
addr, info.le.interval, info.le.latency, info.le.timeout, info.security.level);
|
||||
break;
|
||||
default:
|
||||
ESP_LOGE(TAG, "Not implemented");
|
||||
break;
|
||||
}
|
||||
}
|
||||
#ifdef CONFIG_BT_BONDABLE
|
||||
static void bond_info(const struct bt_bond_info *info, void *user_data) {
|
||||
char addr[BT_ADDR_LE_STR_LEN];
|
||||
|
||||
bt_addr_le_to_str(&info->addr, addr, sizeof(addr));
|
||||
ESP_LOGD(TAG, " Bond remote identity: %s", addr);
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
void BLEServer::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"ble server:\n"
|
||||
" connected: %s\n"
|
||||
" name: %s\n"
|
||||
" appearance: %u\n"
|
||||
" ready: %s\n"
|
||||
#ifdef CONFIG_BT_SMP
|
||||
" security manager: YES",
|
||||
#else
|
||||
" security manager: NO",
|
||||
#endif
|
||||
YESNO(this->conn_), bt_get_name(), bt_get_appearance(), YESNO(bt_is_ready()));
|
||||
|
||||
#ifdef ESPHOME_LOG_HAS_DEBUG
|
||||
bt_conn_foreach(BT_CONN_TYPE_ALL, connection_info, nullptr);
|
||||
#ifdef CONFIG_BT_BONDABLE
|
||||
bt_foreach_bond(BT_ID_DEFAULT, bond_info, nullptr);
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
void BLEServer::numeric_comparison_reply(bool accept) {
|
||||
if (this->conn_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Not connected");
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Numeric comparison %s", accept ? "accepted" : "rejected");
|
||||
if (accept) {
|
||||
bt_conn_auth_passkey_confirm(this->conn_);
|
||||
} else {
|
||||
bt_conn_auth_cancel(this->conn_);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::zephyr_ble_server
|
||||
|
||||
@@ -1,18 +1,36 @@
|
||||
#pragma once
|
||||
#ifdef USE_ZEPHYR
|
||||
#include "esphome/core/component.h"
|
||||
#include <zephyr/bluetooth/conn.h>
|
||||
#include "esphome/core/automation.h"
|
||||
|
||||
namespace esphome::zephyr_ble_server {
|
||||
|
||||
class BLEServer : public Component {
|
||||
public:
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void on_shutdown() override;
|
||||
void dump_config() override;
|
||||
template<typename F> void add_passkey_callback(F &&callback) { this->passkey_cb_.add(std::forward<F>(callback)); }
|
||||
void numeric_comparison_reply(bool accept);
|
||||
|
||||
protected:
|
||||
void resume_();
|
||||
bool suspended_ = false;
|
||||
static void connected(bt_conn *conn, uint8_t err);
|
||||
static void disconnected(bt_conn *conn, uint8_t reason);
|
||||
static void auth_passkey_confirm(bt_conn *conn, unsigned int passkey);
|
||||
bt_conn *conn_{};
|
||||
CallbackManager<void(uint32_t)> passkey_cb_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class BLENumericComparisonReplyAction : public Action<Ts...> {
|
||||
public:
|
||||
explicit BLENumericComparisonReplyAction(BLEServer *parent) : parent_(parent) {}
|
||||
|
||||
TEMPLATABLE_VALUE(bool, accept)
|
||||
|
||||
void play(const Ts &...x) override { this->parent_->numeric_comparison_reply(this->accept_.value(x...)); }
|
||||
|
||||
protected:
|
||||
BLEServer *parent_;
|
||||
};
|
||||
|
||||
} // namespace esphome::zephyr_ble_server
|
||||
|
||||
@@ -544,8 +544,9 @@ def int_(value):
|
||||
try:
|
||||
return int(value, base)
|
||||
except ValueError:
|
||||
# pylint: disable=raise-missing-from
|
||||
raise Invalid(f"Expected integer, but cannot parse {value} as an integer")
|
||||
raise Invalid(
|
||||
f"Expected integer, but cannot parse {value} as an integer"
|
||||
) from None
|
||||
|
||||
|
||||
def int_range(min=None, max=None, min_included=True, max_included=True):
|
||||
@@ -844,8 +845,7 @@ def time_period_str_colon(value):
|
||||
try:
|
||||
parsed = [int(x) for x in value.split(":")]
|
||||
except ValueError:
|
||||
# pylint: disable=raise-missing-from
|
||||
raise Invalid(TIME_PERIOD_ERROR.format(value))
|
||||
raise Invalid(TIME_PERIOD_ERROR.format(value)) from None
|
||||
|
||||
if len(parsed) == 2:
|
||||
hour, minute = parsed
|
||||
@@ -943,7 +943,26 @@ def time_period_in_minutes_(value):
|
||||
def update_interval(value):
|
||||
if value == "never":
|
||||
return TimePeriodMilliseconds(milliseconds=SCHEDULER_DONT_RUN)
|
||||
return positive_time_period_milliseconds(value)
|
||||
result = positive_time_period_milliseconds(value)
|
||||
# 0ms was historically (mis)used as a pseudo-loop() mechanism for
|
||||
# PollingComponents. Under the hood it calls set_interval(0), which
|
||||
# causes Scheduler::call() to spin (WDT reset in the field). Coerce
|
||||
# to 1ms so existing configs keep working at ~1kHz instead of
|
||||
# spinning. Don't hard-fail so configs don't break on upgrade;
|
||||
# authors should migrate to HighFrequencyLoopRequester (C++) for
|
||||
# true run-every-loop behaviour.
|
||||
if result.total_milliseconds == 0:
|
||||
_LOGGER.warning(
|
||||
"update_interval of 0ms is not supported - coercing to 1ms. "
|
||||
"A literal 0ms schedule would spin the main loop (the scheduled "
|
||||
"item would always be due, so the scheduler would never yield "
|
||||
"back) and trigger a watchdog reset. Set update_interval to a "
|
||||
"non-zero value such as 1ms or higher. (Custom C++ components "
|
||||
"that need true run-every-loop behaviour should override loop() "
|
||||
"with HighFrequencyLoopRequester instead.)"
|
||||
)
|
||||
return TimePeriodMilliseconds(milliseconds=1)
|
||||
return result
|
||||
|
||||
|
||||
time_period = Any(time_period_str_unit, time_period_str_colon, time_period_dict)
|
||||
@@ -1047,8 +1066,7 @@ def date_time(date: bool, time: bool):
|
||||
try:
|
||||
date_obj = datetime.strptime(value, format)
|
||||
except ValueError as err:
|
||||
# pylint: disable=raise-missing-from
|
||||
raise Invalid(f"Invalid {exc_message}: {err}")
|
||||
raise Invalid(f"Invalid {exc_message}: {err}") from err
|
||||
|
||||
return_value = {}
|
||||
if date:
|
||||
@@ -1078,8 +1096,9 @@ def mac_address(value):
|
||||
try:
|
||||
parts_int.append(int(part, 16))
|
||||
except ValueError:
|
||||
# pylint: disable=raise-missing-from
|
||||
raise Invalid("MAC Address parts must be hexadecimal values from 00 to FF")
|
||||
raise Invalid(
|
||||
"MAC Address parts must be hexadecimal values from 00 to FF"
|
||||
) from None
|
||||
|
||||
return core.MACAddress(*parts_int)
|
||||
|
||||
@@ -1096,8 +1115,7 @@ def bind_key(value, *, name="Bind key"):
|
||||
try:
|
||||
parts_int.append(int(part, 16))
|
||||
except ValueError:
|
||||
# pylint: disable=raise-missing-from
|
||||
raise Invalid(f"{name} must be hex values from 00 to FF")
|
||||
raise Invalid(f"{name} must be hex values from 00 to FF") from None
|
||||
|
||||
return "".join(f"{part:02X}" for part in parts_int)
|
||||
|
||||
@@ -1425,8 +1443,7 @@ def mqtt_qos(value):
|
||||
try:
|
||||
value = int(value)
|
||||
except (TypeError, ValueError):
|
||||
# pylint: disable=raise-missing-from
|
||||
raise Invalid(f"MQTT Quality of Service must be integer, got {value}")
|
||||
raise Invalid(f"MQTT Quality of Service must be integer, got {value}") from None
|
||||
return one_of(0, 1, 2)(value)
|
||||
|
||||
|
||||
@@ -1518,8 +1535,7 @@ def _parse_percentage(value: object) -> float:
|
||||
else:
|
||||
value = float(value)
|
||||
except ValueError:
|
||||
# pylint: disable=raise-missing-from
|
||||
raise Invalid("invalid number")
|
||||
raise Invalid("invalid number") from None
|
||||
try:
|
||||
if not has_percent_sign and (value > 1 or value < -1):
|
||||
raise Invalid(
|
||||
@@ -1527,9 +1543,7 @@ def _parse_percentage(value: object) -> float:
|
||||
"outside -1.0 to 1.0. Please put a percent sign after the number!"
|
||||
)
|
||||
except TypeError:
|
||||
raise Invalid( # pylint: disable=raise-missing-from
|
||||
"Expected percentage or float"
|
||||
)
|
||||
raise Invalid("Expected percentage or float") from None
|
||||
return float(value)
|
||||
|
||||
|
||||
@@ -1702,8 +1716,7 @@ def dimensions(value):
|
||||
try:
|
||||
width, height = int(value[0]), int(value[1])
|
||||
except ValueError:
|
||||
# pylint: disable=raise-missing-from
|
||||
raise Invalid("Width and height dimensions must be integers")
|
||||
raise Invalid("Width and height dimensions must be integers") from None
|
||||
if width <= 0 or height <= 0:
|
||||
raise Invalid("Width and height must at least be 1")
|
||||
return [width, height]
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
#include "esphome/core/alloc_helpers.h"
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
namespace esphome {
|
||||
|
||||
// --- String helpers ---
|
||||
|
||||
std::string str_truncate(const std::string &str, size_t length) {
|
||||
return str.length() > length ? str.substr(0, length) : str;
|
||||
}
|
||||
|
||||
std::string str_until(const char *str, char ch) {
|
||||
const char *pos = strchr(str, ch);
|
||||
return pos == nullptr ? std::string(str) : std::string(str, pos - str);
|
||||
}
|
||||
std::string str_until(const std::string &str, char ch) { return str.substr(0, str.find(ch)); }
|
||||
|
||||
// wrapper around std::transform to run safely on functions from the ctype.h header
|
||||
// see https://en.cppreference.com/w/cpp/string/byte/toupper#Notes
|
||||
template<int (*fn)(int)> std::string str_ctype_transform(const std::string &str) {
|
||||
std::string result;
|
||||
result.resize(str.length());
|
||||
std::transform(str.begin(), str.end(), result.begin(), [](unsigned char ch) { return fn(ch); });
|
||||
return result;
|
||||
}
|
||||
std::string str_lower_case(const std::string &str) { return str_ctype_transform<std::tolower>(str); }
|
||||
|
||||
std::string str_upper_case(const std::string &str) {
|
||||
std::string result;
|
||||
result.resize(str.length());
|
||||
std::transform(str.begin(), str.end(), result.begin(), [](unsigned char ch) { return std::toupper(ch); });
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string str_snake_case(const std::string &str) {
|
||||
std::string result = str;
|
||||
for (char &c : result) {
|
||||
c = to_snake_case_char(c);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string str_sanitize(const std::string &str) {
|
||||
std::string result;
|
||||
result.resize(str.size());
|
||||
str_sanitize_to(&result[0], str.size() + 1, str.c_str());
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string str_snprintf(const char *fmt, size_t len, ...) {
|
||||
std::string str;
|
||||
va_list args;
|
||||
|
||||
str.resize(len);
|
||||
va_start(args, len);
|
||||
size_t out_length = vsnprintf(&str[0], len + 1, fmt, args);
|
||||
va_end(args);
|
||||
|
||||
if (out_length < len)
|
||||
str.resize(out_length);
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
std::string str_sprintf(const char *fmt, ...) {
|
||||
std::string str;
|
||||
va_list args;
|
||||
|
||||
va_start(args, fmt);
|
||||
size_t length = vsnprintf(nullptr, 0, fmt, args);
|
||||
va_end(args);
|
||||
|
||||
str.resize(length);
|
||||
va_start(args, fmt);
|
||||
vsnprintf(&str[0], length + 1, fmt, args);
|
||||
va_end(args);
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
// --- Value formatting helpers ---
|
||||
|
||||
std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) {
|
||||
char buf[VALUE_ACCURACY_MAX_LEN];
|
||||
value_accuracy_to_buf(buf, value, accuracy_decimals);
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
// --- Base64 helpers ---
|
||||
|
||||
static constexpr const char *BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
"abcdefghijklmnopqrstuvwxyz"
|
||||
"0123456789+/";
|
||||
|
||||
// Encode 3 input bytes to 4 base64 characters, append 'count' to ret.
|
||||
static inline void base64_encode_triple(const char *char_array_3, int count, std::string &ret) {
|
||||
char char_array_4[4];
|
||||
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
|
||||
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
|
||||
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
|
||||
char_array_4[3] = char_array_3[2] & 0x3f;
|
||||
|
||||
for (int j = 0; j < count; j++)
|
||||
ret += BASE64_CHARS[static_cast<uint8_t>(char_array_4[j])];
|
||||
}
|
||||
|
||||
std::string base64_encode(const std::vector<uint8_t> &buf) { return base64_encode(buf.data(), buf.size()); }
|
||||
|
||||
std::string base64_encode(const uint8_t *buf, size_t buf_len) {
|
||||
std::string ret;
|
||||
int i = 0;
|
||||
char char_array_3[3];
|
||||
|
||||
while (buf_len--) {
|
||||
char_array_3[i++] = *(buf++);
|
||||
if (i == 3) {
|
||||
base64_encode_triple(char_array_3, 4, ret);
|
||||
i = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (i) {
|
||||
for (int j = i; j < 3; j++)
|
||||
char_array_3[j] = '\0';
|
||||
|
||||
base64_encode_triple(char_array_3, i + 1, ret);
|
||||
|
||||
while ((i++ < 3))
|
||||
ret += '=';
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> base64_decode(const std::string &encoded_string) {
|
||||
// Calculate maximum decoded size: every 4 base64 chars = 3 bytes
|
||||
size_t max_len = ((encoded_string.size() + 3) / 4) * 3;
|
||||
std::vector<uint8_t> ret(max_len);
|
||||
size_t actual_len = base64_decode(encoded_string, ret.data(), max_len);
|
||||
ret.resize(actual_len);
|
||||
return ret;
|
||||
}
|
||||
|
||||
// --- Hex/binary formatting helpers ---
|
||||
|
||||
std::string format_mac_address_pretty(const uint8_t *mac) {
|
||||
char buf[18];
|
||||
format_mac_addr_upper(mac, buf);
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
std::string format_hex(const uint8_t *data, size_t length) {
|
||||
std::string ret;
|
||||
ret.resize(length * 2);
|
||||
format_hex_to(&ret[0], length * 2 + 1, data, length);
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::string format_hex(const std::vector<uint8_t> &data) { return format_hex(data.data(), data.size()); }
|
||||
|
||||
// Shared implementation for uint8_t and string hex pretty formatting
|
||||
static std::string format_hex_pretty_uint8(const uint8_t *data, size_t length, char separator, bool show_length) {
|
||||
if (data == nullptr || length == 0)
|
||||
return "";
|
||||
std::string ret;
|
||||
size_t hex_len = separator ? (length * 3 - 1) : (length * 2);
|
||||
ret.resize(hex_len);
|
||||
format_hex_pretty_to(&ret[0], hex_len + 1, data, length, separator);
|
||||
if (show_length && length > 4)
|
||||
return ret + " (" + std::to_string(length) + ")";
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::string format_hex_pretty(const uint8_t *data, size_t length, char separator, bool show_length) {
|
||||
return format_hex_pretty_uint8(data, length, separator, show_length);
|
||||
}
|
||||
std::string format_hex_pretty(const std::vector<uint8_t> &data, char separator, bool show_length) {
|
||||
return format_hex_pretty(data.data(), data.size(), separator, show_length);
|
||||
}
|
||||
|
||||
std::string format_hex_pretty(const uint16_t *data, size_t length, char separator, bool show_length) {
|
||||
if (data == nullptr || length == 0)
|
||||
return "";
|
||||
std::string ret;
|
||||
size_t hex_len = separator ? (length * 5 - 1) : (length * 4);
|
||||
ret.resize(hex_len);
|
||||
format_hex_pretty_to(&ret[0], hex_len + 1, data, length, separator);
|
||||
if (show_length && length > 4)
|
||||
return ret + " (" + std::to_string(length) + ")";
|
||||
return ret;
|
||||
}
|
||||
std::string format_hex_pretty(const std::vector<uint16_t> &data, char separator, bool show_length) {
|
||||
return format_hex_pretty(data.data(), data.size(), separator, show_length);
|
||||
}
|
||||
std::string format_hex_pretty(const std::string &data, char separator, bool show_length) {
|
||||
return format_hex_pretty_uint8(reinterpret_cast<const uint8_t *>(data.data()), data.length(), separator, show_length);
|
||||
}
|
||||
|
||||
std::string format_bin(const uint8_t *data, size_t length) {
|
||||
std::string result;
|
||||
result.resize(length * 8);
|
||||
format_bin_to(&result[0], length * 8 + 1, data, length);
|
||||
return result;
|
||||
}
|
||||
|
||||
// --- MAC address helpers ---
|
||||
|
||||
std::string get_mac_address() {
|
||||
uint8_t mac[6];
|
||||
get_mac_address_raw(mac);
|
||||
char buf[13];
|
||||
format_mac_addr_lower_no_sep(mac, buf);
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
std::string get_mac_address_pretty() {
|
||||
char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
return std::string(get_mac_address_pretty_into_buffer(buf));
|
||||
}
|
||||
|
||||
} // namespace esphome
|
||||
@@ -0,0 +1,128 @@
|
||||
#pragma once
|
||||
|
||||
/// @file alloc_helpers.h
|
||||
/// @brief Heap-allocating helper functions.
|
||||
///
|
||||
/// These functions return std::string and allocate heap memory on every call.
|
||||
/// On long-running embedded devices, repeated heap allocations fragment memory
|
||||
/// over time, eventually causing crashes even with free memory available.
|
||||
///
|
||||
/// Prefer the stack-based alternatives documented on each function instead.
|
||||
/// New code should avoid using these functions.
|
||||
|
||||
#include <cstdarg>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome {
|
||||
|
||||
// --- String helpers (allocating) ---
|
||||
|
||||
/// Truncate a string to a specific length.
|
||||
/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices.
|
||||
std::string str_truncate(const std::string &str, size_t length);
|
||||
|
||||
/// Extract the part of the string until either the first occurrence of the specified character, or the end
|
||||
/// (requires str to be null-terminated).
|
||||
/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices.
|
||||
std::string str_until(const char *str, char ch);
|
||||
/// Extract the part of the string until either the first occurrence of the specified character, or the end.
|
||||
/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices.
|
||||
std::string str_until(const std::string &str, char ch);
|
||||
|
||||
/// Convert the string to lower case.
|
||||
/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices.
|
||||
std::string str_lower_case(const std::string &str);
|
||||
|
||||
/// Convert the string to upper case.
|
||||
/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices.
|
||||
std::string str_upper_case(const std::string &str);
|
||||
|
||||
/// Convert the string to snake case (lowercase with underscores).
|
||||
/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices.
|
||||
std::string str_snake_case(const std::string &str);
|
||||
|
||||
/// Sanitizes the input string by removing all characters but alphanumerics, dashes and underscores.
|
||||
/// @warning Allocates heap memory. Use str_sanitize_to() with a stack buffer instead.
|
||||
std::string str_sanitize(const std::string &str);
|
||||
|
||||
/// snprintf-like function returning std::string of maximum length \p len (excluding null terminator).
|
||||
/// @warning Allocates heap memory. Use snprintf() with a stack buffer instead.
|
||||
std::string __attribute__((format(printf, 1, 3))) str_snprintf(const char *fmt, size_t len, ...);
|
||||
|
||||
/// sprintf-like function returning std::string.
|
||||
/// @warning Allocates heap memory. Use snprintf() with a stack buffer instead.
|
||||
std::string __attribute__((format(printf, 1, 2))) str_sprintf(const char *fmt, ...);
|
||||
|
||||
// --- Hex/binary formatting helpers (allocating) ---
|
||||
|
||||
/// Format the six-byte array \p mac into a MAC address string.
|
||||
/// @warning Allocates heap memory. Use format_mac_addr_upper() with a stack buffer instead.
|
||||
std::string format_mac_address_pretty(const uint8_t mac[6]);
|
||||
|
||||
/// Format the byte array \p data of length \p len in lowercased hex.
|
||||
/// @warning Allocates heap memory. Use format_hex_to() with a stack buffer instead.
|
||||
std::string format_hex(const uint8_t *data, size_t length);
|
||||
|
||||
/// Format the vector \p data in lowercased hex.
|
||||
/// @warning Allocates heap memory. Use format_hex_to() with a stack buffer instead.
|
||||
std::string format_hex(const std::vector<uint8_t> &data);
|
||||
|
||||
/// Format a byte array in pretty-printed, human-readable hex format.
|
||||
/// @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
|
||||
std::string format_hex_pretty(const uint8_t *data, size_t length, char separator = '.', bool show_length = true);
|
||||
|
||||
/// Format a 16-bit word array in pretty-printed, human-readable hex format.
|
||||
/// @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
|
||||
std::string format_hex_pretty(const uint16_t *data, size_t length, char separator = '.', bool show_length = true);
|
||||
|
||||
/// Format a byte vector in pretty-printed, human-readable hex format.
|
||||
/// @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
|
||||
std::string format_hex_pretty(const std::vector<uint8_t> &data, char separator = '.', bool show_length = true);
|
||||
|
||||
/// Format a 16-bit word vector in pretty-printed, human-readable hex format.
|
||||
/// @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
|
||||
std::string format_hex_pretty(const std::vector<uint16_t> &data, char separator = '.', bool show_length = true);
|
||||
|
||||
/// Format a string's bytes in pretty-printed, human-readable hex format.
|
||||
/// @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
|
||||
std::string format_hex_pretty(const std::string &data, char separator = '.', bool show_length = true);
|
||||
|
||||
/// Format the byte array \p data of length \p len in binary.
|
||||
/// @warning Allocates heap memory. Use format_bin_to() with a stack buffer instead.
|
||||
std::string format_bin(const uint8_t *data, size_t length);
|
||||
|
||||
// --- Value formatting helpers (allocating) ---
|
||||
|
||||
/// Format a float value with accuracy decimals to a string.
|
||||
/// @deprecated Allocates heap memory. Use value_accuracy_to_buf() instead. Removed in 2026.7.0.
|
||||
__attribute__((deprecated("Allocates heap memory. Use value_accuracy_to_buf() instead. Removed in 2026.7.0.")))
|
||||
std::string
|
||||
value_accuracy_to_string(float value, int8_t accuracy_decimals);
|
||||
|
||||
// --- Base64 helpers (allocating) ---
|
||||
|
||||
/// Encode a byte buffer to base64 string.
|
||||
/// @warning Allocates heap memory.
|
||||
std::string base64_encode(const uint8_t *buf, size_t buf_len);
|
||||
/// Encode a byte vector to base64 string.
|
||||
/// @warning Allocates heap memory.
|
||||
std::string base64_encode(const std::vector<uint8_t> &buf);
|
||||
|
||||
/// Decode a base64 string to a byte vector.
|
||||
/// @warning Allocates heap memory. Use base64_decode(data, len, buf, buf_len) with a pre-allocated buffer instead.
|
||||
std::vector<uint8_t> base64_decode(const std::string &encoded_string);
|
||||
|
||||
// --- MAC address helpers (allocating) ---
|
||||
|
||||
/// Get the device MAC address as a string, in lowercase hex notation.
|
||||
/// @warning Allocates heap memory. Use get_mac_address_into_buffer() instead.
|
||||
std::string get_mac_address();
|
||||
|
||||
/// Get the device MAC address as a string, in colon-separated uppercase hex notation.
|
||||
/// @warning Allocates heap memory. Use get_mac_address_pretty_into_buffer() instead.
|
||||
std::string get_mac_address_pretty();
|
||||
|
||||
} // namespace esphome
|
||||
@@ -211,11 +211,16 @@ void Application::process_dump_config_() {
|
||||
|
||||
void Application::feed_wdt() {
|
||||
// Cold entry: callers without a millis() timestamp in hand. Fetches the
|
||||
// time and takes the same rate-limit path as feed_wdt_with_time().
|
||||
// time and takes the same rate-limit paths as feed_wdt_with_time().
|
||||
uint32_t now = millis();
|
||||
if (now - this->last_wdt_feed_ > WDT_FEED_INTERVAL_MS) {
|
||||
this->feed_wdt_slow_(now);
|
||||
}
|
||||
#ifdef USE_STATUS_LED
|
||||
if (now - this->last_status_led_service_ > STATUS_LED_DISPATCH_INTERVAL_MS) {
|
||||
this->service_status_led_slow_(now);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void HOT Application::feed_wdt_slow_(uint32_t time) {
|
||||
@@ -223,13 +228,36 @@ void HOT Application::feed_wdt_slow_(uint32_t time) {
|
||||
// confirmed the WDT_FEED_INTERVAL_MS rate limit was exceeded.
|
||||
arch_feed_wdt();
|
||||
this->last_wdt_feed_ = time;
|
||||
#ifdef USE_STATUS_LED
|
||||
if (status_led::global_status_led != nullptr) {
|
||||
status_led::global_status_led->call();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_STATUS_LED
|
||||
void HOT Application::service_status_led_slow_(uint32_t time) {
|
||||
// Callers (feed_wdt(), feed_wdt_with_time()) have already confirmed the
|
||||
// STATUS_LED_DISPATCH_INTERVAL_MS rate limit was exceeded. Rate-limited
|
||||
// separately from arch_feed_wdt() so the LED blink pattern stays readable
|
||||
// (status_led error blink period is 250 ms) while HAL watchdog pokes can
|
||||
// still run at the much coarser WDT_FEED_INTERVAL_MS cadence.
|
||||
this->last_status_led_service_ = time;
|
||||
if (status_led::global_status_led == nullptr)
|
||||
return;
|
||||
auto *sl = status_led::global_status_led;
|
||||
uint8_t sl_state = sl->get_component_state() & COMPONENT_STATE_MASK;
|
||||
if (sl_state == COMPONENT_STATE_LOOP_DONE) {
|
||||
// status_led only transitions to LOOP_DONE from inside its own loop() (after the
|
||||
// first idle-path dispatch), so its pin is already initialized by pre_setup() and
|
||||
// its setup() has already run. Re-dispatch only if an error or warning bit has been
|
||||
// set since; otherwise skip entirely.
|
||||
if ((this->app_state_ & STATUS_LED_MASK) == 0)
|
||||
return;
|
||||
sl->enable_loop();
|
||||
} else if (sl_state != COMPONENT_STATE_LOOP) {
|
||||
// CONSTRUCTION/SETUP/FAILED: not our job — App::setup() drives the lifecycle.
|
||||
return;
|
||||
}
|
||||
sl->loop();
|
||||
}
|
||||
#endif
|
||||
|
||||
bool Application::any_component_has_status_flag_(uint8_t flag) const {
|
||||
// Walk all components (not just looping ones) so non-looping components'
|
||||
// status bits are respected. Only called from the slow-path clear helpers
|
||||
@@ -481,32 +509,7 @@ void Application::enable_pending_loops_() {
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
bool Application::register_socket(struct lwip_sock *sock) {
|
||||
// It modifies monitored_sockets_ without locking — must only be called from the main loop.
|
||||
if (sock == nullptr)
|
||||
return false;
|
||||
esphome_lwip_hook_socket(sock);
|
||||
this->monitored_sockets_.push_back(sock);
|
||||
return true;
|
||||
}
|
||||
|
||||
void Application::unregister_socket(struct lwip_sock *sock) {
|
||||
// It modifies monitored_sockets_ without locking — must only be called from the main loop.
|
||||
for (size_t i = 0; i < this->monitored_sockets_.size(); i++) {
|
||||
if (this->monitored_sockets_[i] != sock)
|
||||
continue;
|
||||
|
||||
// Swap with last element and pop - O(1) removal since order doesn't matter.
|
||||
// No need to unhook the netconn callback — all LwIP sockets share the same
|
||||
// static event_callback, and the socket will be closed by the caller.
|
||||
if (i < this->monitored_sockets_.size() - 1)
|
||||
this->monitored_sockets_[i] = this->monitored_sockets_.back();
|
||||
this->monitored_sockets_.pop_back();
|
||||
return;
|
||||
}
|
||||
}
|
||||
#elif defined(USE_HOST)
|
||||
#ifdef USE_HOST
|
||||
bool Application::register_socket_fd(int fd) {
|
||||
// WARNING: This function is NOT thread-safe and must only be called from the main loop
|
||||
// It modifies socket_fds_ and related variables without locking
|
||||
|
||||
+140
-383
@@ -39,78 +39,7 @@
|
||||
#include "esphome/components/runtime_stats/runtime_stats.h"
|
||||
#endif
|
||||
#include "esphome/core/wake.h"
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
#include "esphome/components/switch/switch.h"
|
||||
#endif
|
||||
#ifdef USE_BUTTON
|
||||
#include "esphome/components/button/button.h"
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
#endif
|
||||
#ifdef USE_FAN
|
||||
#include "esphome/components/fan/fan.h"
|
||||
#endif
|
||||
#ifdef USE_CLIMATE
|
||||
#include "esphome/components/climate/climate.h"
|
||||
#endif
|
||||
#ifdef USE_LIGHT
|
||||
#include "esphome/components/light/light_state.h"
|
||||
#endif
|
||||
#ifdef USE_COVER
|
||||
#include "esphome/components/cover/cover.h"
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
#include "esphome/components/number/number.h"
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATE
|
||||
#include "esphome/components/datetime/date_entity.h"
|
||||
#endif
|
||||
#ifdef USE_DATETIME_TIME
|
||||
#include "esphome/components/datetime/time_entity.h"
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
#include "esphome/components/datetime/datetime_entity.h"
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
#include "esphome/components/text/text.h"
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
#include "esphome/components/select/select.h"
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
#include "esphome/components/lock/lock.h"
|
||||
#endif
|
||||
#ifdef USE_VALVE
|
||||
#include "esphome/components/valve/valve.h"
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
#include "esphome/components/media_player/media_player.h"
|
||||
#endif
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
#include "esphome/components/alarm_control_panel/alarm_control_panel.h"
|
||||
#endif
|
||||
#ifdef USE_WATER_HEATER
|
||||
#include "esphome/components/water_heater/water_heater.h"
|
||||
#endif
|
||||
#ifdef USE_INFRARED
|
||||
#include "esphome/components/infrared/infrared.h"
|
||||
#endif
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
#include "esphome/components/serial_proxy/serial_proxy.h"
|
||||
#endif
|
||||
#ifdef USE_EVENT
|
||||
#include "esphome/components/event/event.h"
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
#include "esphome/components/update/update_entity.h"
|
||||
#endif
|
||||
#include "esphome/core/entity_includes.h"
|
||||
|
||||
namespace esphome::socket {
|
||||
#ifdef USE_HOST
|
||||
@@ -190,93 +119,16 @@ class Application {
|
||||
void set_current_component(Component *component) { this->current_component_ = component; }
|
||||
Component *get_current_component() { return this->current_component_; }
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
void register_binary_sensor(binary_sensor::BinarySensor *binary_sensor) {
|
||||
this->binary_sensors_.push_back(binary_sensor);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
void register_sensor(sensor::Sensor *sensor) { this->sensors_.push_back(sensor); }
|
||||
#endif
|
||||
|
||||
#ifdef USE_SWITCH
|
||||
void register_switch(switch_::Switch *a_switch) { this->switches_.push_back(a_switch); }
|
||||
#endif
|
||||
|
||||
#ifdef USE_BUTTON
|
||||
void register_button(button::Button *button) { this->buttons_.push_back(button); }
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
void register_text_sensor(text_sensor::TextSensor *sensor) { this->text_sensors_.push_back(sensor); }
|
||||
#endif
|
||||
|
||||
#ifdef USE_FAN
|
||||
void register_fan(fan::Fan *state) { this->fans_.push_back(state); }
|
||||
#endif
|
||||
|
||||
#ifdef USE_COVER
|
||||
void register_cover(cover::Cover *cover) { this->covers_.push_back(cover); }
|
||||
#endif
|
||||
|
||||
#ifdef USE_CLIMATE
|
||||
void register_climate(climate::Climate *climate) { this->climates_.push_back(climate); }
|
||||
#endif
|
||||
|
||||
#ifdef USE_LIGHT
|
||||
void register_light(light::LightState *light) { this->lights_.push_back(light); }
|
||||
#endif
|
||||
|
||||
#ifdef USE_NUMBER
|
||||
void register_number(number::Number *number) { this->numbers_.push_back(number); }
|
||||
#endif
|
||||
|
||||
#ifdef USE_DATETIME_DATE
|
||||
void register_date(datetime::DateEntity *date) { this->dates_.push_back(date); }
|
||||
#endif
|
||||
|
||||
#ifdef USE_DATETIME_TIME
|
||||
void register_time(datetime::TimeEntity *time) { this->times_.push_back(time); }
|
||||
#endif
|
||||
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
void register_datetime(datetime::DateTimeEntity *datetime) { this->datetimes_.push_back(datetime); }
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT
|
||||
void register_text(text::Text *text) { this->texts_.push_back(text); }
|
||||
#endif
|
||||
|
||||
#ifdef USE_SELECT
|
||||
void register_select(select::Select *select) { this->selects_.push_back(select); }
|
||||
#endif
|
||||
|
||||
#ifdef USE_LOCK
|
||||
void register_lock(lock::Lock *a_lock) { this->locks_.push_back(a_lock); }
|
||||
#endif
|
||||
|
||||
#ifdef USE_VALVE
|
||||
void register_valve(valve::Valve *valve) { this->valves_.push_back(valve); }
|
||||
#endif
|
||||
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
void register_media_player(media_player::MediaPlayer *media_player) { this->media_players_.push_back(media_player); }
|
||||
#endif
|
||||
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
void register_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) {
|
||||
this->alarm_control_panels_.push_back(a_alarm_control_panel);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_WATER_HEATER
|
||||
void register_water_heater(water_heater::WaterHeater *water_heater) { this->water_heaters_.push_back(water_heater); }
|
||||
#endif
|
||||
|
||||
#ifdef USE_INFRARED
|
||||
void register_infrared(infrared::Infrared *infrared) { this->infrareds_.push_back(infrared); }
|
||||
#endif
|
||||
// Entity register methods (generated from entity_types.h)
|
||||
// NOLINTBEGIN(bugprone-macro-parentheses)
|
||||
#define ENTITY_TYPE_(type, singular, plural, count, upper) \
|
||||
void register_##singular(type *obj) { this->plural##_.push_back(obj); }
|
||||
#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) \
|
||||
ENTITY_TYPE_(type, singular, plural, count, upper)
|
||||
#include "esphome/core/entity_types.h"
|
||||
#undef ENTITY_TYPE_
|
||||
#undef ENTITY_CONTROLLER_TYPE_
|
||||
// NOLINTEND(bugprone-macro-parentheses)
|
||||
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
void register_serial_proxy(serial_proxy::SerialProxy *proxy) {
|
||||
@@ -285,14 +137,6 @@ class Application {
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_EVENT
|
||||
void register_event(event::Event *event) { this->events_.push_back(event); }
|
||||
#endif
|
||||
|
||||
#ifdef USE_UPDATE
|
||||
void register_update(update::UpdateEntity *update) { this->updates_.push_back(update); }
|
||||
#endif
|
||||
|
||||
/// Reserve space for components to avoid memory fragmentation
|
||||
|
||||
/// Set up all the registered components. Call this at the end of your setup() function.
|
||||
@@ -385,23 +229,50 @@ class Application {
|
||||
|
||||
void schedule_dump_config() { this->dump_config_at_ = 0; }
|
||||
|
||||
/// Minimum interval between real arch_feed_wdt() calls. Chosen to keep the
|
||||
/// rate of HAL pokes low while still being small enough that any plausible
|
||||
/// watchdog timeout (seconds) has orders of magnitude of safety margin.
|
||||
static constexpr uint32_t WDT_FEED_INTERVAL_MS = 3;
|
||||
/// Minimum interval between real arch_feed_wdt() calls. Sized so the outer
|
||||
/// feed in Application::loop() is effectively rate-limited across both the
|
||||
/// normal ~62 Hz cadence and worst-case wake-storm scenarios (e.g. external
|
||||
/// stacks like OpenThread posting frequent wake notifications). Component
|
||||
/// loops and scheduler items still feed after every op, so any op exceeding
|
||||
/// this threshold triggers a real feed naturally.
|
||||
/// Safety margins vs. platform watchdog timeouts:
|
||||
/// - ESP32 task WDT default (5 s): ~16x
|
||||
/// - ESP8266 soft WDT (~1.6 s): ~5x <-- floor case; any future change
|
||||
/// must keep comfortable margin here
|
||||
/// - ESP8266 HW WDT (~6 s): ~20x
|
||||
static constexpr uint32_t WDT_FEED_INTERVAL_MS = 300;
|
||||
|
||||
/// Feed the task watchdog. Cold entry — callers without a millis()
|
||||
/// timestamp in hand. Out of line to keep call sites tiny.
|
||||
void feed_wdt();
|
||||
|
||||
#ifdef USE_STATUS_LED
|
||||
/// Dispatch interval for the status LED update. Deliberately shorter than
|
||||
/// WDT_FEED_INTERVAL_MS because the status LED error blink has a 250 ms
|
||||
/// period (status_led.cpp:ERROR_PERIOD_MS) and a 150 ms on-window; the
|
||||
/// dispatch cadence must be short enough to render that blink without
|
||||
/// aliasing. Sampling every 100 ms yields an on/off observation inside
|
||||
/// every error period with headroom for the 250 ms warning on-window.
|
||||
static constexpr uint32_t STATUS_LED_DISPATCH_INTERVAL_MS = 100;
|
||||
#endif
|
||||
|
||||
/// Feed the task watchdog, hot entry. Callers that already have a
|
||||
/// millis() timestamp pay only a load + sub + branch on the common
|
||||
/// (no-op) path. The actual arch feed + status LED update live in
|
||||
/// feed_wdt_slow_.
|
||||
/// (no-op) path. The actual arch feed lives in feed_wdt_slow_.
|
||||
/// When USE_STATUS_LED is compiled in, also gates a separate (shorter)
|
||||
/// interval for dispatching status_led so the LED blink pattern stays
|
||||
/// readable even though arch_feed_wdt pokes are now rate-limited at
|
||||
/// WDT_FEED_INTERVAL_MS. The two rate limits are independent so raising
|
||||
/// WDT_FEED_INTERVAL_MS does not distort the LED cadence.
|
||||
void ESPHOME_ALWAYS_INLINE feed_wdt_with_time(uint32_t time) {
|
||||
if (static_cast<uint32_t>(time - this->last_wdt_feed_) > WDT_FEED_INTERVAL_MS) [[unlikely]] {
|
||||
this->feed_wdt_slow_(time);
|
||||
}
|
||||
#ifdef USE_STATUS_LED
|
||||
if (static_cast<uint32_t>(time - this->last_status_led_service_) > STATUS_LED_DISPATCH_INTERVAL_MS) [[unlikely]] {
|
||||
this->service_status_led_slow_(time);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void reboot();
|
||||
@@ -456,120 +327,31 @@ class Application {
|
||||
#ifdef USE_AREAS
|
||||
const auto &get_areas() { return this->areas_; }
|
||||
#endif
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
auto &get_binary_sensors() const { return this->binary_sensors_; }
|
||||
GET_ENTITY_METHOD(binary_sensor::BinarySensor, binary_sensor, binary_sensors)
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
auto &get_switches() const { return this->switches_; }
|
||||
GET_ENTITY_METHOD(switch_::Switch, switch, switches)
|
||||
#endif
|
||||
#ifdef USE_BUTTON
|
||||
auto &get_buttons() const { return this->buttons_; }
|
||||
GET_ENTITY_METHOD(button::Button, button, buttons)
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
auto &get_sensors() const { return this->sensors_; }
|
||||
GET_ENTITY_METHOD(sensor::Sensor, sensor, sensors)
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
auto &get_text_sensors() const { return this->text_sensors_; }
|
||||
GET_ENTITY_METHOD(text_sensor::TextSensor, text_sensor, text_sensors)
|
||||
#endif
|
||||
#ifdef USE_FAN
|
||||
auto &get_fans() const { return this->fans_; }
|
||||
GET_ENTITY_METHOD(fan::Fan, fan, fans)
|
||||
#endif
|
||||
#ifdef USE_COVER
|
||||
auto &get_covers() const { return this->covers_; }
|
||||
GET_ENTITY_METHOD(cover::Cover, cover, covers)
|
||||
#endif
|
||||
#ifdef USE_LIGHT
|
||||
auto &get_lights() const { return this->lights_; }
|
||||
GET_ENTITY_METHOD(light::LightState, light, lights)
|
||||
#endif
|
||||
#ifdef USE_CLIMATE
|
||||
auto &get_climates() const { return this->climates_; }
|
||||
GET_ENTITY_METHOD(climate::Climate, climate, climates)
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
auto &get_numbers() const { return this->numbers_; }
|
||||
GET_ENTITY_METHOD(number::Number, number, numbers)
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATE
|
||||
auto &get_dates() const { return this->dates_; }
|
||||
GET_ENTITY_METHOD(datetime::DateEntity, date, dates)
|
||||
#endif
|
||||
#ifdef USE_DATETIME_TIME
|
||||
auto &get_times() const { return this->times_; }
|
||||
GET_ENTITY_METHOD(datetime::TimeEntity, time, times)
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
auto &get_datetimes() const { return this->datetimes_; }
|
||||
GET_ENTITY_METHOD(datetime::DateTimeEntity, datetime, datetimes)
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
auto &get_texts() const { return this->texts_; }
|
||||
GET_ENTITY_METHOD(text::Text, text, texts)
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
auto &get_selects() const { return this->selects_; }
|
||||
GET_ENTITY_METHOD(select::Select, select, selects)
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
auto &get_locks() const { return this->locks_; }
|
||||
GET_ENTITY_METHOD(lock::Lock, lock, locks)
|
||||
#endif
|
||||
#ifdef USE_VALVE
|
||||
auto &get_valves() const { return this->valves_; }
|
||||
GET_ENTITY_METHOD(valve::Valve, valve, valves)
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
auto &get_media_players() const { return this->media_players_; }
|
||||
GET_ENTITY_METHOD(media_player::MediaPlayer, media_player, media_players)
|
||||
#endif
|
||||
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
auto &get_alarm_control_panels() const { return this->alarm_control_panels_; }
|
||||
GET_ENTITY_METHOD(alarm_control_panel::AlarmControlPanel, alarm_control_panel, alarm_control_panels)
|
||||
#endif
|
||||
|
||||
#ifdef USE_WATER_HEATER
|
||||
auto &get_water_heaters() const { return this->water_heaters_; }
|
||||
GET_ENTITY_METHOD(water_heater::WaterHeater, water_heater, water_heaters)
|
||||
#endif
|
||||
|
||||
#ifdef USE_INFRARED
|
||||
auto &get_infrareds() const { return this->infrareds_; }
|
||||
GET_ENTITY_METHOD(infrared::Infrared, infrared, infrareds)
|
||||
#endif
|
||||
// Entity getter methods (generated from entity_types.h)
|
||||
// NOLINTBEGIN(bugprone-macro-parentheses)
|
||||
#define ENTITY_TYPE_(type, singular, plural, count, upper) \
|
||||
auto &get_##plural() const { return this->plural##_; } \
|
||||
GET_ENTITY_METHOD(type, singular, plural)
|
||||
#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) \
|
||||
ENTITY_TYPE_(type, singular, plural, count, upper)
|
||||
#include "esphome/core/entity_types.h"
|
||||
#undef ENTITY_TYPE_
|
||||
#undef ENTITY_CONTROLLER_TYPE_
|
||||
// NOLINTEND(bugprone-macro-parentheses)
|
||||
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
auto &get_serial_proxies() const { return this->serial_proxies_; }
|
||||
#endif
|
||||
|
||||
#ifdef USE_EVENT
|
||||
auto &get_events() const { return this->events_; }
|
||||
GET_ENTITY_METHOD(event::Event, event, events)
|
||||
#endif
|
||||
|
||||
#ifdef USE_UPDATE
|
||||
auto &get_updates() const { return this->updates_; }
|
||||
GET_ENTITY_METHOD(update::UpdateEntity, update, updates)
|
||||
#endif
|
||||
|
||||
Scheduler scheduler;
|
||||
|
||||
/// Register/unregister a socket to be monitored for read events.
|
||||
/// WARNING: These functions are NOT thread-safe. They must only be called from the main loop.
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
/// Fast select path: hooks netconn callback and registers for monitoring.
|
||||
/// @return true if registration was successful, false if sock is null
|
||||
bool register_socket(struct lwip_sock *sock);
|
||||
void unregister_socket(struct lwip_sock *sock);
|
||||
#elif defined(USE_HOST)
|
||||
/// Fallback select() path: monitors file descriptors.
|
||||
#ifdef USE_HOST
|
||||
/// Register/unregister a socket file descriptor with the host select() fallback loop.
|
||||
/// USE_LWIP_FAST_SELECT builds do not use this API — sockets hook the lwIP netconn
|
||||
/// event_callback directly (see socket.h hook_fd_for_fast_select) and rely on FreeRTOS
|
||||
/// task notifications for wake-up.
|
||||
/// NOTE: File descriptors >= FD_SETSIZE (typically 10 on ESP) will be rejected with an error.
|
||||
/// WARNING: These functions are NOT thread-safe. They must only be called from the main loop.
|
||||
/// @return true if registration was successful, false if fd exceeds limits
|
||||
bool register_socket_fd(int fd);
|
||||
void unregister_socket_fd(int fd);
|
||||
@@ -579,9 +361,12 @@ class Application {
|
||||
/// @see esphome::wake_loop_threadsafe() in wake.h for platform details.
|
||||
void wake_loop_threadsafe() { esphome::wake_loop_threadsafe(); }
|
||||
|
||||
#ifdef USE_ESP32
|
||||
/// Wake from ISR (ESP32 only).
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
/// Wake from ISR (ESP32 and LibreTiny).
|
||||
static void IRAM_ATTR wake_loop_isrsafe(BaseType_t *px) { esphome::wake_loop_isrsafe(px); }
|
||||
#elif defined(USE_ESP8266)
|
||||
/// Wake from ISR (ESP8266). No task_woken arg — no FreeRTOS. Caller must be IRAM_ATTR.
|
||||
static void IRAM_ATTR ESPHOME_ALWAYS_INLINE wake_loop_isrsafe() { esphome::wake_loop_isrsafe(); }
|
||||
#endif
|
||||
|
||||
/// Wake from any context (ISR, thread, callback).
|
||||
@@ -641,7 +426,7 @@ class Application {
|
||||
void enable_component_loop_(Component *component);
|
||||
void enable_pending_loops_();
|
||||
void activate_looping_component_(uint16_t index);
|
||||
inline void ESPHOME_ALWAYS_INLINE before_loop_tasks_(uint32_t loop_start_time);
|
||||
inline uint32_t ESPHOME_ALWAYS_INLINE before_loop_tasks_(uint32_t loop_start_time);
|
||||
inline void ESPHOME_ALWAYS_INLINE after_loop_tasks_() { this->in_loop_ = false; }
|
||||
|
||||
/// Process dump_config output one component per loop iteration.
|
||||
@@ -649,11 +434,21 @@ class Application {
|
||||
/// Caller must ensure dump_config_at_ < components_.size().
|
||||
void __attribute__((noinline)) process_dump_config_();
|
||||
|
||||
/// Slow path for feed_wdt(): actually calls arch_feed_wdt(), updates
|
||||
/// last_wdt_feed_, and re-dispatches the status LED. Out of line so the
|
||||
/// inline wrapper stays tiny.
|
||||
/// Slow path for feed_wdt(): actually calls arch_feed_wdt() and updates
|
||||
/// last_wdt_feed_. Out of line so the inline wrapper stays tiny. Does NOT
|
||||
/// touch status_led — that's gated separately via service_status_led_slow_
|
||||
/// because the two rate limits have very different safe ranges (~ seconds
|
||||
/// for WDT, < 250 ms for LED blink rendering).
|
||||
void feed_wdt_slow_(uint32_t time);
|
||||
|
||||
#ifdef USE_STATUS_LED
|
||||
/// Slow path for the status_led dispatch rate limit. Runs the status_led
|
||||
/// component's loop() based on its state (LOOP / LOOP_DONE with status
|
||||
/// bits set), and updates last_status_led_service_. Out of line to keep
|
||||
/// the feed_wdt_with_time hot path a couple of load+branch sequences.
|
||||
void service_status_led_slow_(uint32_t time);
|
||||
#endif
|
||||
|
||||
/// Perform a delay while also monitoring socket file descriptors for readiness
|
||||
#ifdef USE_HOST
|
||||
// select() fallback path is too complex to inline (host platform)
|
||||
@@ -690,9 +485,7 @@ class Application {
|
||||
// and active_end_ is incremented
|
||||
// - This eliminates branch mispredictions from flag checking in the hot loop
|
||||
FixedVector<Component *> looping_components_{};
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
std::vector<struct lwip_sock *> monitored_sockets_; // Cached lwip_sock pointers for direct rcvevent read
|
||||
#elif defined(USE_HOST)
|
||||
#ifdef USE_HOST
|
||||
std::vector<int> socket_fds_; // Vector of all monitored socket file descriptors
|
||||
#endif
|
||||
#ifdef USE_HOST
|
||||
@@ -707,6 +500,10 @@ class Application {
|
||||
uint32_t last_loop_{0};
|
||||
uint32_t loop_component_start_time_{0};
|
||||
uint32_t last_wdt_feed_{0}; // millis() of most recent arch_feed_wdt(); rate-limits feed_wdt() hot path
|
||||
#ifdef USE_STATUS_LED
|
||||
// millis() of most recent status_led dispatch; rate-limits independently of last_wdt_feed_
|
||||
uint32_t last_status_led_service_{0};
|
||||
#endif
|
||||
|
||||
#ifdef USE_HOST
|
||||
int max_fd_{-1}; // Highest file descriptor number for select()
|
||||
@@ -743,79 +540,19 @@ class Application {
|
||||
#ifdef USE_AREAS
|
||||
StaticVector<Area *, ESPHOME_AREA_COUNT> areas_{};
|
||||
#endif
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
StaticVector<binary_sensor::BinarySensor *, ESPHOME_ENTITY_BINARY_SENSOR_COUNT> binary_sensors_{};
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
StaticVector<switch_::Switch *, ESPHOME_ENTITY_SWITCH_COUNT> switches_{};
|
||||
#endif
|
||||
#ifdef USE_BUTTON
|
||||
StaticVector<button::Button *, ESPHOME_ENTITY_BUTTON_COUNT> buttons_{};
|
||||
#endif
|
||||
#ifdef USE_EVENT
|
||||
StaticVector<event::Event *, ESPHOME_ENTITY_EVENT_COUNT> events_{};
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
StaticVector<sensor::Sensor *, ESPHOME_ENTITY_SENSOR_COUNT> sensors_{};
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
StaticVector<text_sensor::TextSensor *, ESPHOME_ENTITY_TEXT_SENSOR_COUNT> text_sensors_{};
|
||||
#endif
|
||||
#ifdef USE_FAN
|
||||
StaticVector<fan::Fan *, ESPHOME_ENTITY_FAN_COUNT> fans_{};
|
||||
#endif
|
||||
#ifdef USE_COVER
|
||||
StaticVector<cover::Cover *, ESPHOME_ENTITY_COVER_COUNT> covers_{};
|
||||
#endif
|
||||
#ifdef USE_CLIMATE
|
||||
StaticVector<climate::Climate *, ESPHOME_ENTITY_CLIMATE_COUNT> climates_{};
|
||||
#endif
|
||||
#ifdef USE_LIGHT
|
||||
StaticVector<light::LightState *, ESPHOME_ENTITY_LIGHT_COUNT> lights_{};
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
StaticVector<number::Number *, ESPHOME_ENTITY_NUMBER_COUNT> numbers_{};
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATE
|
||||
StaticVector<datetime::DateEntity *, ESPHOME_ENTITY_DATE_COUNT> dates_{};
|
||||
#endif
|
||||
#ifdef USE_DATETIME_TIME
|
||||
StaticVector<datetime::TimeEntity *, ESPHOME_ENTITY_TIME_COUNT> times_{};
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
StaticVector<datetime::DateTimeEntity *, ESPHOME_ENTITY_DATETIME_COUNT> datetimes_{};
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
StaticVector<select::Select *, ESPHOME_ENTITY_SELECT_COUNT> selects_{};
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
StaticVector<text::Text *, ESPHOME_ENTITY_TEXT_COUNT> texts_{};
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
StaticVector<lock::Lock *, ESPHOME_ENTITY_LOCK_COUNT> locks_{};
|
||||
#endif
|
||||
#ifdef USE_VALVE
|
||||
StaticVector<valve::Valve *, ESPHOME_ENTITY_VALVE_COUNT> valves_{};
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
StaticVector<media_player::MediaPlayer *, ESPHOME_ENTITY_MEDIA_PLAYER_COUNT> media_players_{};
|
||||
#endif
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
StaticVector<alarm_control_panel::AlarmControlPanel *, ESPHOME_ENTITY_ALARM_CONTROL_PANEL_COUNT>
|
||||
alarm_control_panels_{};
|
||||
#endif
|
||||
#ifdef USE_WATER_HEATER
|
||||
StaticVector<water_heater::WaterHeater *, ESPHOME_ENTITY_WATER_HEATER_COUNT> water_heaters_{};
|
||||
#endif
|
||||
#ifdef USE_INFRARED
|
||||
StaticVector<infrared::Infrared *, ESPHOME_ENTITY_INFRARED_COUNT> infrareds_{};
|
||||
#endif
|
||||
// Entity StaticVector fields (generated from entity_types.h)
|
||||
// NOLINTBEGIN(bugprone-macro-parentheses)
|
||||
#define ENTITY_TYPE_(type, singular, plural, count, upper) StaticVector<type *, count> plural##_{};
|
||||
#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) \
|
||||
ENTITY_TYPE_(type, singular, plural, count, upper)
|
||||
#include "esphome/core/entity_types.h"
|
||||
#undef ENTITY_TYPE_
|
||||
#undef ENTITY_CONTROLLER_TYPE_
|
||||
// NOLINTEND(bugprone-macro-parentheses)
|
||||
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
StaticVector<serial_proxy::SerialProxy *, SERIAL_PROXY_COUNT> serial_proxies_{};
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
StaticVector<update::UpdateEntity *, ESPHOME_ENTITY_UPDATE_COUNT> updates_{};
|
||||
#endif
|
||||
};
|
||||
|
||||
/// Global storage of Application pointer - only one Application can exist.
|
||||
@@ -845,18 +582,15 @@ inline void Application::drain_wake_notifications_() {
|
||||
}
|
||||
#endif // USE_HOST
|
||||
|
||||
inline void ESPHOME_ALWAYS_INLINE Application::before_loop_tasks_(uint32_t loop_start_time) {
|
||||
inline uint32_t ESPHOME_ALWAYS_INLINE Application::before_loop_tasks_(uint32_t loop_start_time) {
|
||||
#ifdef USE_HOST
|
||||
// Drain wake notifications first to clear socket for next wake
|
||||
this->drain_wake_notifications_();
|
||||
#endif
|
||||
|
||||
// Process scheduled tasks. Scheduler::call now feeds the watchdog itself
|
||||
// after each scheduled item that actually runs, so we no longer need an
|
||||
// unconditional feed here — when Scheduler::call has no work to do, the
|
||||
// only elapsed time is a sleep wake + a few instructions, and when it does
|
||||
// have work, it fed the wdt as it went.
|
||||
this->scheduler.call(loop_start_time);
|
||||
// Scheduler::call feeds the WDT per item and returns the timestamp of the
|
||||
// last fired item, or the input unchanged when nothing ran.
|
||||
uint32_t last_op_end_time = this->scheduler.call(loop_start_time);
|
||||
|
||||
// Process any pending enable_loop requests from ISRs
|
||||
// This must be done before marking in_loop_ = true to avoid race conditions
|
||||
@@ -874,14 +608,35 @@ inline void ESPHOME_ALWAYS_INLINE Application::before_loop_tasks_(uint32_t loop_
|
||||
|
||||
// Mark that we're in the loop for safe reentrant modifications
|
||||
this->in_loop_ = true;
|
||||
return last_op_end_time;
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(clang-diagnostic-unknown-attributes)
|
||||
inline void ESPHOME_ALWAYS_INLINE __attribute__((optimize("O2"))) Application::loop() {
|
||||
#ifdef USE_RUNTIME_STATS
|
||||
// Capture the start of the active (non-sleeping) portion of this iteration.
|
||||
// Used to derive main-loop overhead = active time − Σ(component time) −
|
||||
// before/tail splits recorded below.
|
||||
uint32_t loop_active_start_us = micros();
|
||||
// Snapshot the cumulative component-recorded time so we can subtract the
|
||||
// slice that the scheduler spends inside its own WarnIfComponentBlockingGuard
|
||||
// (scheduler.cpp) — that time is already counted in per-component stats,
|
||||
// so charging it again to "before" would double-count.
|
||||
uint64_t loop_recorded_snap = ComponentRuntimeStats::global_recorded_us;
|
||||
#endif
|
||||
// Get the initial loop time at the start
|
||||
uint32_t last_op_end_time = millis();
|
||||
|
||||
this->before_loop_tasks_(last_op_end_time);
|
||||
// Returned timestamp keeps us monotonic with last_wdt_feed_ (advanced by
|
||||
// the scheduler's per-item feeds) without an extra millis() call.
|
||||
last_op_end_time = this->before_loop_tasks_(last_op_end_time);
|
||||
// Guarantee a WDT touch every tick — covers configs with no looping
|
||||
// components and no scheduler work, where the per-item / per-component
|
||||
// feeds never fire. Rate-limited inline fast path, ~free when unneeded.
|
||||
this->feed_wdt_with_time(last_op_end_time);
|
||||
#ifdef USE_RUNTIME_STATS
|
||||
uint32_t loop_before_end_us = micros();
|
||||
uint64_t loop_before_scheduled_us = ComponentRuntimeStats::global_recorded_us - loop_recorded_snap;
|
||||
#endif
|
||||
|
||||
for (this->current_loop_index_ = 0; this->current_loop_index_ < this->looping_components_active_end_;
|
||||
this->current_loop_index_++) {
|
||||
@@ -900,12 +655,24 @@ inline void ESPHOME_ALWAYS_INLINE __attribute__((optimize("O2"))) Application::l
|
||||
this->feed_wdt_with_time(last_op_end_time);
|
||||
}
|
||||
|
||||
#ifdef USE_RUNTIME_STATS
|
||||
uint32_t loop_tail_start_us = micros();
|
||||
#endif
|
||||
this->after_loop_tasks_();
|
||||
|
||||
#ifdef USE_RUNTIME_STATS
|
||||
// Process any pending runtime stats printing after all components have run
|
||||
// This ensures stats printing doesn't affect component timing measurements
|
||||
if (global_runtime_stats != nullptr) {
|
||||
uint32_t loop_now_us = micros();
|
||||
// Subtract scheduled-component time from the "before" bucket so it is
|
||||
// not double-counted (it is already attributed to per-component stats).
|
||||
uint32_t loop_before_wall_us = loop_before_end_us - loop_active_start_us;
|
||||
uint32_t loop_before_overhead_us = loop_before_wall_us > loop_before_scheduled_us
|
||||
? loop_before_wall_us - static_cast<uint32_t>(loop_before_scheduled_us)
|
||||
: 0;
|
||||
global_runtime_stats->record_loop_active(loop_now_us - loop_active_start_us, loop_before_overhead_us,
|
||||
loop_now_us - loop_tail_start_us);
|
||||
global_runtime_stats->process_pending_stats(last_op_end_time);
|
||||
}
|
||||
#endif
|
||||
@@ -933,26 +700,16 @@ inline void ESPHOME_ALWAYS_INLINE __attribute__((optimize("O2"))) Application::l
|
||||
#ifndef USE_HOST
|
||||
inline void ESPHOME_ALWAYS_INLINE Application::yield_with_select_(uint32_t delay_ms) {
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
// Fast path (ESP32/LibreTiny): reads rcvevent directly from cached lwip_sock pointers.
|
||||
// Safe because this runs on the main loop which owns socket lifetime (create, read, close).
|
||||
// Fast path (ESP32/LibreTiny): FreeRTOS task notifications posted by the lwip
|
||||
// event_callback wrapper (see lwip_fast_select.c) are the single source of truth for
|
||||
// socket wake-ups. Every NETCONN_EVT_RCVPLUS posts an xTaskNotifyGive, so any notification
|
||||
// that lands between wakes keeps the counter non-zero (next ulTaskNotifyTake returns
|
||||
// immediately) or wakes a blocked Take directly. Additional wake sources:
|
||||
// wake_loop_threadsafe() from background tasks, and the delay_ms timeout.
|
||||
if (delay_ms == 0) [[unlikely]] {
|
||||
yield();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if any socket already has pending data before sleeping.
|
||||
// If a socket still has unread data (rcvevent > 0) but the task notification was already
|
||||
// consumed, ulTaskNotifyTake would block until timeout — adding up to delay_ms latency.
|
||||
// This scan preserves select() semantics: return immediately when any fd is ready.
|
||||
for (struct lwip_sock *sock : this->monitored_sockets_) {
|
||||
if (esphome_lwip_socket_has_data(sock)) {
|
||||
yield();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Sleep with instant wake via FreeRTOS task notification.
|
||||
// Woken by: callback wrapper (socket data), wake_loop_threadsafe() (background tasks), or timeout.
|
||||
#endif
|
||||
esphome::internal::wakeable_delay(delay_ms);
|
||||
}
|
||||
|
||||
@@ -62,6 +62,18 @@ template<typename T, typename... X> class TemplatableFn {
|
||||
!std::convertible_to<std::invoke_result_t<F, X...>, T> ||
|
||||
!std::default_initializable<F>) = delete;
|
||||
|
||||
// Reject raw (non-callable) values with a helpful diagnostic pointing at the Python-side fix.
|
||||
// TemplatableFn stores only a function pointer (4 bytes), so constants must be wrapped in a
|
||||
// stateless lambda by codegen. External components hitting this error should use
|
||||
// `cg.templatable(value, args, type)` in their Python __init__.py before passing to the setter.
|
||||
template<typename V> TemplatableFn(V) requires(!std::invocable<V, X...>) && (!std::convertible_to<V, T (*)(X...)>) {
|
||||
static_assert(sizeof(V) == 0, "Missing cg.templatable(...) in Python codegen for this TEMPLATABLE_VALUE "
|
||||
"field. The wrapper was always required; it worked by accident because the old "
|
||||
"TemplatableValue implicitly converted raw constants. TemplatableFn cannot. See "
|
||||
"https://developers.esphome.io/blog/2026/04/09/"
|
||||
"templatablefn-4-byte-templatable-storage-for-trivially-copyable-types/");
|
||||
}
|
||||
|
||||
bool has_value() const { return this->f_ != nullptr; }
|
||||
|
||||
T value(X... x) const { return this->f_ ? this->f_(x...) : T{}; }
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user